Repository: bitwarden/clients
Pull request #13706: [PM-9115] implement view data persistence in 2FA flows
Author: login: alec-livefront
Created: 2025-03-05T18:32:57Z | Merged: 2025-04-25T15:02:55Z

## 🎟️ Tracking

<!-- Paste the link to the Jira or GitHub issue or otherwise describe / point to where this change is coming from. -->

https://bitwarden.atlassian.net/browse/PM-9115?atlOrigin=eyJpIjoiYmQzN2IxZGIwYjFlNGM2ZDkyNDllNDg1YjUxNmRmNTUiLCJwIjoiaiJ9

## 📔 Objective

<!-- Describe what the purpose of this PR is, for example what bug you're fixing or new feature you're adding. -->

Adds persistence to the two factor authentication section of the extension login flow.

## 📸 Screenshots

<!-- Required for any UI changes; delete if not applicable. Use fixed width images for better display. -->

https://github.com/user-attachments/assets/22e31640-846f-40f6-b582-fc5407cb67d8


## ⏰ Reminders before review

- Contributor guidelines followed
- All formatters and local linters executed and passed
- Written new unit and / or integration tests where applicable
- Protected functional changes with optionality (feature flags)
- Used internationalization (i18n) for all UI strings
- CI builds passed
- Communicated to DevOps any deployment requirements
- Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team

## 🦮 Reviewer guidelines

<!-- Suggested interactions but feel free to use (or not) as you desire! -->

- 👍 (`:+1:`) or similar for great changes
- 📝 (`:memo:`) or ℹ️ (`:information_source:`) for notes or general info
- ❓ (`:question:`) for questions
- 🤔 (`:thinking:`) or 💭 (`:thought_balloon:`) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion
- 🎨 (`:art:`) for suggestions / improvements
- ❌ (`:x:`) or ⚠️ (`:warning:`) for more significant problems or concerns needing attention
- 🌱 (`:seedling:`) or ♻️ (`:recycle:`) for future improvements or indications of technical debt
- ⛏ (`:pick:`) for minor or nitpick changes



Diff:
diff --git a/apps/browser/src/auth/services/extension-two-factor-auth-email-component.service.spec.ts b/apps/browser/src/auth/services/extension-two-factor-auth-email-component.service.spec.ts
index 223375fd9033..01a0129d0e52 100644
--- a/apps/browser/src/auth/services/extension-two-factor-auth-email-component.service.spec.ts
+++ b/apps/browser/src/auth/services/extension-two-factor-auth-email-component.service.spec.ts
@@ -1,5 +1,6 @@
 import { MockProxy, mock } from "jest-mock-extended";
 
+import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
 import { DialogService } from "@bitwarden/components";
 
 // Must mock modules before importing
