Dev.to WebDev πŸ›  Dev πŸ‘ 0 πŸ“– 6 min read

Securing an Angular Firebase AI App with Email/Password Authentication (Part 3: Lightweight Home Screen, Lazy CDK Dialog, and Header Navigation)

We now have AuthService for authentication and SignInModalComponent for users to enter their email and password, and we need a lightweight home screen (HomeComponent) to connect them. When a user is unauthenticated, clic

Securing an Angular Firebase AI App with Email/Password Authentication (Part 3: Lightweight Home Screen, Lazy CDK Dialog, and Header Navigation)

We now have AuthService for authentication and SignInModalComponent for users to enter their email and password, and we need a lightweight home screen (HomeComponent) to connect them. When a user is unauthenticated, clicking the Sign In button on the home screen opens the CDK Sign-In modal. The user then enters their email and password to complete authentication and navigate to the /dashboard route.

In addition, HeaderComponent no longer displays only static text. It now renders a Home icon link on the left edge and a conditional Sign-Out button on the right edge.

Home Screen in Unauthenticated User Mode
Caption: The unauthenticated Home screen displaying the left-aligned Home icon in the header, the "Sign In" button in the hero card, and the feature metadata cards.

Architecture & Mental Model

HomeComponent and HeaderComponent share the AuthService.isAuthenticated computed signal to coordinate the UI across three states:

  • Unauthenticated /home: HeaderComponent displays only the left-aligned Home icon link (/home), and HomeComponent renders the Sign In button.
  • Lazy-loaded CDK Dialog (openSignInModal()): Clicking Sign In dynamically imports SignInModalComponent and Dialog (@angular/cdk/dialog) with Promise.all(), keeping the CDK dialog and Signal Forms out of the initial main.js bundle.
  • Authenticated /dashboard and /home: Once signed in, HeaderComponent reveals the right-aligned Sign-Out button. When an authenticated user returns to /home, HomeComponent checks isAuthenticated() and displays the Launch Studio link (/dashboard) instead of the Sign In button.
flowchart LR
    subgraph Header["HeaderComponent (app-header)"]
        HOME_ICON["HomeIconComponent ([routerLink]='/home')"]
        AUTH_HDR{"isAuthenticated()?"}
        SIGNOUT_BTN["SignOutIconComponent (onSignOut -> /home)"]
        AUTH_HDR -- "true" --> SIGNOUT_BTN
    end

    subgraph Home["HomeComponent (/home)"]
        RESTORE["constructor: ensureAuth().catch(...)"]
        AUTH_HOME{"isAuthenticated()?"}
        SIGNIN_BTN["Button: Sign In (openSignInModal)"]
        LAUNCH_LINK["Link: Launch Studio ([routerLink]='/dashboard')"]
        RESTORE --> AUTH_HOME
        AUTH_HOME -- "false" --> SIGNIN_BTN
        AUTH_HOME -- "true" --> LAUNCH_LINK
    end

    subgraph LazyModal["Deferred Modal Chunk"]
        IMPORT["Promise.all([import(SignInModalComponent), import('@angular/cdk/dialog')])"]
        INJECT["runInInjectionContext(#injector, () => inject(Dialog).open(...))"]
        IMPORT --> INJECT
    end

    SIGNIN_BTN --> IMPORT
    INJECT -- "Auth Success" --> AUTH_HOME
    INJECT -- "Auth Success" --> AUTH_HDR

1. Updating HeaderComponent with Home Navigation and Conditional Sign-Out

In HeaderComponent, we add home navigation and a conditional Sign-Out button:

  • Component imports: Imports HomeIconComponent, RouterLink, and SignOutIconComponent in the imports array.
  • Home link: Displays <app-home-icon /> inside an <a> tag with [routerLink]="homeLink" (APP_LINKS.HOME, or /home) to navigate to the home screen on click.
  • Sign-Out button: Shows the <app-sign-out-icon /> button only when isAuthenticated() is true; clicking it calls onSignOut() to sign out, update isAuthenticated(), and return to /home.
// src/app/shared/ui/layout/header/header.component.ts
@Component({
  selector: 'app-header',
  imports: [HomeIconComponent, RouterLink, SignOutIconComponent],
})
export class HeaderComponent {
  readonly homeLink = APP_LINKS.HOME;
  readonly #authService = inject(AuthService);
  readonly #router = inject(Router);
  readonly isAuthenticated = this.#authService.isAuthenticated;

  async onSignOut(): Promise<void> {
    await this.#authService.signOut();
    await this.#router.navigate([APP_LINKS.HOME]);
  }
}
<!-- src/app/shared/ui/layout/header/header.component.html -->
<header>
  <a [routerLink]="homeLink" aria-label="Go to Home Screen">
    <app-home-icon />
  </a>
  <h1>Firebase AI Logic Obscure Fact Speech Generator</h1>
  @if (isAuthenticated()) {
    <button type="button" (click)="onSignOut()" aria-label="Sign out">
      <app-sign-out-icon />
    </button>
  }
</header>

2. Building the Lightweight HomeComponent & Lazy-Loading the CDK Dialog

2.1 Restoring Auth State in the Constructor (ensureAuth().catch(...))

We need to ensure that the Firebase Auth Emulator and Firebase Auth are properly initialized when HomeComponent is constructed. However, a constructor() cannot be async, so we must execute this.#authService.ensureAuth() with a Promise chain instead of async/await.

The .catch() block logs and silently swallows any error without rethrowing it. Because this background task does not require the user's attention, showing an error message here would only cause confusion and unwanted panic.

