
We now have AuthService for authentication and SignInModalComponent for users to enter their email...
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.
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.
HomeComponent and HeaderComponent share the AuthService.isAuthenticated computed signal to coordinate the UI across three states:
/home: HeaderComponent displays only the left-aligned Home icon link (/home), and HomeComponent renders the Sign In button.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./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
HeaderComponent with Home Navigation and Conditional Sign-OutIn HeaderComponent, we add home navigation and a conditional Sign-Out button:
HomeIconComponent, RouterLink, and SignOutIconComponent in the imports array.<app-home-icon /> inside an <a> tag with [routerLink]="homeLink" (APP_LINKS.HOME, or /home) to navigate to the home screen on click.<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>
HomeComponent & Lazy-Loading the CDK DialogensureAuth().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));
}
@angular/cdk/dialog & Solving the NG0203 Injection Context Error with runInInjectionContextWhen opening the Sign-In modal in openSignInModal(), we keep the initial bundle small while preserving Angular's injection context:
main.js lightweight: Because HomeComponent is eagerly loaded into the main.js bundle, any static import increases its raw size and transfer time.@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.runInInjectionContext using HomeComponent's Injector (this.#injector).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.this.#dialogRef on Close (dialogRef.closed.subscribe)In openSignInModal(), we prevent duplicate Sign-In modals and clean up the dialog reference on close:
#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.SignInModalComponent and @angular/cdk/dialog with Promise.all(), inject Dialog to open the modal, and store its reference in this.#dialogRef.#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;
}
}
}
home.component.htmlIn 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:
isAuthenticated() is true: The Home screen renders a Launch Studio link ([routerLink]="dashboard") that routes directly to /dashboard.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>
}
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.
In Part 3, we completed our authentication flow by connecting HomeComponent, SignInModalComponent, and HeaderComponent:
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.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.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.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.
devchallengeHey all, we have a quick update for everyone who participated in the Hacktoberfest Open-Source AI...
devchallengeOctober's MLH x DEV Writing Challenge is a wrap! This is our way of celebrating MLH hackers who take...
careerHey friends 👋 Back in June, we posted about a Community Program Manager role. Thank you to everyone...
aiWhy standard Kubernetes pods fail when AI agents start writing code and how to build a sub-second,...
discussDigital transformation, anyone?
aiWant AI to answer in a specific way? Don't just tell it. Show it. In this episode of Prompt Pad,...
Workflows from the Neura Market marketplace related to this Perplexity resource