@@ -26,22 +27,26 @@ describe("ExtensionTwoFactorAuthEmailComponentService", () => {
 
   let dialogService: MockProxy<DialogService>;
   let window: MockProxy<Window>;
+  let configService: MockProxy<ConfigService>;
 
   beforeEach(() => {
     jest.clearAllMocks();
 
     dialogService = mock<DialogService>();
     window = mock<Window>();
+    configService = mock<ConfigService>();
 
     extensionTwoFactorAuthEmailComponentService = new ExtensionTwoFactorAuthEmailComponentService(
       dialogService,
       window,
+      configService,
     );
   });
 
   describe("openPopoutIfApprovedForEmail2fa", () => {
     it("should open a popout if the user confirms the warning to popout the extension when in the popup", async () => {
       // Arrange
+      configService.getFeatureFlag.mockResolvedValue(false);
       dialogService.openSimpleDialog.mockResolvedValue(true);
 
       jest.spyOn(BrowserPopupUtils, "inPopup").mockReturnValue(true);
@@ -61,6 +66,7 @@ describe("ExtensionTwoFactorAuthEmailComponentService", () => {
 
     it("should not open a popout if the user cancels the warning to popout the extension when in the popup", async () => {
       // Arrange
+      configService.getFeatureFlag.mockResolvedValue(false);
       dialogService.openSimpleDialog.mockResolvedValue(false);
 
       jest.spyOn(BrowserPopupUtils, "inPopup").mockReturnValue(true);
@@ -80,6 +86,7 @@ describe("ExtensionTwoFactorAuthEmailComponentService", () => {
 
     it("should not open a popout if not in the popup", async () => {
       // Arrange
+      configService.getFeatureFlag.mockResolvedValue(false);
       jest.spyOn(BrowserPopupUtils, "inPopup").mockReturnValue(false);
 
       // Act
@@ -89,5 +96,15 @@ describe("ExtensionTwoFactorAuthEmailComponentService", () => {
       expect(dialogService.openSimpleDialog).not.toHaveBeenCalled();
       expect(openTwoFactorAuthEmailPopout).not.toHaveBeenCalled();
     });
+
+    it("does not prompt or open a popout if the feature flag is enabled", async () => {
+      configService.getFeatureFlag.mockResolvedValue(true);
+      jest.spyOn(BrowserPopupUtils, "inPopup").mockReturnValue(true);
+
+      await extensionTwoFactorAuthEmailComponentService.openPopoutIfApprovedForEmail2fa();
+
+      expect(dialogService.openSimpleDialog).not.toHaveBeenCalled();
+      expect(openTwoFactorAuthEmailPopout).not.toHaveBeenCalled();
+    });
   });
 });
diff --git a/apps/browser/src/auth/services/extension-two-factor-auth-email-component.service.ts b/apps/browser/src/auth/services/extension-two-factor-auth-email-component.service.ts
index 5d8d269412e7..293d88c4e646 100644
--- a/apps/browser/src/auth/services/extension-two-factor-auth-email-component.service.ts
+++ b/apps/browser/src/auth/services/extension-two-factor-auth-email-component.service.ts
@@ -2,6 +2,8 @@ import {
   DefaultTwoFactorAuthEmailComponentService,
   TwoFactorAuthEmailComponentService,
 } from "@bitwarden/auth/angular";
+import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
+import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
 import { DialogService } from "@bitwarden/components";
 
 import { openTwoFactorAuthEmailPopout } from "../../auth/popup/utils/auth-popout-window";
@@ -15,11 +17,21 @@ export class ExtensionTwoFactorAuthEmailComponentService
   constructor(
     private dialogService: DialogService,
     private window: Window,
+    private configService: ConfigService,
   ) {
     super();
   }
 
   async openPopoutIfApprovedForEmail2fa(): Promise<void> {
+    const isTwoFactorFormPersistenceEnabled = await this.configService.getFeatureFlag(
+      FeatureFlag.PM9115_TwoFactorExtensionDataPersistence,
+    );
+
+    if (isTwoFactorFormPersistenceEnabled) {
+      // If the feature flag is enabled, we don't need to prompt the user to open the popout
+      return;
+    }
+
     if (BrowserPopupUtils.inPopup(this.window)) {
       const confirmed = await this.dialogService.openSimpleDialog({
         title: { key: "warning" },
diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts
index dad4e887a12f..00b8ae81cf93 100644
--- a/apps/browser/src/popup/services/services.module.ts
+++ b/apps/browser/src/popup/services/services.module.ts
@@ -557,7 +557,7 @@ const safeProviders: SafeProvider[] = [
   safeProvider({
     provide: TwoFactorAuthEmailComponentService,
     useClass: ExtensionTwoFactorAuthEmailComponentService,
-    deps: [DialogService, WINDOW],
+    deps: [DialogService, WINDOW, ConfigService],
   }),
   safeProvider({
     provide: TwoFactorAuthWebAuthnComponentService,
diff --git a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-authenticator.component.html b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-authenticator.component.html
index af3a8569efa3..b52af7b38207 100644
--- a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-authenticator.component.html
+++ b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-authenticator.component.html
@@ -1,6 +1,13 @@
 <ng-container>
   <bit-form-field>
     <bit-label>{{ "verificationCode" | i18n }}</bit-label>
-    <input bitInput type="text" appAutofocus appInputVerbatim [formControl]="tokenFormControl" />
+    <input
+      bitInput
+      type="text"
+      appAutofocus
+      appInputVerbatim
+      [formControl]="tokenFormControl"
+      (keyup)="onTokenChange($event)"
+    />
   </bit-form-field>
 </ng-container>
diff --git a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-authenticator.component.ts b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-authenticator.component.ts
index 333afc22d2a3..d8735d3fd547 100644
--- a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-authenticator.component.ts
+++ b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-authenticator.component.ts
@@ -1,5 +1,5 @@
 import { CommonModule } from "@angular/common";
-import { Component, Input } from "@angular/core";
+import { Component, Input, Output, EventEmitter } from "@angular/core";
 import { ReactiveFormsModule, FormsModule, FormControl } from "@angular/forms";
 
 import { JslibModule } from "@bitwarden/angular/jslib.module";
@@ -32,4 +32,10 @@ import {
 })
 export class TwoFactorAuthAuthenticatorComponent {
   @Input({ required: true }) tokenFormControl: FormControl | undefined = undefined;
+  @Output() tokenChange = new EventEmitter<{ token: string }>();
+
+  onTokenChange(event: Event) {
+    const tokenValue = (event.target as HTMLInputElement).value || "";
+    this.tokenChange.emit({ token: tokenValue });
+  }
 }
diff --git a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-component-email-cache.service.spec.ts b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-component-email-cache.service.spec.ts
new file mode 100644
index 000000000000..f3b904a4ea67
--- /dev/null
+++ b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-component-email-cache.service.spec.ts
@@ -0,0 +1,165 @@
+import { TestBed } from "@angular/core/testing";
+import { mock, MockProxy } from "jest-mock-extended";
+import { BehaviorSubject } from "rxjs";
+
+import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service";
+import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
+import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
+
+import {
+  TwoFactorAuthEmailComponentCache,
+  TwoFactorAuthEmailComponentCacheService,
+} from "./two-factor-auth-email-component-cache.service";
+
+describe("TwoFactorAuthEmailCache", () => {
+  describe("fromJSON", () => {
+    it("returns null when input is null", () => {
+      const result = TwoFactorAuthEmailComponentCache.fromJSON(null as any);
+      expect(result).toBeNull();
+    });
+
+    it("creates a TwoFactorAuthEmailCache instance from valid JSON", () => {
+      const jsonData = { emailSent: true };
+      const result = TwoFactorAuthEmailComponentCache.fromJSON(jsonData);
+
+      expect(result).not.toBeNull();
+      expect(result).toBeInstanceOf(TwoFactorAuthEmailComponentCache);
+      expect(result?.emailSent).toBe(true);
+    });
+  });
+});
+
+describe("TwoFactorAuthEmailComponentCacheService", () => {
+  let service: TwoFactorAuthEmailComponentCacheService;
+  let mockViewCacheService: MockProxy<ViewCacheService>;
+  let mockConfigService: MockProxy<ConfigService>;
+  let cacheData: BehaviorSubject<TwoFactorAuthEmailComponentCache | null>;
+  let mockSignal: any;
+
+  beforeEach(() => {
+    mockViewCacheService = mock<ViewCacheService>();
+    mockConfigService = mock<ConfigService>();
+    cacheData = new BehaviorSubject<TwoFactorAuthEmailComponentCache | null>(null);
+    mockSignal = jest.fn(() => cacheData.getValue());
+    mockSignal.set = jest.fn((value: TwoFactorAuthEmailComponentCache | null) =>
+      cacheData.next(value),
+    );
+    mockViewCacheService.signal.mockReturnValue(mockSignal);
+
+    TestBed.configureTestingModule({
+      providers: [
+        TwoFactorAuthEmailComponentCacheService,
+        { provide: ViewCacheService, useValue: mockViewCacheService },
+        { provide: ConfigService, useValue: mockConfigService },
+      ],
+    });
+
+    service = TestBed.inject(TwoFactorAuthEmailComponentCacheService);
+  });
+
+  it("creates the service", () => {
+    expect(service).toBeTruthy();
+  });
+
+  describe("init", () => {
+    it("sets featureEnabled to true when flag is enabled", async () => {
+      mockConfigService.getFeatureFlag.mockResolvedValue(true);
+
+      await service.init();
+
+      expect(mockConfigService.getFeatureFlag).toHaveBeenCalledWith(
+        FeatureFlag.PM9115_TwoFactorExtensionDataPersistence,
+      );
+
+      service.cacheData({ emailSent: true });
+      expect(mockSignal.set).toHaveBeenCalled();
+    });
+
+    it("sets featureEnabled to false when flag is disabled", async () => {
+      mockConfigService.getFeatureFlag.mockResolvedValue(false);
+
+      await service.init();
+
+      expect(mockConfigService.getFeatureFlag).toHaveBeenCalledWith(
+        FeatureFlag.PM9115_TwoFactorExtensionDataPersistence,
+      );
+
+      service.cacheData({ emailSent: true });
+      expect(mockSignal.set).not.toHaveBeenCalled();
+    });
+  });
+
+  describe("cacheData", () => {
+    beforeEach(async () => {
+      mockConfigService.getFeatureFlag.mockResolvedValue(true);
+      await service.init();
+    });
+
+    it("caches email sent state when feature is enabled", () => {
+      service.cacheData({ emailSent: true });
+
+      expect(mockSignal.set).toHaveBeenCalledWith({
+        emailSent: true,
+      });
+    });
+
+    it("does not cache data when feature is disabled", async () => {
+      mockConfigService.getFeatureFlag.mockResolvedValue(false);
+      await service.init();
+
+      service.cacheData({ emailSent: true });
+
+      expect(mockSignal.set).not.toHaveBeenCalled();
+    });
+  });
+
+  describe("clearCachedData", () => {
+    beforeEach(async () => {
+      mockConfigService.getFeatureFlag.mockResolvedValue(true);
+      await service.init();
+    });
+
+    it("clears cached data when feature is enabled", () => {
+      service.clearCachedData();
+
+      expect(mockSignal.set).toHaveBeenCalledWith(null);
+    });
+
+    it("does not clear cached data when feature is disabled", async () => {
+      mockConfigService.getFeatureFlag.mockResolvedValue(false);
+      await service.init();
+
+      service.clearCachedData();
+
+      expect(mockSignal.set).not.toHaveBeenCalled();
+    });
+  });
+
+  describe("getCachedData", () => {
+    beforeEach(async () => {
+      mockConfigService.getFeatureFlag.mockResolvedValue(true);
+      await service.init();
+    });
+
+    it("returns cached data when feature is enabled", () => {
+      const testData = new TwoFactorAuthEmailComponentCache();
+      testData.emailSent = true;
+      cacheData.next(testData);
+
+      const result = service.getCachedData();
+
+      expect(result).toEqual(testData);
+      expect(mockSignal).toHaveBeenCalled();
+    });
+
+    it("returns null when feature is disabled", async () => {
+      mockConfigService.getFeatureFlag.mockResolvedValue(false);
+      await service.init();
+
+      const result = service.getCachedData();
+
+      expect(result).toBeNull();
+      expect(mockSignal).not.toHaveBeenCalled();
+    });
+  });
+});
diff --git a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email-component-cache.service.spec.ts b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email-component-cache.service.spec.ts
new file mode 100644
index 000000000000..1613c0e4af87
--- /dev/null
+++ b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email-component-cache.service.spec.ts
@@ -0,0 +1,165 @@
+import { TestBed } from "@angular/core/testing";
+import { mock, MockProxy } from "jest-mock-extended";
+import { BehaviorSubject } from "rxjs";
+
+import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service";
+import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
+import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
+
+import {
+  TwoFactorAuthEmailComponentCache,
+  TwoFactorAuthEmailComponentCacheService,
+} from "./two-factor-auth-email-component-cache.service";
+
+describe("TwoFactorAuthEmailComponentCache", () => {
+  describe("fromJSON", () => {
+    it("returns null when input is null", () => {
+      const result = TwoFactorAuthEmailComponentCache.fromJSON(null as any);
+      expect(result).toBeNull();
+    });
+
+    it("creates a TwoFactorAuthEmailCache instance from valid JSON", () => {
+      const jsonData = { emailSent: true };
+      const result = TwoFactorAuthEmailComponentCache.fromJSON(jsonData);
+
+      expect(result).not.toBeNull();
+      expect(result).toBeInstanceOf(TwoFactorAuthEmailComponentCache);
+      expect(result?.emailSent).toBe(true);
+    });
+  });
+});
+
+describe("TwoFactorAuthEmailComponentCacheService", () => {
+  let service: TwoFactorAuthEmailComponentCacheService;
+  let mockViewCacheService: MockProxy<ViewCacheService>;
+  let mockConfigService: MockProxy<ConfigService>;
+  let cacheData: BehaviorSubject<TwoFactorAuthEmailComponentCache | null>;
+  let mockSignal: any;
+
+  beforeEach(() => {
+    mockViewCacheService = mock<ViewCacheService>();
+    mockConfigService = mock<ConfigService>();
+    cacheData = new BehaviorSubject<TwoFactorAuthEmailComponentCache | null>(null);
+    mockSignal = jest.fn(() => cacheData.getValue());
+    mockSignal.set = jest.fn((value: TwoFactorAuthEmailComponentCache | null) =>
+      cacheData.next(value),
+    );
+    mockViewCacheService.signal.mockReturnValue(mockSignal);
+
+    TestBed.configureTestingModule({
+      providers: [
+        TwoFactorAuthEmailComponentCacheService,
+        { provide: ViewCacheService, useValue: mockViewCacheService },
+        { provide: ConfigService, useValue: mockConfigService },
+      ],
+    });
+
+    service = TestBed.inject(TwoFactorAuthEmailComponentCacheService);
+  });
+
+  it("creates the service", () => {
+    expect(service).toBeTruthy();
+  });
+
+  describe("init", () => {
+    it("sets featureEnabled to true when flag is enabled", async () => {
+      mockConfigService.getFeatureFlag.mockResolvedValue(true);
+
+      await service.init();
+
+      expect(mockConfigService.getFeatureFlag).toHaveBeenCalledWith(
+        FeatureFlag.PM9115_TwoFactorExtensionDataPersistence,
+      );
+
+      service.cacheData({ emailSent: true });
+      expect(mockSignal.set).toHaveBeenCalled();
+    });
+
+    it("sets featureEnabled to false when flag is disabled", async () => {
+      mockConfigService.getFeatureFlag.mockResolvedValue(false);
+
+      await service.init();
+
+      expect(mockConfigService.getFeatureFlag).toHaveBeenCalledWith(
+        FeatureFlag.PM9115_TwoFactorExtensionDataPersistence,
+      );
+
+      service.cacheData({ emailSent: true });
+      expect(mockSignal.set).not.toHaveBeenCalled();
+    });
+  });
+
+  describe("cacheData", () => {
+    beforeEach(async () => {
+      mockConfigService.getFeatureFlag.mockResolvedValue(true);
+      await service.init();
+    });
+
+    it("caches email sent state when feature is enabled", () => {
+      service.cacheData({ emailSent: true });
+
+      expect(mockSignal.set).toHaveBeenCalledWith({
+        emailSent: true,
+      });
+    });
+
+    it("does not cache data when feature is disabled", async () => {
+      mockConfigService.getFeatureFlag.mockResolvedValue(false);
+      await service.init();
+
+      service.cacheData({ emailSent: true });
+
+      expect(mockSignal.set).not.toHaveBeenCalled();
+    });
+  });
+
+  describe("clearCachedData", () => {
+    beforeEach(async () => {
+      mockConfigService.getFeatureFlag.mockResolvedValue(true);
+      await service.init();
+    });
+
+    it("clears cached data when feature is enabled", () => {
+      service.clearCachedData();
+
+      expect(mockSignal.set).toHaveBeenCalledWith(null);
+    });
+
+    it("does not clear cached data when feature is disabled", async () => {
+      mockConfigService.getFeatureFlag.mockResolvedValue(false);
+      await service.init();
+
+      service.clearCachedData();
+
+      expect(mockSignal.set).not.toHaveBeenCalled();
+    });
+  });
+
+  describe("getCachedData", () => {
+    beforeEach(async () => {
+      mockConfigService.getFeatureFlag.mockResolvedValue(true);
+      await service.init();
+    });
+
+    it("returns cached data when feature is enabled", () => {
+      const testData = new TwoFactorAuthEmailComponentCache();
+      testData.emailSent = true;
+      cacheData.next(testData);
+
+      const result = service.getCachedData();
+
+      expect(result).toEqual(testData);
+      expect(mockSignal).toHaveBeenCalled();
+    });
+
+    it("returns null when feature is disabled", async () => {
+      mockConfigService.getFeatureFlag.mockResolvedValue(false);
+      await service.init();
+
+      const result = service.getCachedData();
+
+      expect(result).toBeNull();
+      expect(mockSignal).not.toHaveBeenCalled();
+    });
+  });
+});
diff --git a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email-component-cache.service.ts b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email-component-cache.service.ts
new file mode 100644
index 000000000000..e32b6cd1385a
--- /dev/null
+++ b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email-component-cache.service.ts
@@ -0,0 +1,95 @@
+import { inject, Injectable, WritableSignal } from "@angular/core";
+import { Jsonify } from "type-fest";
+
+import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service";
+import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
+import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
+
+/**
+ * The key for the email two factor auth component cache.
+ */
+export const TWO_FACTOR_AUTH_EMAIL_COMPONENT_CACHE_KEY = "two-factor-auth-email-component-cache";
+
+/**
+ * Cache model for the email two factor auth component.
+ */
+export class TwoFactorAuthEmailComponentCache {
+  emailSent: boolean = false;
+
+  static fromJSON(
+    obj: Partial<Jsonify<TwoFactorAuthEmailComponentCache>>,
+  ): TwoFactorAuthEmailComponentCache | null {
+    // Return null if the cache is empty
+    if (obj == null) {
+      return null;
+    }
+
+    return Object.assign(new TwoFactorAuthEmailComponentCache(), obj);
+  }
+}
+
+/**
+ * Cache service for the two factor auth email component.
+ */
+@Injectable()
+export class TwoFactorAuthEmailComponentCacheService {
+  private viewCacheService: ViewCacheService = inject(ViewCacheService);
+  private configService: ConfigService = inject(ConfigService);
+
+  /** True when the feature flag is enabled */
+  private featureEnabled: boolean = false;
+
+  /**
+   * Signal for the cached email state.
+   */
+  private emailCache: WritableSignal<TwoFactorAuthEmailComponentCache | null> =
+    this.viewCacheService.signal<TwoFactorAuthEmailComponentCache | null>({
+      key: TWO_FACTOR_AUTH_EMAIL_COMPONENT_CACHE_KEY,
+      initialValue: null,
+      deserializer: TwoFactorAuthEmailComponentCache.fromJSON,
+    });
+
+  /**
+   * Must be called once before interacting with the cached data.
+   */
+  async init() {
+    this.featureEnabled = await this.configService.getFeatureFlag(
+      FeatureFlag.PM9115_TwoFactorExtensionDataPersistence,
+    );
+  }
+
+  /**
+   * Cache the email sent state.
+   */
+  cacheData(data: { emailSent: boolean }): void {
+    if (!this.featureEnabled) {
+      return;
+    }
+
+    this.emailCache.set({
+      emailSent: data.emailSent,
+    } as TwoFactorAuthEmailComponentCache);
+  }
+
+  /**
+   * Clear the cached email data.
+   */
+  clearCachedData(): void {
+    if (!this.featureEnabled) {
+      return;
+    }
+
+    this.emailCache.set(null);
+  }
+
+  /**
+   * Get whether the email has been sent.
+   */
+  getCachedData(): TwoFactorAuthEmailComponentCache | null {
+    if (!this.featureEnabled) {
+      return null;
+    }
+
+    return this.emailCache();
+  }
+}
diff --git a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email.component.html b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email.component.html
index 41873c32ed05..90f1d74ae481 100644
--- a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email.component.html
+++ b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email.component.html
@@ -1,6 +1,13 @@
 <bit-form-field class="!tw-mb-0">
   <bit-label>{{ "verificationCode" | i18n }}</bit-label>
-  <input bitInput type="text" appAutofocus appInputVerbatim [formControl]="tokenFormControl" />
+  <input
+    bitInput
+    type="text"
+    appAutofocus
+    appInputVerbatim
+    [formControl]="tokenFormControl"
+    (keyup)="onTokenChange($event)"
+  />
 </bit-form-field>
 
 <div class="tw-mb-4">
diff --git a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email.component.ts b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email.component.ts
index 0c67416532f2..25235017bd14 100644
--- a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email.component.ts
+++ b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email.component.ts
@@ -1,5 +1,5 @@
 import { CommonModule } from "@angular/common";
-import { Component, Input, OnInit } from "@angular/core";
+import { Component, Input, OnInit, Output, EventEmitter } from "@angular/core";
 import { ReactiveFormsModule, FormsModule, FormControl } from "@angular/forms";
 
 import { JslibModule } from "@bitwarden/angular/jslib.module";
@@ -22,6 +22,7 @@ import {
   ToastService,
 } from "@bitwarden/components";
 
+import { TwoFactorAuthEmailComponentCacheService } from "./two-factor-auth-email-component-cache.service";
 import { TwoFactorAuthEmailComponentService } from "./two-factor-auth-email-component.service";
 
 @Component({
@@ -40,14 +41,20 @@ import { TwoFactorAuthEmailComponentService } from "./two-factor-auth-email-comp
     AsyncActionsModule,
     FormsModule,
   ],
-  providers: [],
+  providers: [
+    {
+      provide: TwoFactorAuthEmailComponentCacheService,
+      useClass: TwoFactorAuthEmailComponentCacheService,
+    },
+  ],
 })
 export class TwoFactorAuthEmailComponent implements OnInit {
   @Input({ required: true }) tokenFormControl: FormControl | undefined = undefined;
+  @Output() tokenChange = new EventEmitter<{ token: string }>();
 
   twoFactorEmail: string | undefined = undefined;
-  emailPromise: Promise<any> | undefined = undefined;
-  tokenValue: string = "";
+  emailPromise: Promise<any> | undefined;
+  emailSent = false;
 
   constructor(
     protected i18nService: I18nService,
@@ -59,14 +66,22 @@ export class TwoFactorAuthEmailComponent implements OnInit {
     protected appIdService: AppIdService,
     private toastService: ToastService,
     private twoFactorAuthEmailComponentService: TwoFactorAuthEmailComponentService,
+    private cacheService: TwoFactorAuthEmailComponentCacheService,
   ) {}
 
   async ngOnInit(): Promise<void> {
     await this.twoFactorAuthEmailComponentService.openPopoutIfApprovedForEmail2fa?.();
+    await this.cacheService.init();
+
+    // Check if email was already sent
+    const cachedData = this.cacheService.getCachedData();
+    if (cachedData?.emailSent) {
+      this.emailSent = true;
+    }
 
     const providers = await this.twoFactorService.getProviders();
 
-    if (!providers) {
+    if (!providers || providers.size === 0) {
       throw new Error("User has no 2FA Providers");
     }
 
@@ -78,11 +93,20 @@ export class TwoFactorAuthEmailComponent implements OnInit {
 
     this.twoFactorEmail = email2faProviderData.Email;
 
-    if (providers.size > 1) {
+    if (!this.emailSent) {
       await this.sendEmail(false);
     }
   }
 
+  /**
+   * Emits the token value to the parent component
+   * @param event - The event object from the input field
+   */
+  onTokenChange(event: Event) {
+    const tokenValue = (event.target as HTMLInputElement).value || "";
+    this.tokenChange.emit({ token: tokenValue });
+  }
+
   async sendEmail(doToast: boolean) {
     if (this.emailPromise !== undefined) {
       return;
@@ -113,6 +137,10 @@ export class TwoFactorAuthEmailComponent implements OnInit {
       request.authRequestId = (await this.loginStrategyService.getAuthRequestId()) ?? "";
       this.emailPromise = this.apiService.postTwoFactorEmail(request);
       await this.emailPromise;
+
+      this.emailSent = true;
+      this.cacheService.cacheData({ emailSent: this.emailSent });
+
       if (doToast) {
         this.toastService.showToast({
           variant: "success",
diff --git a/libs/auth/src/angular/two-factor-auth/two-factor-auth-component-cache.service.spec.ts b/libs/auth/src/angular/two-factor-auth/two-factor-auth-component-cache.service.spec.ts
new file mode 100644
index 000000000000..0993954fde10
--- /dev/null
+++ b/libs/auth/src/angular/two-factor-auth/two-factor-auth-component-cache.service.spec.ts
@@ -0,0 +1,191 @@
+import { TestBed } from "@angular/core/testing";
+import { mock, MockProxy } from "jest-mock-extended";
+import { BehaviorSubject } from "rxjs";
+
+import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service";
+import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
+import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
+import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
+
+import {
+  TwoFactorAuthComponentCache,
+  TwoFactorAuthComponentCacheService,
+  TwoFactorAuthComponentData,
+} from "./two-factor-auth-component-cache.service";
+
+describe("TwoFactorAuthCache", () => {
+  describe("fromJSON", () => {
+    it("returns null when input is null", () => {
+      const result = TwoFactorAuthComponentCache.fromJSON(null as any);
+      expect(result).toBeNull();
+    });
+
+    it("creates a TwoFactorAuthCache instance from valid JSON", () => {
+      const jsonData = {
+        token: "123456",
+        remember: true,
+        selectedProviderType: TwoFactorProviderType.Email,
+      };
+      const result = TwoFactorAuthComponentCache.fromJSON(jsonData as any);
+
+      expect(result).not.toBeNull();
+      expect(result).toBeInstanceOf(TwoFactorAuthComponentCache);
+      expect(result?.token).toBe("123456");
+      expect(result?.remember).toBe(true);
+      expect(result?.selectedProviderType).toBe(TwoFactorProviderType.Email);
+    });
+  });
+});
+
+describe("TwoFactorAuthComponentCacheService", () => {
+  let service: TwoFactorAuthComponentCacheService;
+  let mockViewCacheService: MockProxy<ViewCacheService>;
+  let mockConfigService: MockProxy<ConfigService>;
+  let cacheData: BehaviorSubject<TwoFactorAuthComponentCache | null>;
+  let mockSignal: any;
+
+  beforeEach(() => {
+    mockViewCacheService = mock<ViewCacheService>();
+    mockConfigService = mock<ConfigService>();
+    cacheData = new BehaviorSubject<TwoFactorAuthComponentCache | null>(null);
+    mockSignal = jest.fn(() => cacheData.getValue());
+    mockSignal.set = jest.fn((value: TwoFactorAuthComponentCache | null) => cacheData.next(value));
+    mockViewCacheService.signal.mockReturnValue(mockSignal);
+
+    TestBed.configureTestingModule({
+      providers: [
+        TwoFactorAuthComponentCacheService,
+        { provide: ViewCacheService, useValue: mockViewCacheService },
+        { provide: ConfigService, useValue: mockConfigService },
+      ],
+    });
+
+    service = TestBed.inject(TwoFactorAuthComponentCacheService);
+  });
+
+  it("creates the service", () => {
+    expect(service).toBeTruthy();
+  });
+
+  describe("init", () => {
+    it("sets featureEnabled to true when flag is enabled", async () => {
+      mockConfigService.getFeatureFlag.mockResolvedValue(true);
+
+      await service.init();
+
+      expect(mockConfigService.getFeatureFlag).toHaveBeenCalledWith(
+        FeatureFlag.PM9115_TwoFactorExtensionDataPersistence,
+      );
+
+      service.cacheData({ token: "123456" });
+      expect(mockSignal.set).toHaveBeenCalled();
+    });
+
+    it("sets featureEnabled to false when flag is disabled", async () => {
+      mockConfigService.getFeatureFlag.mockResolvedValue(false);
+
+      await service.init();
+
+      expect(mockConfigService.getFeatureFlag).toHaveBeenCalledWith(
+        FeatureFlag.PM9115_TwoFactorExtensionDataPersistence,
+      );
+
+      service.cacheData({ token: "123456" });
+      expect(mockSignal.set).not.toHaveBeenCalled();
+    });
+  });
+
+  describe("cacheData", () => {
+    beforeEach(async () => {
+      mockConfigService.getFeatureFlag.mockResolvedValue(true);
+      await service.init();
+    });
+
+    it("caches complete data when feature is enabled", () => {
+      const testData: TwoFactorAuthComponentData = {
+        token: "123456",
+        remember: true,
+        selectedProviderType: TwoFactorProviderType.Email,
+      };
+
+      service.cacheData(testData);
+
+      expect(mockSignal.set).toHaveBeenCalledWith({
+        token: "123456",
+        remember: true,
+        selectedProviderType: TwoFactorProviderType.Email,
+      });
+    });
+
+    it("caches partial data when feature is enabled", () => {
+      service.cacheData({ token: "123456" });
+
+      expect(mockSignal.set).toHaveBeenCalledWith({
+        token: "123456",
+        remember: undefined,
+        selectedProviderType: undefined,
+      });
+    });
+
+    it("does not cache data when feature is disabled", async () => {
+      mockConfigService.getFeatureFlag.mockResolvedValue(false);
+      await service.init();
+
+      service.cacheData({ token: "123456" });
+
+      expect(mockSignal.set).not.toHaveBeenCalled();
+    });
+  });
+
+  describe("clearCachedData", () => {
+    beforeEach(async () => {
+      mockConfigService.getFeatureFlag.mockResolvedValue(true);
+      await service.init();
+    });
+
+    it("clears cached data when feature is enabled", () => {
+      service.clearCachedData();
+
+      expect(mockSignal.set).toHaveBeenCalledWith(null);
+    });
+
+    it("does not clear cached data when feature is disabled", async () => {
+      mockConfigService.getFeatureFlag.mockResolvedValue(false);
+      await service.init();
+
+      service.clearCachedData();
+
+      expect(mockSignal.set).not.toHaveBeenCalled();
+    });
+  });
+
+  describe("getCachedData", () => {
+    beforeEach(async () => {
+      mockConfigService.getFeatureFlag.mockResolvedValue(true);
+      await service.init();
+    });
+
+    it("returns cached data when feature is enabled", () => {
+      const testData = new TwoFactorAuthComponentCache();
+      testData.token = "123456";
+      testData.remember = true;
+      testData.selectedProviderType = TwoFactorProviderType.Email;
+      cacheData.next(testData);
+
+      const result = service.getCachedData();
+
+      expect(result).toEqual(testData);
+      expect(mockSignal).toHaveBeenCalled();
+    });
+
+    it("returns null when feature is disabled", async () => {
+      mockConfigService.getFeatureFlag.mockResolvedValue(false);
+      await service.init();
+
+      const result = service.getCachedData();
+
+      expect(result).toBeNull();
+      expect(mockSignal).not.toHaveBeenCalled();
+    });
+  });
+});
diff --git a/libs/auth/src/angular/two-factor-auth/two-factor-auth-component-cache.service.ts b/libs/auth/src/angular/two-factor-auth/two-factor-auth-component-cache.service.ts
new file mode 100644
index 000000000000..61b44aa98ddc
--- /dev/null
+++ b/libs/auth/src/angular/two-factor-auth/two-factor-auth-component-cache.service.ts
@@ -0,0 +1,105 @@
+import { inject, Injectable, WritableSignal } from "@angular/core";
+import { Jsonify } from "type-fest";
+
+import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service";
+import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
+import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
+import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
+
+const TWO_FACTOR_AUTH_COMPONENT_CACHE_KEY = "two-factor-auth-component-cache";
+
+/**
+ * Cache model for the two factor authentication data.
+ */
+export class TwoFactorAuthComponentCache {
+  token: string | undefined = undefined;
+  remember: boolean | undefined = undefined;
+  selectedProviderType: TwoFactorProviderType | undefined = undefined;
+
+  static fromJSON(
+    obj: Partial<Jsonify<TwoFactorAuthComponentCache>>,
+  ): TwoFactorAuthComponentCache | null {
+    // Return null if the cache is empty
+    if (obj == null) {
+      return null;
+    }
+
+    return Object.assign(new TwoFactorAuthComponentCache(), obj);
+  }
+}
+
+export interface TwoFactorAuthComponentData {
+  token?: string;
+  remember?: boolean;
+  selectedProviderType?: TwoFactorProviderType;
+}
+
+/**
+ * Cache service used for the two factor auth component.
+ */
+@Injectable()
+export class TwoFactorAuthComponentCacheService {
+  private viewCacheService: ViewCacheService = inject(ViewCacheService);
+  private configService: ConfigService = inject(ConfigService);
+
+  /** True when the `PM9115_TwoFactorExtensionDataPersistence` flag is enabled */
+  private featureEnabled: boolean = false;
+
+  /**
+   * Signal for the cached TwoFactorAuthData.
+   */
+  private twoFactorAuthComponentCache: WritableSignal<TwoFactorAuthComponentCache | null> =
+    this.viewCacheService.signal<TwoFactorAuthComponentCache | null>({
+      key: TWO_FACTOR_AUTH_COMPONENT_CACHE_KEY,
+      initialValue: null,
+      deserializer: TwoFactorAuthComponentCache.fromJSON,
+    });
+
+  constructor() {}
+
+  /**
+   * Must be called once before interacting with the cached data.
+   */
+  async init() {
+    this.featureEnabled = await this.configService.getFeatureFlag(
+      FeatureFlag.PM9115_TwoFactorExtensionDataPersistence,
+    );
+  }
+
+  /**
+   * Update the cache with the new TwoFactorAuthData.
+   */
+  cacheData(data: TwoFactorAuthComponentData): void {
+    if (!this.featureEnabled) {
+      return;
+    }
+
+    this.twoFactorAuthComponentCache.set({
+      token: data.token,
+      remember: data.remember,
+      selectedProviderType: data.selectedProviderType,
+    } as TwoFactorAuthComponentCache);
+  }
+
+  /**
+   * Clears the cached TwoFactorAuthData.
+   */
+  clearCachedData(): void {
+    if (!this.featureEnabled) {
+      return;
+    }
+
+    this.twoFactorAuthComponentCache.set(null);
+  }
+
+  /**
+   * Returns the cached TwoFactorAuthData (when available).
+   */
+  getCachedData(): TwoFactorAuthComponentCache | null {
+    if (!this.featureEnabled) {
+      return null;
+    }
+
+    return this.twoFactorAuthComponentCache();
+  }
+}
diff --git a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.html b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.html
index ec03944a9546..8ad35c7b5c65 100644
--- a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.html
+++ b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.html
@@ -13,11 +13,13 @@
   >
     <app-two-factor-auth-email
       [tokenFormControl]="tokenFormControl"
+      (tokenChange)="saveFormDataWithPartialData($event)"
       *ngIf="selectedProviderType === providerType.Email"
     />
 
     <app-two-factor-auth-authenticator
       [tokenFormControl]="tokenFormControl"
+      (tokenChange)="saveFormDataWithPartialData($event)"
       *ngIf="selectedProviderType === providerType.Authenticator"
     />
     <app-two-factor-auth-yubikey
@@ -36,7 +38,7 @@
     />
     <bit-form-control *ngIf="!hideRememberMe()">
       <bit-label>{{ "dontAskAgainOnThisDeviceFor30Days" | i18n }}</bit-label>
-      <input type="checkbox" bitCheckbox formControlName="remember" />
+      <input type="checkbox" bitCheckbox formControlName="remember" (change)="onRememberChange()" />
     </bit-form-control>
 
     <app-two-factor-auth-webauthn
diff --git a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.spec.ts b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.spec.ts
index 8378ac7794ce..d48d725e251e 100644
--- a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.spec.ts
+++ b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.spec.ts
@@ -1,8 +1,6 @@
-// FIXME: Update this file to be type safe and remove this and next line
-// @ts-strict-ignore
 import { Component } from "@angular/core";
 import { ComponentFixture, TestBed } from "@angular/core/testing";
-import { ActivatedRoute, convertToParamMap, Router } from "@angular/router";
+import { ActivatedRoute, Router, convertToParamMap } from "@angular/router";
 import { mock, MockProxy } from "jest-mock-extended";
 import { BehaviorSubject } from "rxjs";
 
@@ -38,6 +36,7 @@ import { DialogService, ToastService } from "@bitwarden/components";
 
 import { AnonLayoutWrapperDataService } from "../anon-layout/anon-layout-wrapper-data.service";
 
+import { TwoFactorAuthComponentCacheService } from "./two-factor-auth-component-cache.service";
 import { TwoFactorAuthComponentService } from "./two-factor-auth-component.service";
 import { TwoFactorAuthComponent } from "./two-factor-auth.component";
 
@@ -72,6 +71,7 @@ describe("TwoFactorAuthComponent", () => {
   let anonLayoutWrapperDataService: MockProxy<AnonLayoutWrapperDataService>;
   let mockEnvService: MockProxy<EnvironmentService>;
   let mockLoginSuccessHandlerService: MockProxy<LoginSuccessHandlerService>;
+  let mockTwoFactorAuthCompCacheService: MockProxy<TwoFactorAuthComponentCacheService>;
 
   let mockUserDecryptionOpts: {
     noMasterPassword: UserDecryptionOptions;
@@ -112,6 +112,10 @@ describe("TwoFactorAuthComponent", () => {
 
     anonLayoutWrapperDataService = mock<AnonLayoutWrapperDataService>();
 
+    mockTwoFactorAuthCompCacheService = mock<TwoFactorAuthComponentCacheService>();
+    mockTwoFactorAuthCompCacheService.getCachedData.mockReturnValue(null);
+    mockTwoFactorAuthCompCacheService.init.mockResolvedValue();
+
     mockUserDecryptionOpts = {
       noMasterPassword: new UserDecryptionOptions({
         hasMasterPassword: false,
@@ -155,7 +159,9 @@ describe("TwoFactorAuthComponent", () => {
       }),
     };
 
-    selectedUserDecryptionOptions = new BehaviorSubject<UserDecryptionOptions>(undefined);
+    selectedUserDecryptionOptions = new BehaviorSubject<UserDecryptionOptions>(
+      mockUserDecryptionOpts.withMasterPassword,
+    );
     mockUserDecryptionOptionsService.userDecryptionOptions$ = selectedUserDecryptionOptions;
 
     TestBed.configureTestingModule({
@@ -194,6 +200,10 @@ describe("TwoFactorAuthComponent", () => {
         { provide: EnvironmentService, useValue: mockEnvService },
         { provide: AnonLayoutWrapperDataService, useValue: anonLayoutWrapperDataService },
         { provide: LoginSuccessHandlerService, useValue: mockLoginSuccessHandlerService },
+        {
+          provide: TwoFactorAuthComponentCacheService,
+          useValue: mockTwoFactorAuthCompCacheService,
+        },
       ],
     });
 
diff --git a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts
index aed9f9f07a5f..f09d71636679 100644
--- a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts
+++ b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts
@@ -60,6 +60,10 @@ import { TwoFactorAuthDuoComponent } from "./child-components/two-factor-auth-du
 import { TwoFactorAuthEmailComponent } from "./child-components/two-factor-auth-email/two-factor-auth-email.component";
 import { TwoFactorAuthWebAuthnComponent } from "./child-components/two-factor-auth-webauthn/two-factor-auth-webauthn.component";
 import { TwoFactorAuthYubikeyComponent } from "./child-components/two-factor-auth-yubikey.component";
+import {
+  TwoFactorAuthComponentCacheService,
+  TwoFactorAuthComponentData,
+} from "./two-factor-auth-component-cache.service";
 import {
   DuoLaunchAction,
   LegacyKeyMigrationAction,
@@ -90,7 +94,11 @@ import {
     TwoFactorAuthYubikeyComponent,
     TwoFactorAuthWebAuthnComponent,
   ],
-  providers: [],
+  providers: [
+    {
+      provide: TwoFactorAuthComponentCacheService,
+    },
+  ],
 })
 export class TwoFactorAuthComponent implements OnInit, OnDestroy {
   @ViewChild("continueButton", { read: ElementRef, static: false }) continueButton:
@@ -160,6 +168,7 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
     private anonLayoutWrapperDataService: AnonLayoutWrapperDataService,
     private environmentService: EnvironmentService,
     private loginSuccessHandlerService: LoginSuccessHandlerService,
+    private twoFactorAuthComponentCacheService: TwoFactorAuthComponentCacheService,
   ) {}
 
   async ngOnInit() {
@@ -168,7 +177,33 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
 
     this.listenForAuthnSessionTimeout();
 
-    await this.setSelected2faProviderType();
+    // Initialize the cache
+    await this.twoFactorAuthComponentCacheService.init();
+
+    // Load cached form data if available
+    let loadedCachedProviderType = false;
+    const cachedData = this.twoFactorAuthComponentCacheService.getCachedData();
+    if (cachedData) {
+      if (cachedData.token) {
+        this.form.patchValue({ token: cachedData.token });
+      }
+      if (cachedData.remember !== undefined) {
+        this.form.patchValue({ remember: cachedData.remember });
+      }
+      if (cachedData.selectedProviderType !== undefined) {
+        this.selectedProviderType = cachedData.selectedProviderType;
+        loadedCachedProviderType = true;
+      }
+    }
+
+    // If we don't have a cached provider type, set it to the default and cache it
+    if (!loadedCachedProviderType) {
+      this.selectedProviderType = await this.initializeSelected2faProviderType();
+      this.twoFactorAuthComponentCacheService.cacheData({
+        selectedProviderType: this.selectedProviderType,
+      });
+    }
+
     await this.set2faProvidersAndData();
     await this.setAnonLayoutDataByTwoFactorProviderType();
 
@@ -181,7 +216,29 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
     this.loading = false;
   }
 
-  private async setSelected2faProviderType() {
+  /**
+   * Save specific form data fields to the cache
+   */
+  async saveFormDataWithPartialData(data: Partial<TwoFactorAuthComponentData>) {
+    // Get current cached data
+    const currentData = this.twoFactorAuthComponentCacheService.getCachedData();
+
+    this.twoFactorAuthComponentCacheService.cacheData({
+      token: data?.token ?? currentData?.token ?? "",
+      remember: data?.remember ?? currentData?.remember ?? false,
+      selectedProviderType: data?.selectedProviderType ?? currentData?.selectedProviderType,
+    });
+  }
+
+  /**
+   * Save the remember value to the cache when the checkbox is checked or unchecked
+   */
+  async onRememberChange() {
+    const rememberValue = !!this.rememberFormControl.value;
+    await this.saveFormDataWithPartialData({ remember: rememberValue });
+  }
+
+  private async initializeSelected2faProviderType(): Promise<TwoFactorProviderType> {
     const webAuthnSupported = this.platformUtilsService.supportsWebAuthn(this.win);
 
     if (
@@ -190,18 +247,19 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
     ) {
       const webAuthn2faResponse = this.activatedRoute.snapshot.paramMap.get("webAuthnResponse");
       if (webAuthn2faResponse) {
-        this.selectedProviderType = TwoFactorProviderType.WebAuthn;
-        return;
+        return TwoFactorProviderType.WebAuthn;
       }
     }
 
-    this.selectedProviderType = await this.twoFactorService.getDefaultProvider(webAuthnSupported);
+    return await this.twoFactorService.getDefaultProvider(webAuthnSupported);
   }
 
   private async set2faProvidersAndData() {
     this.twoFactorProviders = await this.twoFactorService.getProviders();
-    const providerData = this.twoFactorProviders?.get(this.selectedProviderType);
-    this.selectedProviderData = providerData;
+    if (this.selectedProviderType !== undefined) {
+      const providerData = this.twoFactorProviders?.get(this.selectedProviderType);
+      this.selectedProviderData = providerData;
+    }
   }
 
   private listenForAuthnSessionTimeout() {
@@ -267,6 +325,13 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
     // In all flows but WebAuthn, the remember value is taken from the form.
     const rememberValue = remember ?? this.rememberFormControl.value ?? false;
 
+    // Cache form data before submitting
+    this.twoFactorAuthComponentCacheService.cacheData({
+      token: tokenValue,
+      remember: rememberValue,
+      selectedProviderType: this.selectedProviderType,
+    });
+
     try {
       this.formPromise = this.loginStrategyService.logInTwoFactor(
         new TokenTwoFactorRequest(this.selectedProviderType, tokenValue, rememberValue),
@@ -274,6 +339,7 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
       );
       const authResult: AuthResult = await this.formPromise;
       this.logService.info("Successfully submitted two factor token");
+
       await this.handleAuthResult(authResult);
     } catch {
       this.logService.error("Error submitting two factor token");
@@ -299,6 +365,13 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
       this.selectedProviderType = response.type;
       await this.setAnonLayoutDataByTwoFactorProviderType();
 
+      // Update the cached provider type when a new one is chosen
+      this.twoFactorAuthComponentCacheService.cacheData({
+        token: "",
+        remember: false,
+        selectedProviderType: response.type,
+      });
+
       this.form.reset();
       this.form.updateValueAndValidity();
     }
@@ -376,6 +449,9 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
   }
 
   private async handleAuthResult(authResult: AuthResult) {
+    // Clear form cache
+    this.twoFactorAuthComponentCacheService.clearCachedData();
+
     if (await this.handleMigrateEncryptionKey(authResult)) {
       return; // stop login process
     }
diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts
index e353d79988f5..affa7ccff4fe 100644
--- a/libs/common/src/enums/feature-flag.enum.ts
+++ b/libs/common/src/enums/feature-flag.enum.ts
@@ -17,6 +17,7 @@ export enum FeatureFlag {
 
   /* Auth */
   PM9112_DeviceApprovalPersistence = "pm-9112-device-approval-persistence",
+  PM9115_TwoFactorExtensionDataPersistence = "pm-9115-two-factor-extension-data-persistence",
 
   /* Autofill */
   BlockBrowserInjectionsByDomain = "block-browser-injections-by-domain",
@@ -116,6 +117,7 @@ export const DefaultFeatureFlagValue = {
 
   /* Auth */
   [FeatureFlag.PM9112_DeviceApprovalPersistence]: FALSE,
+  [FeatureFlag.PM9115_TwoFactorExtensionDataPersistence]: FALSE,
 
   /* Billing */
   [FeatureFlag.TrialPaymentOptional]: FALSE,


Reviews:
user: login: dani-garcia
body: Platform changes LGTM
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-03-12T12:40:42Z
user: login: JaredSnider-Bitwarden
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T13:30:05Z
user: login: alec-livefront
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T20:58:10Z
user: login: dani-garcia
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-27T17:10:06Z
user: login: alec-livefront
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-29T20:21:13Z
user: login: dani-garcia
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-04-02T10:31:28Z
user: login: JaredSnider-Bitwarden
body: This is really close!  Please let me know if I need to clarify anything below:
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T22:10:11Z
user: login: alec-livefront
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T23:41:34Z
user: login: alec-livefront
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T23:41:50Z
user: login: alec-livefront
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T23:48:08Z
user: login: JaredSnider-Bitwarden
body: This is nearly there! Ty for sticking with this!
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T18:53:14Z
user: login: JaredSnider-Bitwarden
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T18:53:53Z
user: login: alec-livefront
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T20:56:10Z
user: login: alec-livefront
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T21:08:39Z
user: login: alec-livefront
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T21:18:15Z
user: login: alec-livefront
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T21:22:46Z
user: login: alec-livefront
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T21:42:47Z
user: login: JaredSnider-Bitwarden
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T03:11:40Z
user: login: JaredSnider-Bitwarden
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T18:56:52Z
user: login: alec-livefront
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T20:02:17Z
user: login: alec-livefront
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T20:08:54Z
user: login: alec-livefront
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T20:48:47Z
user: login: JaredSnider-Bitwarden
body: Nice work!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T22:38:04Z
user: login: dani-garcia
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-25T09:35:35Z

Review comments:
path: apps/browser/src/auth/popup/two-factor-options-v1.component.ts
user: login: JaredSnider-Bitwarden
body: Just taking a glance at this quickly, but we shouldn't be making any updates to the v1 components as we will be removing them shortly. 
created_at: 2025-03-13T13:30:04Z
author_association: CONTRIBUTOR
path: apps/browser/src/auth/popup/two-factor-options-v1.component.ts
user: login: alec-livefront
body: [41df497](https://github.com/bitwarden/clients/pull/13706/commits/41df49761171ad9851299122e387105b47ce597f) & [9f55091](https://github.com/bitwarden/clients/pull/13706/commits/9f550915d0433e388e4356fc1a03708f4f9a2706) & [06273f3](https://github.com/bitwarden/clients/pull/13706/commits/06273f3b2afdd2a19a5b775a85d20dd0124fe58f)
created_at: 2025-03-20T20:58:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -168,7 +180,29 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
 
     this.listenForAuthnSessionTimeout();
 
-    await this.setSelected2faProviderType();
+    // Load persisted form data if available
+    let loadedCachedProviderType = false;
+    if (this.twoFactorFormCacheService) {
path: libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts
user: login: dani-garcia
body: Question: I see this `if (this.twoFactorFormCacheService) {` in a few places in this PR, is it expected that the service will be null/undefined/falsy in some cases?

The service seems to be injected in the constructor, so not sure how that could happen
created_at: 2025-03-27T17:10:05Z
author_association: MEMBER
diff_hunk: @@ -168,7 +180,29 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
 
     this.listenForAuthnSessionTimeout();
 
-    await this.setSelected2faProviderType();
+    // Load persisted form data if available
+    let loadedCachedProviderType = false;
+    if (this.twoFactorFormCacheService) {
path: libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts
user: login: alec-livefront
body: Yes, good call - I was experimenting with that before implementing the no-ops for desktop/web.

[4c7684b](https://github.com/bitwarden/clients/pull/13706/commits/4c7684bc5ec8ec6508715946f3a0439cafd2d6e8)
created_at: 2025-03-29T20:21:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,19 @@
+import { Jsonify } from "type-fest";
+
+import { View } from "@bitwarden/common/models/view/view";
+
+import { TwoFactorProviderType } from "../../enums/two-factor-provider-type";
+
+/**
+ * This is a cache model for the two factor form.
+ */
+export class TwoFactorFormView implements View {
path: libs/common/src/auth/models/view/two-factor-form.view.ts
user: login: JaredSnider-Bitwarden
body: 🎨 , I just realized that I missed that the `LoginViaAuthRequestView` model used the `View` class. I believe this is incorrect as, per our [docs](https://contributing.bitwarden.com/architecture/clients/data-model#view), a `view` is reserved to represent the decrypted state of a `domain` model. This should just be a simple `TwoFactorAuthCache` class that can live in its own file or within the component cache service itself. I've updated the internal docs on this here: https://bitwarden.atlassian.net/wiki/spaces/EN/pages/1684013058/Extension+View+Caching+Guide 
created_at: 2025-04-09T21:57:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,19 @@
+import { Jsonify } from "type-fest";
+
+import { View } from "@bitwarden/common/models/view/view";
+
+import { TwoFactorProviderType } from "../../enums/two-factor-provider-type";
+
+/**
+ * This is a cache model for the two factor form.
+ */
+export class TwoFactorFormView implements View {
path: libs/common/src/auth/models/view/two-factor-form.view.ts
user: login: JaredSnider-Bitwarden
body: Note: we should remove form from the name as `selectedProviderType` and `emailSent` are not form data. This is really just a grab bag of component data. 
created_at: 2025-04-09T21:58:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,90 @@
+import { inject, Injectable, WritableSignal } from "@angular/core";
+
+import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service";
+import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
+import { TwoFactorFormView } from "@bitwarden/common/auth/models/view/two-factor-form.view";
+import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
+import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
+
+const TWO_FACTOR_FORM_CACHE_KEY = "two-factor-form-cache";
+
+export interface TwoFactorFormData {
+  token?: string;
+  remember?: boolean;
+  selectedProviderType?: TwoFactorProviderType;
+  emailSent?: boolean;
+}
+
+/**
+ * This is a cache service used for the login via auth request component.
+ *
+ * There is sensitive information stored temporarily here. Cache will be cleared
+ * after 2 minutes.
+ */
+@Injectable()
+export class DefaultTwoFactorFormCacheService {
path: libs/auth/src/common/services/auth-request/default-two-factor-form-cache.service.ts
user: login: JaredSnider-Bitwarden
body: We don't need a default prefix as we don't have an abstraction and all client specific details are delegated to the platform team's `ViewCacheService` layer.  Docs: https://bitwarden.atlassian.net/wiki/spaces/EN/pages/1684013058/Extension+View+Caching+Guide
created_at: 2025-04-09T22:04:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,90 @@
+import { inject, Injectable, WritableSignal } from "@angular/core";
+
+import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service";
+import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
+import { TwoFactorFormView } from "@bitwarden/common/auth/models/view/two-factor-form.view";
+import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
+import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
+
+const TWO_FACTOR_FORM_CACHE_KEY = "two-factor-form-cache";
+
+export interface TwoFactorFormData {
+  token?: string;
+  remember?: boolean;
+  selectedProviderType?: TwoFactorProviderType;
+  emailSent?: boolean;
+}
+
+/**
+ * This is a cache service used for the login via auth request component.
+ *
+ * There is sensitive information stored temporarily here. Cache will be cleared
+ * after 2 minutes.
+ */
+@Injectable()
+export class DefaultTwoFactorFormCacheService {
path: libs/auth/src/common/services/auth-request/default-two-factor-form-cache.service.ts
user: login: JaredSnider-Bitwarden
body: Naming nit: we can strip `form` out of this across the board. 
created_at: 2025-04-09T22:04:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -113,6 +123,9 @@ export class TwoFactorAuthEmailComponent implements OnInit {
       request.authRequestId = (await this.loginStrategyService.getAuthRequestId()) ?? "";
       this.emailPromise = this.apiService.postTwoFactorEmail(request);
       await this.emailPromise;
+
+      this.emailSendEvent.emit();
path: libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email.component.ts
user: login: JaredSnider-Bitwarden
body: Per our previous discussion, we should not be saving this in the two factor auth component cache service. Whether or not we have already sent the email is specific to the `two-factor-auth-email.component`. We should have a `TwoFactorAuthEmailComponentCacheService` which saves this boolean and then we don't need the input or event emitter. This means that each component then manages its own data better as well.
created_at: 2025-04-09T22:08:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,19 @@
+import { Jsonify } from "type-fest";
+
+import { View } from "@bitwarden/common/models/view/view";
+
+import { TwoFactorProviderType } from "../../enums/two-factor-provider-type";
+
+/**
+ * This is a cache model for the two factor form.
+ */
+export class TwoFactorFormView implements View {
path: libs/common/src/auth/models/view/two-factor-form.view.ts
user: login: alec-livefront
body: [116c4ec](https://github.com/bitwarden/clients/pull/13706/commits/116c4ec5ed6df398a98fac61a58822e9f497d304)
created_at: 2025-04-10T23:41:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,90 @@
+import { inject, Injectable, WritableSignal } from "@angular/core";
+
+import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service";
+import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
+import { TwoFactorFormView } from "@bitwarden/common/auth/models/view/two-factor-form.view";
+import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
+import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
+
+const TWO_FACTOR_FORM_CACHE_KEY = "two-factor-form-cache";
+
+export interface TwoFactorFormData {
+  token?: string;
+  remember?: boolean;
+  selectedProviderType?: TwoFactorProviderType;
+  emailSent?: boolean;
+}
+
+/**
+ * This is a cache service used for the login via auth request component.
+ *
+ * There is sensitive information stored temporarily here. Cache will be cleared
+ * after 2 minutes.
+ */
+@Injectable()
+export class DefaultTwoFactorFormCacheService {
path: libs/auth/src/common/services/auth-request/default-two-factor-form-cache.service.ts
user: login: alec-livefront
body: [92797c9](https://github.com/bitwarden/clients/pull/13706/commits/92797c99c7bfe423197ac1d76aeedd1396130cf2)
created_at: 2025-04-10T23:41:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -113,6 +123,9 @@ export class TwoFactorAuthEmailComponent implements OnInit {
       request.authRequestId = (await this.loginStrategyService.getAuthRequestId()) ?? "";
       this.emailPromise = this.apiService.postTwoFactorEmail(request);
       await this.emailPromise;
+
+      this.emailSendEvent.emit();
path: libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email.component.ts
user: login: alec-livefront
body: [3ad40d7](https://github.com/bitwarden/clients/pull/13706/commits/3ad40d7082ccb9e2cd33936ed0f84dcf08d466c0)

This got me thinking: if we want each component to manage its own data do we take this a step further and have both `TwoFactorAuthEmailComponent` and `TwoFactorAuthAuthenticatorComponent` handle their own tokens as well? I'd imagine that would mean adding a `TwoFactorAuthAuthenticatorComponentCacheService`.
created_at: 2025-04-10T23:48:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,85 @@
+import { inject, Injectable, WritableSignal } from "@angular/core";
+import { Jsonify } from "type-fest";
+
+import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service";
+import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
+import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
+
+const TWO_FACTOR_AUTH_EMAIL_CACHE_KEY = "two-factor-auth-email-cache";
+
+/**
+ * Cache model for the email two factor
+ */
+export class TwoFactorAuthEmailCache {
+  emailSent: boolean = false;
+
+  static fromJSON(obj: Partial<Jsonify<TwoFactorAuthEmailCache>>): TwoFactorAuthEmailCache {
+    return Object.assign(new TwoFactorAuthEmailCache(), obj);
path: libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email-cache.service.ts
user: login: JaredSnider-Bitwarden
body: We need a block here to return null if the cache value is null. Returning a new object instance isn't correct. Returning an object caused issues w/ the login via auth request view caching implementation. 
```
if (obj == null) {
      return null;
    }
```
created_at: 2025-04-14T18:44:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -113,6 +123,9 @@ export class TwoFactorAuthEmailComponent implements OnInit {
       request.authRequestId = (await this.loginStrategyService.getAuthRequestId()) ?? "";
       this.emailPromise = this.apiService.postTwoFactorEmail(request);
       await this.emailPromise;
+
+      this.emailSendEvent.emit();
path: libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email.component.ts
user: login: JaredSnider-Bitwarden
body: I see what you were thinking, but the token value comes from the token `formControl` which is owned by the parent `two-factor-auth.component` so I think it is correct for the `TwoFactorAuthComponentCacheService` to manage that cache. 
created_at: 2025-04-14T18:45:44Z
author_association: CONTRIBUTOR
path: libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email-cache.service.ts
user: login: JaredSnider-Bitwarden
body: 🎨 : file name needs `component` in the name to match service name. 
created_at: 2025-04-14T18:46:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -181,6 +219,36 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
     this.loading = false;
   }
 
+  /**
+   * Save specific form data fields to the cache
+   */
+  async saveFormDataWithPartialData(data: Partial<TwoFactorCacheData>) {
+    // Get current cached data
+    const currentData = this.twoFactorCacheService.getCachedData();
+
+    this.twoFactorCacheService.cacheData({
+      token: data?.token ?? currentData?.token ?? "",
+      remember: data?.remember ?? currentData?.remember ?? false,
+      selectedProviderType:
+        data?.selectedProviderType ??
+        currentData?.selectedProviderType ??
+        TwoFactorProviderType.Authenticator,
path: libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts
user: login: JaredSnider-Bitwarden
body: I'm not sure we should have a default of authenticator?  If we don't have a defined `TwoFactorProviderType`, then I don't think we can really cache anything. Then, we don't have to worry about a default. 
created_at: 2025-04-14T18:49:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -70,6 +71,15 @@ import {
   TwoFactorOptionsDialogResult,
 } from "./two-factor-options.component";
 
+/**
+ * Interface for the cache data structure
+ */
+interface TwoFactorCacheData {
+  token?: string;
+  remember?: boolean;
+  selectedProviderType?: TwoFactorProviderType;
+}
+
path: libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts
user: login: JaredSnider-Bitwarden
body: This duplicates the `TwoFactorAuthData` interface in the `TwoFactorAuthComponentCacheService`. We should just have one `TwoFactorAuthComponentCacheData.` which is fine to live in the cache service. 
created_at: 2025-04-14T18:51:21Z
author_association: CONTRIBUTOR
path: libs/auth/src/common/services/auth-request/two-factor-auth-cache.service.ts
user: login: JaredSnider-Bitwarden
body: File name should include `component`.  We also should move this service and test file to live alongside the component as it is never meant to be used in a non-angular context. 
created_at: 2025-04-14T18:52:32Z
author_association: CONTRIBUTOR
path: libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email-cache.service.spec.ts
user: login: JaredSnider-Bitwarden
body: Tyvm for the test coverage!
created_at: 2025-04-14T18:52:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,85 @@
+import { inject, Injectable, WritableSignal } from "@angular/core";
+import { Jsonify } from "type-fest";
+
+import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service";
+import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
+import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
+
+const TWO_FACTOR_AUTH_EMAIL_CACHE_KEY = "two-factor-auth-email-cache";
+
+/**
+ * Cache model for the email two factor
+ */
+export class TwoFactorAuthEmailCache {
+  emailSent: boolean = false;
+
+  static fromJSON(obj: Partial<Jsonify<TwoFactorAuthEmailCache>>): TwoFactorAuthEmailCache {
+    return Object.assign(new TwoFactorAuthEmailCache(), obj);
path: libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email-cache.service.ts
user: login: JaredSnider-Bitwarden
body: This also applies to the `TwoFactorAuthCache`
created_at: 2025-04-14T18:53:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,85 @@
+import { inject, Injectable, WritableSignal } from "@angular/core";
+import { Jsonify } from "type-fest";
+
+import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service";
+import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
+import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
+
+const TWO_FACTOR_AUTH_EMAIL_CACHE_KEY = "two-factor-auth-email-cache";
+
+/**
+ * Cache model for the email two factor
+ */
+export class TwoFactorAuthEmailCache {
+  emailSent: boolean = false;
+
+  static fromJSON(obj: Partial<Jsonify<TwoFactorAuthEmailCache>>): TwoFactorAuthEmailCache {
+    return Object.assign(new TwoFactorAuthEmailCache(), obj);
path: libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email-cache.service.ts
user: login: alec-livefront
body: b11b950ef5fdabd08e5a5e205f1c33bbaa08fd0e
created_at: 2025-04-15T20:56:10Z
author_association: CONTRIBUTOR
path: libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email-cache.service.ts
user: login: alec-livefront
body: d0c1f6d6629304034fe8b01103a964851fd3a419
created_at: 2025-04-15T21:08:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -70,6 +71,15 @@ import {
   TwoFactorOptionsDialogResult,
 } from "./two-factor-options.component";
 
+/**
+ * Interface for the cache data structure
+ */
+interface TwoFactorCacheData {
+  token?: string;
+  remember?: boolean;
+  selectedProviderType?: TwoFactorProviderType;
+}
+
path: libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts
user: login: alec-livefront
body: 73a6f059a86a798ced0ad4cbdaadcdfccdc759c9
created_at: 2025-04-15T21:18:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -181,6 +219,36 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
     this.loading = false;
   }
 
+  /**
+   * Save specific form data fields to the cache
+   */
+  async saveFormDataWithPartialData(data: Partial<TwoFactorCacheData>) {
+    // Get current cached data
+    const currentData = this.twoFactorCacheService.getCachedData();
+
+    this.twoFactorCacheService.cacheData({
+      token: data?.token ?? currentData?.token ?? "",
+      remember: data?.remember ?? currentData?.remember ?? false,
+      selectedProviderType:
+        data?.selectedProviderType ??
+        currentData?.selectedProviderType ??
+        TwoFactorProviderType.Authenticator,
path: libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts
user: login: alec-livefront
body: d09293a9341c59c84483b54f555fd08b5987bf2a
created_at: 2025-04-15T21:22:45Z
author_association: CONTRIBUTOR
path: libs/auth/src/common/services/auth-request/two-factor-auth-cache.service.ts
user: login: alec-livefront
body: 6ee84f502c873d45f24a4e1e762a9f6a439e4d2d & f6a4e30fbbf65347bb87897eb354a958e68217cc
created_at: 2025-04-15T21:42:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,85 @@
+import { inject, Injectable, WritableSignal } from "@angular/core";
+import { Jsonify } from "type-fest";
+
+import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service";
+import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
+import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
+
+const TWO_FACTOR_AUTH_EMAIL_CACHE_KEY = "two-factor-auth-email-cache";
+
+/**
+ * Cache model for the email two factor
+ */
+export class TwoFactorAuthEmailCache {
+  emailSent: boolean = false;
+
+  static fromJSON(obj: Partial<Jsonify<TwoFactorAuthEmailCache>>): TwoFactorAuthEmailCache {
+    return Object.assign(new TwoFactorAuthEmailCache(), obj);
path: libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email-cache.service.ts
user: login: JaredSnider-Bitwarden
body: Excellent. Ty for the tests as well. 
created_at: 2025-04-16T03:11:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -286,6 +355,13 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
   };
 
   async selectOtherTwoFactorMethod() {
+    // Cache current form data before navigating to another method
+    this.twoFactorAuthComponentCacheService.cacheData({
+      token: "",
+      remember: false,
+      selectedProviderType: this.selectedProviderType,
+    });
path: libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts
user: login: JaredSnider-Bitwarden
body: Per sync discussion, let's remove this cache just in case users accidentally hit this. 
created_at: 2025-04-24T18:53:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -181,6 +213,33 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
     this.loading = false;
   }
 
+  /**
+   * Save specific form data fields to the cache
+   */
+  async saveFormDataWithPartialData(data: Partial<TwoFactorAuthComponentData>) {
+    // Get current cached data
+    const currentData = this.twoFactorAuthComponentCacheService.getCachedData();
+
+    this.twoFactorAuthComponentCacheService.cacheData({
+      token: data?.token ?? currentData?.token ?? "",
+      remember: data?.remember ?? currentData?.remember ?? false,
+      selectedProviderType: data?.selectedProviderType ?? currentData?.selectedProviderType,
+    });
+  }
+
+  /**
+   * Save all current form data to the cache
+   */
+  async saveFormData() {
+    const formData: TwoFactorAuthComponentData = {
+      token: this.tokenFormControl.value || undefined,
+      remember: this.rememberFormControl.value ?? undefined,
+      selectedProviderType: this.selectedProviderType,
+    };
+
+    await this.saveFormDataWithPartialData(formData);
+  }
+
path: libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts
user: login: JaredSnider-Bitwarden
body: Per discussion, let's remove this call and just use `saveFormDataWithPartialData` across the board
created_at: 2025-04-24T18:53:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -168,7 +177,30 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
 
     this.listenForAuthnSessionTimeout();
 
-    await this.setSelected2faProviderType();
+    // Initialize the cache
+    await this.twoFactorAuthComponentCacheService.init();
+
+    // Load cached form data if available
+    let loadedCachedProviderType = false;
+    const cachedData = this.twoFactorAuthComponentCacheService.getCachedData();
+    if (cachedData) {
+      if (cachedData.token) {
+        this.form.patchValue({ token: cachedData.token });
+      }
+      if (cachedData.remember !== undefined) {
+        this.form.patchValue({ remember: cachedData.remember });
+      }
+      if (cachedData.selectedProviderType !== undefined) {
+        this.selectedProviderType = cachedData.selectedProviderType;
+        loadedCachedProviderType = true;
+      }
+    }
+
+    // Only set default 2FA provider type if we don't have one from cache
+    if (!loadedCachedProviderType) {
+      await this.setSelected2faProviderType();
path: libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts
user: login: JaredSnider-Bitwarden
body: 1. Let's rename `setSelected2faProviderType` to be `initializeSelected2faProviderType` to make it clear it isn't a setter method and instead an initializer
2. Cache the `selected2faProviderType` using the partial data method immediately after the init so that we don't have any scenarios where a token can be set without a 2FA provider set in cache as well. 
created_at: 2025-04-24T18:56:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -181,6 +213,33 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
     this.loading = false;
   }
 
+  /**
+   * Save specific form data fields to the cache
+   */
+  async saveFormDataWithPartialData(data: Partial<TwoFactorAuthComponentData>) {
+    // Get current cached data
+    const currentData = this.twoFactorAuthComponentCacheService.getCachedData();
+
+    this.twoFactorAuthComponentCacheService.cacheData({
+      token: data?.token ?? currentData?.token ?? "",
+      remember: data?.remember ?? currentData?.remember ?? false,
+      selectedProviderType: data?.selectedProviderType ?? currentData?.selectedProviderType,
+    });
+  }
+
+  /**
+   * Save all current form data to the cache
+   */
+  async saveFormData() {
+    const formData: TwoFactorAuthComponentData = {
+      token: this.tokenFormControl.value || undefined,
+      remember: this.rememberFormControl.value ?? undefined,
+      selectedProviderType: this.selectedProviderType,
+    };
+
+    await this.saveFormDataWithPartialData(formData);
+  }
+
path: libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts
user: login: alec-livefront
body: 1515af21710b849bfb28b0388eb54228522fee97
created_at: 2025-04-24T20:02:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -286,6 +355,13 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
   };
 
   async selectOtherTwoFactorMethod() {
+    // Cache current form data before navigating to another method
+    this.twoFactorAuthComponentCacheService.cacheData({
+      token: "",
+      remember: false,
+      selectedProviderType: this.selectedProviderType,
+    });
path: libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts
user: login: alec-livefront
body: 6a7de6a0d3db88e0318f0f0bcb3eba08ae98535e
created_at: 2025-04-24T20:08:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -168,7 +177,30 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
 
     this.listenForAuthnSessionTimeout();
 
-    await this.setSelected2faProviderType();
+    // Initialize the cache
+    await this.twoFactorAuthComponentCacheService.init();
+
+    // Load cached form data if available
+    let loadedCachedProviderType = false;
+    const cachedData = this.twoFactorAuthComponentCacheService.getCachedData();
+    if (cachedData) {
+      if (cachedData.token) {
+        this.form.patchValue({ token: cachedData.token });
+      }
+      if (cachedData.remember !== undefined) {
+        this.form.patchValue({ remember: cachedData.remember });
+      }
+      if (cachedData.selectedProviderType !== undefined) {
+        this.selectedProviderType = cachedData.selectedProviderType;
+        loadedCachedProviderType = true;
+      }
+    }
+
+    // Only set default 2FA provider type if we don't have one from cache
+    if (!loadedCachedProviderType) {
+      await this.setSelected2faProviderType();
path: libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts
user: login: alec-livefront
body: 1. b4d2cd974d3899c4a658bfef509b29488a25c415
2. c5574ee156de1638d5a0f220f1cba44e1d67b943
created_at: 2025-04-24T20:48:47Z
author_association: CONTRIBUTOR

PR comments:
user: login: codecov[bot]
created_at: 2025-03-05T18:34:28Z
body: ## [Codecov](https://app.codecov.io/gh/bitwarden/clients/pull/13706?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden) Report
Attention: Patch coverage is `60.18519%` with `43 lines` in your changes missing coverage. Please review.
> Project coverage is 36.09%. Comparing base [(`241cbf8`)](https://app.codecov.io/gh/bitwarden/clients/commit/241cbf8cc98b98db4dac8455a48e346365c62244?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden) to head [(`6005a4a`)](https://app.codecov.io/gh/bitwarden/clients/commit/6005a4acffcda6a1fda7daf6e9cf08294f296a57?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden).
> Report is 5 commits behind head on main.

:white_check_mark: All tests successful. No failed tests found.

| [Files with missing lines](https://app.codecov.io/gh/bitwarden/clients/pull/13706?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden) | Patch % | Lines |
|---|---|---|
| [...gular/two-factor-auth/two-factor-auth.component.ts](https://app.codecov.io/gh/bitwarden/clients/pull/13706?src=pr&el=tree&filepath=libs%2Fauth%2Fsrc%2Fangular%2Ftwo-factor-auth%2Ftwo-factor-auth.component.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden#diff-bGlicy9hdXRoL3NyYy9hbmd1bGFyL3R3by1mYWN0b3ItYXV0aC90d28tZmFjdG9yLWF1dGguY29tcG9uZW50LnRz) | 12.90% | [27 Missing :warning: ](https://app.codecov.io/gh/bitwarden/clients/pull/13706?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden) |
| [...ctor-auth-email/two-factor-auth-email.component.ts](https://app.codecov.io/gh/bitwarden/clients/pull/13706?src=pr&el=tree&filepath=libs%2Fauth%2Fsrc%2Fangular%2Ftwo-factor-auth%2Fchild-components%2Ftwo-factor-auth-email%2Ftwo-factor-auth-email.component.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden#diff-bGlicy9hdXRoL3NyYy9hbmd1bGFyL3R3by1mYWN0b3ItYXV0aC9jaGlsZC1jb21wb25lbnRzL3R3by1mYWN0b3ItYXV0aC1lbWFpbC90d28tZmFjdG9yLWF1dGgtZW1haWwuY29tcG9uZW50LnRz) | 13.33% | [13 Missing :warning: ](https://app.codecov.io/gh/bitwarden/clients/pull/13706?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden) |
| [...ponents/two-factor-auth-authenticator.component.ts](https://app.codecov.io/gh/bitwarden/clients/pull/13706?src=pr&el=tree&filepath=libs%2Fauth%2Fsrc%2Fangular%2Ftwo-factor-auth%2Fchild-components%2Ftwo-factor-auth-authenticator.component.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden#diff-bGlicy9hdXRoL3NyYy9hbmd1bGFyL3R3by1mYWN0b3ItYXV0aC9jaGlsZC1jb21wb25lbnRzL3R3by1mYWN0b3ItYXV0aC1hdXRoZW50aWNhdG9yLmNvbXBvbmVudC50cw==) | 25.00% | [3 Missing :warning: ](https://app.codecov.io/gh/bitwarden/clients/pull/13706?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main   #13706      +/-   ##
==========================================
+ Coverage   36.07%   36.09%   +0.02%     
==========================================
  Files        3185     3187       +2     
  Lines       92021    92117      +96     
  Branches    16521    16544      +23     
==========================================
+ Hits        33199    33254      +55     
- Misses      56448    56487      +39     
- Partials     2374     2376       +2     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/bitwarden/clients/pull/13706?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-05T18:41:34Z
body: ![Logo](https://cdn.ast.checkmarx.net/integrations/logo/Checkmarx.png)
**Checkmarx One – Scan Summary & Details** – [bc180383\-32ed\-4544\-afdb\-b49207bbf71f](https://ast.checkmarx.net/projects/41bd746e-d417-4eda-afeb-01a59d162661/scans?id=bc180383-32ed-4544-afdb-b49207bbf71f&branch=auth%2Fpm-9115%2Fimplement-view-data-persistence-in-2FA-flows)
### Great job, no security vulnerabilities found in this Pull Request

author_association: CONTRIBUTOR
user: login: trmartin4
created_at: 2025-03-31T18:05:58Z
body: @alec-livefront I've clarified the flag name as well as the enum name on the server: https://github.com/bitwarden/server/pull/5583.  Could you update the flag and the enum name to match on `clients`?
author_association: MEMBER
user: login: alec-livefront
created_at: 2025-03-31T19:30:56Z
body: > @alec-livefront I've clarified the flag name as well as the enum name on the server: [bitwarden/server#5583](https://github.com/bitwarden/server/pull/5583). Could you update the flag and the enum name to match on `clients`?

Update @trmartin4 : [cf72430](https://github.com/bitwarden/clients/pull/13706/commits/cf72430ec7a7258709d215e08e43cac020e6c7ff)
author_association: CONTRIBUTOR
user: login: Thomas-Avery
created_at: 2025-04-17T21:21:19Z
body: Doesn't look like there is KM code being changed here anymore. Going to remove our team from reviewers for now. Feel free to add us back if there is something you would like us to look at. 
author_association: CONTRIBUTOR
user: login: sonarqubecloud[bot]
created_at: 2025-04-24T21:50:59Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=bitwarden_clients&pullRequest=13706) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=bitwarden_clients&pullRequest=13706&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=bitwarden_clients&pullRequest=13706&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=bitwarden_clients&pullRequest=13706&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=bitwarden_clients&pullRequest=13706&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=bitwarden_clients&pullRequest=13706&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=bitwarden_clients&pullRequest=13706)


author_association: NONE