// src/app/features/home/home.component.ts (Part 1: Fields & Constructor)
@Component({
  selector: 'app-home',
  imports: [RouterLink, ArrowRightIconComponent],
})
export class HomeComponent {
  readonly dashboard = APP_LINKS.DASHBOARD;
  readonly #authService = inject(AuthService);
  readonly isAuthenticated = this.#authService.isAuthenticated;
  readonly #injector = inject(Injector);
  #dialogRef: DialogRef<unknown, SignInModalComponent> | null = null;
  #isOpeningModal = false;

  constructor() {
    this.#authService.ensureAuth().catch((error) => console.error('Failed to restore auth session:', error));
  }

2.2 Lazy-Loading @angular/cdk/dialog & Solving the NG0203 Injection Context Error with runInInjectionContext

When opening the Sign-In modal in openSignInModal(), we keep the initial bundle small while preserving Angular's injection context:

  • Keeping main.js lightweight: Because HomeComponent is eagerly loaded into the main.js bundle, any static import increases its raw size and transfer time.
  • Lazy-loading @angular/cdk/dialog and the NG0203 error: We optimize the bundle by dynamically importing SignInModalComponent and @angular/cdk/dialog in openSignInModal(). However, calling inject(Dialog) after await Promise.all(...) throws an NG0203 error because it runs outside a valid injection context.
  • Restoring the injection context: We solve this by running the dialog logic inside the callback of runInInjectionContext using HomeComponent's Injector (this.#injector).
  • Why runInInjectionContext over a higher-order function: Using runInInjectionContext lets us assign this.#dialogRef = dialogRef directly inside an arrow function, which is cleaner than passing .bind(this) to a higher-order function.

2.3 Preventing Duplicate Modals & Resetting this.#dialogRef on Close (dialogRef.closed.subscribe)

In openSignInModal(), we prevent duplicate Sign-In modals and clean up the dialog reference on close:

  • Guarding against duplicate modals: #isOpeningModal and #dialogRef prevent opening multiple Sign-In modals; when either is truthy, a modal is already opening or open, so the method exits early.
  • Lazy-loading and opening the modal: Otherwise, we dynamically import SignInModalComponent and @angular/cdk/dialog with Promise.all(), inject Dialog to open the modal, and store its reference in this.#dialogRef.
  • Resetting #dialogRef on close: Subscribing to dialogRef.closed resets this.#dialogRef to null when the modal closes, allowing the dialog to be reopened later.
// src/app/features/home/home.component.ts (Part 2: openSignInModal)
  async openSignInModal(): Promise<void> {
    if (this.#isOpeningModal || this.#dialogRef) {
      return;
    }

    this.#isOpeningModal = true;
    try {
      const [{ SignInModalComponent }, { Dialog }] = await Promise.all([
        import('@/shared/ui/sign-in-modal/sign-in-modal.component'),
        import('@angular/cdk/dialog'),
      ]);
      runInInjectionContext(this.#injector, () => {
        const dialog = inject(Dialog);
        const dialogRef = dialog.open(SignInModalComponent, {
          backdropClass: ['bg-surface-card'],
        });

        this.#dialogRef = dialogRef;
        dialogRef.closed.subscribe(() => (this.#dialogRef = null));
      });
    } finally {
      this.#isOpeningModal = false;
    }
  }
}

2.4 Conditional CTA Rendering in home.component.html

In home.component.html, the isAuthenticated() computed signal determines which button to displayβ€”including when a user navigates back to /home by typing /home in the address bar or clicking the Home icon in HeaderComponent:

  • When isAuthenticated() is true: The Home screen renders a Launch Studio link ([routerLink]="dashboard") that routes directly to /dashboard.
  • When isAuthenticated() is false: The Home screen renders a Sign In button ((click)="openSignInModal()") that opens the CDK Sign-In modal.
<!-- src/app/features/home/home.component.html (CTA excerpt) -->
@if (isAuthenticated()) {
  <a [routerLink]="dashboard">
    <span>Launch Studio</span>
    <app-arrow-right-icon />
  </a>
} @else {
  <button type="button" (click)="openSignInModal()">
    <span>Sign In</span>
  </button>
}

Home Screen in Authenticated User Mode
Caption: The authenticated Home screen displaying the Home icon on the left and Sign-Out icon on the right of the header, and the "Launch Studio" button in the hero card.

Takeaways

In Part 3, we completed our authentication flow by connecting HomeComponent, SignInModalComponent, and HeaderComponent:

  • Conditional Actions in HomeComponent: We built a lightweight HomeComponent that conditionally renders the Sign In and Launch Studio buttons based on the isAuthenticated() signal. When isAuthenticated() is false, it displays the Sign In button; when true, it displays Launch Studio.
  • Lazy CDK Sign-In Modal Flow: Clicking Sign In opens the CDK SignInModalComponent where users enter their email and password. When the credentials are valid, the modal navigates the user to the /dashboard route; otherwise, it displays an authentication error message.
  • Resolving NG0203 with runInInjectionContext: Injecting Dialog after await Promise.all(...) in openSignInModal() triggers Angular's NG0203 error because the async continuation runs outside an injection context. We resolve this by executing the dialog logic inside the callback of runInInjectionContext using HomeComponent's Injector.
  • Header Navigation and Sign-Out: HeaderComponent adds home navigation and sign-out functionality. It always displays the Home icon to navigate back to the /home screen when clicked, while the conditional Sign-Out icon allows authenticated users to sign out from Firebase Auth and reset the isAuthenticated() signal to false.

Resources

πŸ“° Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.