npm package: @svt_089/angular-msal-session-persistence
Description: Enterprise-grade session persistence and idle detection library for Angular applications with Azure AD MSAL authentication. Automatically saves form data, detects computer sleep/wake cycles, and enforces re-authentication after idle timeout.
Author: Your Name
Latest version: 2.0.2

# angular-msal-session-persistence

[![npm version](https://badge.fury.io/js/angular-msal-session-persistence.svg)](https://www.npmjs.com/package/angular-msal-session-persistence)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

Enterprise-grade idle detection and form persistence library for Angular applications with Azure AD MSAL authentication.

## Table of Contents

- [Overview](#overview)
- [Key Features](#key-features)
- [Installation](#installation)
- [Quick Start](#quick-start)
  - [1. Configure MSAL](#1-configure-msal)
  - [2. Add Idle Detection](#2-add-idle-detection)
  - [3. Enable Form Persistence](#3-enable-form-persistence)
- [How It Works](#how-it-works)
  - [Idle Detection Flow](#idle-detection-flow)
  - [Form Persistence Flow](#form-persistence-flow)
  - [Smart Form Restoration](#smart-form-restoration)
- [API Reference](#api-reference)
  - [IdleDetectionService](#idledetectionservice)
  - [Idle Events](#idle-events)
  - [GuardConfigService](#guardconfigservice)
  - [Navigation Events](#navigation-events)
  - [MsalAuthService](#msalauthservice)
  - [FormPersistenceService](#formpersistenceservice)
  - [AutoPersistFormDirective](#autopersistformdirective)
  - [FormLifecycleService](#formlifecycleservice)
  - [NavigationTrackerService](#navigationtrackerservice)
- [Configuration](#configuration)
  - [Multiple Forms on Same Page](#️-important-multiple-forms-on-same-page)
  - [Form Key Generation Priority](#form-key-generation-priority)
  - [Deterministic Hash Fallback](#deterministic-hash-fallback)
  - [Best Practices for Multiple Forms](#best-practices-for-multiple-forms)
  - [Environment-Based Configuration](#environment-based-configuration)
  - [Exclude Sensitive Forms](#exclude-sensitive-forms)
- [Advanced Usage](#advanced-usage)
  - [Custom Re-Authentication UI](#custom-re-authentication-ui)
  - [Callback Pattern for Idle Events](#callback-pattern-for-idle-events)
  - [Route Guards with Custom Dialogs](#route-guards-with-custom-dialogs)
  - [Check Form State Before Navigation](#check-form-state-before-navigation)
- [Security](#security)
- [Testing](#testing)
  - [Manual Testing Checklist](#manual-testing-checklist)
  - [Testing Idle Detection](#testing-idle-detection)
  - [Debugging](#debugging)
- [Troubleshooting](#troubleshooting)
  - [Problem: Form Not Persisting](#problem-form-not-persisting)
  - [Problem: Forms Restored on Manual Refresh](#problem-forms-restored-on-manual-refresh)
  - [Problem: Idle Detection Not Working](#problem-idle-detection-not-working)
  - [Problem: Popup Blocked](#problem-popup-blocked)
  - [Problem: Multiple Forms Collision Error](#problem-multiple-forms-collision-error)
  - [Problem: Sleep/Wake Not Detected](#problem-sleepwake-not-detected)
- [Form Type Support](#form-type-support)
- [Design Philosophy](#design-philosophy)
- [Contributing](#contributing)
- [License](#license)
- [Support](#support)
- [Links](#links)

---

## Overview

`angular-msal-session-persistence` solves two critical problems for Angular applications using Azure AD MSAL authentication:

1. **Idle Detection** - Detect when users are away and enforce re-authentication for security
2. **Form Persistence** - Automatically save and restore form data to prevent data loss

The library uses a **popup-only approach for idle re-authentication** to preserve page state and form data during security checks.

## Key Features

- **Zero Configuration** - Import directive, forms automatically persist (no template changes)
- **Automatic Form Persistence** - Saves reactive forms to sessionStorage with 500ms debounce
- **Smart Form Restoration** - Restores after auth redirect, skips on manual refresh (F5)
- **Intelligent Idle Detection** - Hybrid detection using Page Visibility, Window Focus, and Page Lifecycle APIs
- **Sleep/Wake Detection** - Accurately detects computer sleep and enforces re-authentication
- **Event-Driven Architecture** - Rich events with full context, no UI coupling
- **Bring Your Own UI** - Use any dialog framework (Material, Bootstrap, custom)
- **Configurable Guards** - Custom navigation dialogs for unsaved changes
- **Security-First** - No automatic token refresh during idle, enforces manual re-authentication
- **Browser Event Monitoring** - Saves forms before critical events (unload, visibility change, freeze)
- **Drop-in Integration** - Works with existing MSAL setups (popup or redirect)

## Installation

```bash
npm install angular-msal-session-persistence
```

### Peer Dependencies

```bash
npm install @angular/common @angular/core @angular/forms @angular/router \
            @azure/msal-angular @azure/msal-browser rxjs
```

**Minimum Versions:**
- Angular: 19.x or later
- @azure/msal-angular: 3.x or later
- @azure/msal-browser: 3.x or later

---

## Quick Start

### 1. Configure MSAL

```typescript
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import {
  BrowserCacheLocation,
  IPublicClientApplication,
  PublicClientApplication
} from '@azure/msal-browser';
import {
  MSAL_INSTANCE,
  MsalService,
  MsalGuard,
  MsalBroadcastService
} from '@azure/msal-angular';

export function MSALInstanceFactory(): IPublicClientApplication {
  return new PublicClientApplication({
    auth: {
      clientId: 'YOUR_CLIENT_ID',
      authority: 'https://login.microsoftonline.com/YOUR_TENANT_ID',
      redirectUri: window.location.origin,
      postLogoutRedirectUri: window.location.origin
    },
    cache: {
      cacheLocation: BrowserCacheLocation.SessionStorage,
      storeAuthStateInCookie: false
    }
  });
}

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideHttpClient(),
    {
      provide: MSAL_INSTANCE,
      useFactory: MSALInstanceFactory
    },
    MsalService,
    MsalGuard,
    MsalBroadcastService
  ]
};
```

### 2. Add Idle Detection

Use rich events with full context and your own UI components:

```typescript
// app.component.ts
import { Component, OnInit, OnDestroy, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterOutlet } from '@angular/router';
import { Subscription } from 'rxjs';
import { filter } from 'rxjs/operators';
import {
  IdleDetectionService,
  MsalAuthService,
  IdleEventType,
  IdleUserDecision,
  IdleEvent
} from 'angular-msal-session-persistence';
import { IdleDialogComponent } from './components/idle-dialog/idle-dialog.component';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [CommonModule, RouterOutlet, IdleDialogComponent],
  template: `
    <router-outlet></router-outlet>

    <!-- Custom dialog with rich event data -->
    <app-idle-dialog
      *ngIf="showIdleDialog && currentIdleEvent"
      [event]="currentIdleEvent"
      (decision)="onIdleDialogDecision($event)">
    </app-idle-dialog>
  `
})
export class AppComponent implements OnInit, OnDestroy {
  private idleService = inject(IdleDetectionService);
  private authService = inject(MsalAuthService);
  private subscription?: Subscription;

  showIdleDialog = false;
  currentIdleEvent?: IdleEvent;

  ngOnInit(): void {
    if (this.authService.isAuthenticated()) {
      this.startIdleDetection();
    }
  }

  private startIdleDetection(): void {
    this.idleService.configure({
      idleTimeoutSeconds: 900, // 15 minutes
      enableLogging: false
    });

    // Subscribe to rich idle events with full context
    this.subscription = this.idleService.onIdleEvent$
      .pipe(filter(e => e.type === IdleEventType.TIMEOUT_EXCEEDED))
      .subscribe((event) => {
        // Show custom dialog with event context
        this.currentIdleEvent = event;
        this.showIdleDialog = true;
      });

    this.idleService.start();
  }

  async onIdleDialogDecision(decision: IdleUserDecision): Promise<void> {
    this.showIdleDialog = false;

    if (decision === IdleUserDecision.REAUTHENTICATE) {
      try {
        await this.authService.loginPopup();
        this.idleService.reset();
      } catch (error) {
        console.error('Re-authentication failed', error);
      }
    } else if (decision === IdleUserDecision.SIGN_OUT) {
      await this.authService.logoutPopup();
    }
  }

  ngOnDestroy(): void {
    this.subscription?.unsubscribe();
    this.idleService.stop();
  }
}
```


### 3. Enable Form Persistence

Simply import the directive:

```typescript
// user-form.component.ts
import { Component, OnInit, inject } from '@angular/core';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { AutoPersistFormDirective } from 'angular-msal-session-persistence';

@Component({
  selector: 'app-user-form',
  standalone: true,
  imports: [
    ReactiveFormsModule,
    AutoPersistFormDirective  // Add this - that's it!
  ],
  template: `
    <form [formGroup]="userForm" (ngSubmit)="onSubmit()">
      <input formControlName="firstName" placeholder="First Name" />
      <input formControlName="lastName" placeholder="Last Name" />
      <input formControlName="email" type="email" placeholder="Email" />
      <button type="submit" [disabled]="!userForm.valid">Submit</button>
    </form>
  `
})
export class UserFormComponent implements OnInit {
  private fb = inject(FormBuilder);
  userForm!: FormGroup;

  ngOnInit(): void {
    this.userForm = this.fb.group({
      firstName: ['', Validators.required],
      lastName: ['', Validators.required],
      email: ['', [Validators.required, Validators.email]]
    });
  }

  onSubmit(): void {
    if (this.userForm.valid) {
      console.log('Form submitted:', this.userForm.value);
    }
  }
}
```

**What happens automatically:**
- ✅ Saves to sessionStorage every 500ms after changes
- ✅ Restores after auth popup/redirect (preserves work during re-auth)
- ✅ Skips restore on manual refresh (F5 = fresh start)
- ✅ Clears after successful form submission
- ✅ Saves before page unload/visibility change

---

## How It Works

### Idle Detection Flow

1. User switches tab / minimizes window / locks computer
2. Idle timer starts
3. Timer reaches timeout (default 15 minutes)
4. `onIdleEvent$` emits rich event with context
5. Your app shows custom re-auth dialog
6. User authenticates via popup
7. Form data restored from sessionStorage

**Detection Methods:**
- **Page Visibility API** - Detects tab switches
- **Window Focus/Blur** - Detects minimize, lock screen
- **Page Lifecycle API** - Detects computer sleep/wake (Chrome/Edge)
- **Long Inactivity Check** - Fallback for missed events

**Scenarios Detected:**
- ✅ Switch to another browser tab
- ✅ Minimize browser window
- ✅ Switch to another application (Alt+Tab)
- ✅ Lock computer (Windows+L, Ctrl+Cmd+Q)
- ✅ Computer sleep/hibernate
- ✅ Virtual desktop switch
- ✅ Focus on different monitor

### Form Persistence Flow

1. User types in form
2. Debounce 500ms
3. Save to sessionStorage with key `sessionpersist_form_<route>_<unique>`
4. On component init, directive checks navigation type
5. Restores if auth redirect or SPA navigation
6. Clears if manual refresh (F5)
7. Clears on successful form submission

### Smart Form Restoration

**Restores Forms When:**
- **Auth Popup Returns** - User completed re-authentication popup (preserves work during security check)
- **SPA Navigation** - User navigates within the app, route changes
- **Browser Back/Forward** - User uses browser navigation buttons

**Skips Restoration When:**
- **Manual Page Refresh** (F5, Ctrl+R, browser refresh button) - User explicitly requested fresh start
- **First Page Load** - New session starts clean

**How Detection Works:**

```typescript
// NavigationTrackerService uses Navigation Timing API
if (sessionStorage.getItem('_auth_redirect_in_progress') === 'true') {
  return true; // Restore - auth popup return
}

const navType = performance.getEntriesByType('navigation')[0].type;
if (navType === 'reload') {
  return false; // Manual refresh - don't restore
}

return true; // Normal navigation - restore
```

---

## API Reference

### IdleDetectionService

```typescript
class IdleDetectionService {
  // Configuration
  configure(config: Partial<IdleConfig>): void;

  // Control
  start(): void;
  stop(): void;
  reset(): void;

  // State
  isActive(): boolean;
  isUserPresent(): boolean;
  getTimeRemaining(): number; // seconds until timeout

  // Events - Rich events with full context
  readonly onIdleEvent$: Observable<IdleEvent>;
}

interface IdleConfig {
  idleTimeoutSeconds: number;         // Default: 900 (15 minutes)
  enableLogging?: boolean;            // Default: false
  onTimeoutHandler?: IdleDecisionHandler;  // Optional callback pattern
  warningThresholdSeconds?: number;   // Future feature (not yet implemented)
}
```

**Example:**

```typescript
import { filter } from 'rxjs/operators';
import { IdleEventType } from 'angular-msal-session-persistence';

// Configure and start
this.idleService.configure({
  idleTimeoutSeconds: 900,
  enableLogging: true
});
this.idleService.start();

// Subscribe to rich idle events with full context
this.idleService.onIdleEvent$
  .pipe(filter(e => e.type === IdleEventType.TIMEOUT_EXCEEDED))
  .subscribe((event: IdleEvent) => {
    console.log('Idle timeout:', {
      elapsedTime: event.elapsedTimeMs,
      lastActive: event.lastActiveTimestamp,
      formsSaved: event.metadata.formsSaved,
      returnReason: event.metadata.returnReason
    });
    this.handleIdleWithContext(event);
  });

// Check state
console.log('Active:', this.idleService.isActive());
console.log('User present:', this.idleService.isUserPresent());
console.log('Time remaining:', this.idleService.getTimeRemaining());

// Reset after re-authentication
await this.authService.loginPopup();
this.idleService.reset();

// Stop on logout
this.idleService.stop();
```

### Idle Events

Rich event objects with full context for idle timeout events.

```typescript
// Event Types
enum IdleEventType {
  TIMEOUT_EXCEEDED = 'TIMEOUT_EXCEEDED',
  WARNING = 'WARNING',           // Future feature
  USER_RETURNED = 'USER_RETURNED' // Future feature
}

// Rich event object with full context
interface IdleEvent {
  type: IdleEventType;
  timestamp: Date;
  lastActiveTimestamp: Date;
  elapsedTimeMs: number;          // How long user was idle
  configuredTimeoutMs: number;    // Configured timeout value
  metadata: IdleEventMetadata;
}

// Metadata about the idle event
interface IdleEventMetadata {
  returnReason?: 'tab_visible' | 'window_focused' | 'page_resumed' | 'manual_check';
  formsSaved?: boolean;
  formCount?: number;
  [key: string]: any;  // Extensible for custom data
}

// User's decision after idle timeout
enum IdleUserDecision {
  REAUTHENTICATE = 'REAUTHENTICATE',
  SIGN_OUT = 'SIGN_OUT',
  CANCELLED = 'CANCELLED'
}

// Callback handler type (alternative to Observable pattern)
type IdleDecisionHandler = (event: IdleEvent) => Promise<IdleUserDecision | void> | IdleUserDecision | void;
```

**Example: Using Rich Events**

```typescript
import { IdleEventType, IdleUserDecision } from 'angular-msal-session-persistence';
import { filter } from 'rxjs/operators';

// Subscribe to specific event types
this.idleService.onIdleEvent$
  .pipe(filter(e => e.type === IdleEventType.TIMEOUT_EXCEEDED))
  .subscribe((event: IdleEvent) => {
    console.log('Idle Event Context:', {
      elapsedMinutes: Math.floor(event.elapsedTimeMs / 60000),
      lastActive: event.lastActiveTimestamp.toLocaleTimeString(),
      howDetected: event.metadata.returnReason,
      formsAutosaved: event.metadata.formCount
    });

    // Show custom dialog with event context
    this.showCustomIdleDialog(event);
  });
```

**Example: Using Callback Pattern**

```typescript
// Alternative: Configure callback handler in IdleConfig
this.idleService.configure({
  idleTimeoutSeconds: 900,
  onTimeoutHandler: async (event: IdleEvent) => {
    const result = await this.myCustomDialogService.show(event);
    return result; // IdleUserDecision
  }
});
```

### GuardConfigService

Configure custom navigation handlers for `UnsavedChangesGuard`.

```typescript
class GuardConfigService {
  // Register custom handler for navigation events
  setNavigationHandler(handler: NavigationDecisionHandler): void;

  // Get registered handler
  getNavigationHandler(): NavigationDecisionHandler | undefined;

  // Check if custom handler configured
  hasCustomHandler(): boolean;

  // Get default handler (window.confirm)
  getDefaultHandler(): NavigationDecisionHandler;

  // Clear registered handler
  clearNavigationHandler(): void;
}
```

**Example: Configure Custom Navigation Dialog**

```typescript
import { GuardConfigService, NavigationDecision } from 'angular-msal-session-persistence';

@Component({
  selector: 'app-root',
  // ...
})
export class AppComponent implements OnInit {
  private guardConfig = inject(GuardConfigService);

  ngOnInit(): void {
    // Configure custom navigation handler
    this.guardConfig.setNavigationHandler(async (event: UnsavedChangesEvent) => {
      // Show your custom dialog
      const userWantsToProceed = await this.showCustomNavigationDialog(event);

      return userWantsToProceed
        ? NavigationDecision.PROCEED
        : NavigationDecision.STAY;
    });
  }

  private async showCustomNavigationDialog(event: UnsavedChangesEvent): Promise<boolean> {
    // Example with Material Dialog
    const dialogRef = this.dialog.open(UnsavedChangesDialogComponent, {
      data: event,
      disableClose: true
    });

    return await dialogRef.afterClosed().toPromise();
  }
}
```

### Navigation Events

Rich event objects for navigation blocking with unsaved changes.

```typescript
// Event emitted when navigation is blocked
interface UnsavedChangesEvent {
  componentName?: string;
  currentRoute: string;
  targetRoute: string;
  formSummary?: {
    changedFieldCount?: number;
    changedFields?: string[];
    [key: string]: any;
  };
  timestamp: Date;
  metadata?: {
    [key: string]: any;
  };
}

// User's navigation decision
enum NavigationDecision {
  PROCEED = 'PROCEED',  // Allow navigation (discard changes)
  STAY = 'STAY'         // Block navigation (keep changes)
}

// Callback handler type for navigation events
type NavigationDecisionHandler = (event: UnsavedChangesEvent) => Promise<NavigationDecision> | NavigationDecision;
```

**Example: Using Navigation Events**

```typescript
// The guard automatically calls your configured handler
// when navigation is attempted with unsaved changes

this.guardConfig.setNavigationHandler(async (event: UnsavedChangesEvent) => {
  console.log('Navigation blocked:', {
    from: event.currentRoute,
    to: event.targetRoute,
    component: event.componentName
  });

  // Show dialog, return user decision
  const decision = await this.myDialog.show(event);
  return decision;
});
```

### MsalAuthService

```typescript
class MsalAuthService {
  // Authentication
  isAuthenticated(): boolean;
  getAccount(): AccountInfo | null;
  getAllAccounts(): AccountInfo[];
  loginPopup(scopes?: string[]): Promise<AuthenticationResult>;
  logoutPopup(account?: AccountInfo): Promise<void>;

  // Observables
  readonly isAuthenticated$: Observable<boolean>;
  readonly account$: Observable<AccountInfo | null>;
}
```

**Example:**

```typescript
// Check authentication
if (this.authService.isAuthenticated()) {
  const account = this.authService.getAccount();
  console.log('Logged in as:', account?.name);
}

// Listen to auth changes
this.authService.isAuthenticated$.subscribe(isAuth => {
  if (isAuth) {
    this.startIdleDetection();
  } else {
    this.idleService.stop();
  }
});

// Login/Logout
await this.authService.loginPopup(['user.read']);
await this.authService.logoutPopup();
```

### FormPersistenceService

```typescript
class FormPersistenceService {
  // Auto-save
  enableAutoSave(form: FormGroup, key: string, debounceMs?: number): void;
  disableAutoSave(key: string): void;

  // Manual operations
  saveFormState(form: FormGroup, key: string): boolean;
  restoreFormState(form: FormGroup, key: string): boolean;
  clearFormState(key: string): void;
  clearAllFormStates(): void;

  // State checking
  hasUnsavedChanges(form: FormGroup): boolean;
  hasSavedState(key: string): boolean;
}
```

**Example:**

```typescript
// Manual save/restore
this.formPersistence.saveFormState(this.myForm, 'my-form-key');
this.formPersistence.restoreFormState(this.myForm, 'my-form-key');

// Check state
if (this.formPersistence.hasSavedState('my-form-key')) {
  console.log('Form has saved data');
}

if (this.formPersistence.hasUnsavedChanges(this.myForm)) {
  console.log('Form has unsaved changes');
}

// Clear state
this.formPersistence.clearFormState('my-form-key');
this.formPersistence.clearAllFormStates();
```

### AutoPersistFormDirective

```typescript
@Directive({
  selector: '[formGroup]',  // Automatically attaches to ALL forms
  standalone: true
})
class AutoPersistFormDirective {
  @Input() autoPersist: boolean = true;   // Enable/disable
  @Input() persistKey?: string;           // Custom storage key
  @Input() debounceMs: number = 500;     // Debounce time
}
```

**Examples:**

```html
<!-- Default: Automatic persistence -->
<form [formGroup]="myForm">
  <!-- Automatically persists -->
</form>

<!-- Disable for sensitive forms -->
<form [formGroup]="loginForm" [autoPersist]="false">
  <input formControlName="password" type="password" />
</form>

<!-- Custom storage key -->
<form [formGroup]="checkoutForm" [persistKey]="'checkout-cart'">
  <!-- Uses 'checkout-cart' instead of auto-generated key -->
</form>

<!-- Custom debounce time -->
<form [formGroup]="criticalForm" [debounceMs]="200">
  <!-- Saves 200ms after last change (faster) -->
</form>

<!-- Combine options -->
<form [formGroup]="form"
      [persistKey]="'user-profile'"
      [debounceMs]="300">
</form>
```

### FormLifecycleService

```typescript
class FormLifecycleService {
  registerForm(form: FormGroup, key: string): void;
  unregisterForm(key: string): void;
  saveAllForms(): void;
}
```

**Example:**

```typescript
// Save all forms before navigation
saveBeforeNavigation(): void {
  this.formLifecycle.saveAllForms();
  this.router.navigate(['/next-page']);
}
```

### NavigationTrackerService

```typescript
class NavigationTrackerService {
  shouldRestoreForms(): boolean;
  markAuthRedirect(): void;
  clearAll(): void;
  getDebugInfo(): {
    isAuthRedirect: boolean;
    navigationType: 'navigate' | 'reload' | 'back_forward' | undefined;
    shouldRestore: boolean;
  };
}
```

**Example (Advanced):**

```typescript
// Debug navigation state
const debug = this.navTracker.getDebugInfo();
console.log('Navigation type:', debug.navigationType);
console.log('Should restore:', debug.shouldRestore);
```

---

## Configuration

### ⚠️ IMPORTANT: Multiple Forms on Same Page

**CRITICAL:** When you have multiple forms on the same page, you **MUST** provide unique keys to prevent data collisions.

#### ❌ Wrong - Forms Will Collide

```html
<!-- DON'T DO THIS -->
<form [formGroup]="billingForm">...</form>
<form [formGroup]="shippingForm">...</form>
<!-- Both use same key: form__checkout_default -->
<!-- Forms will overwrite each other's data! -->
```

The library will throw an error:
```
Error: Form key collision detected: form__checkout_default
```

#### ✅ Correct - Each Form Has Unique Key

**Option 1: Using [persistKey]**

```html
<form [formGroup]="billingForm" [persistKey]="'checkout-billing'">...</form>
<form [formGroup]="shippingForm" [persistKey]="'checkout-shipping'">...</form>
```

**Option 2: Using HTML id attribute**

```html
<form [formGroup]="billingForm" id="billing">...</form>
<form [formGroup]="shippingForm" id="shipping">...</form>
```

**Option 3: Using HTML name attribute**

```html
<form [formGroup]="billingForm" name="billing">...</form>
<form [formGroup]="shippingForm" name="shipping">...</form>
```

### Form Key Generation Priority

The directive checks for identifiers in this order:

1. **`[persistKey]`** input → Explicit control (recommended for dynamic forms)
2. **`id`** attribute → Semantic, accessible (recommended for static forms)
3. **`name`** attribute → Semantic, standard HTML
4. **`data-persist-key`** attribute → Custom data attribute
5. **Deterministic hash** → Automatic fallback (based on route + classes + controls + position)

**Examples:**

```html
<!-- Priority 1: Explicit [persistKey] -->
<form [formGroup]="myForm" [persistKey]="'user-profile'">
  <!-- Key: form_user-profile -->
  <!-- ✅ Most explicit, full control, best for dynamic forms -->
</form>

<!-- Priority 2: HTML id -->
<form [formGroup]="myForm" id="billing-form">
  <!-- Key: form__checkout_billing-form -->
  <!-- ✅ Semantic, accessible, best for static forms -->
</form>

<!-- Priority 3: HTML name -->
<form [formGroup]="myForm" name="contact-form">
  <!-- Key: form__checkout_contact-form -->
  <!-- ✅ Standard HTML, works with form submission -->
</form>

<!-- Priority 4: data-persist-key -->
<form [formGroup]="myForm" data-persist-key="notes">
  <!-- Key: form__checkout_notes -->
  <!-- ✅ Custom attribute, explicit -->
</form>

<!-- Priority 5: Deterministic hash (fallback) -->
<form [formGroup]="myForm" class="user-form">
  <!-- Key: form__checkout_5m8k3p9n -->
  <!-- ✅ Automatic, deterministic (same form = same key) -->
  <!-- Hash based on: route + classes + control names + position -->
</form>
```

### Deterministic Hash Fallback

When no explicit identifier is provided, the library generates a **deterministic hash** based on:

1. **Route** - The page URL path
2. **CSS Classes** - Element classes (excluding Angular internal classes)
3. **Form Controls** - Names of all form controls (sorted)
4. **Position** - Index among sibling `<form>` elements

**Benefits:**
- ✅ Same form structure = same key across page reloads
- ✅ No random numbers or counters
- ✅ Unique even for identical forms (via position)
- ✅ Predictable and stable

**Example:**

```html
<form [formGroup]="billingForm" class="billing-form card">
  <input formControlName="address" />
  <input formControlName="city" />
</form>

<!-- Key calculation:
  Route: _checkout
  Classes: billing_form_card
  Controls: address_city
  Position: 0
  Hash: 8k3m9p5n
  Final Key: form__checkout_8k3m9p5n
-->
```

**Console Output:**

```
[AutoPersistFormDirective] Generated deterministic key:
  Route: _checkout
  Classes: billing_form_card
  Controls: address_city
  Position: 0
  Hash: 8k3m9p5n
  Key: form__checkout_8k3m9p5n
  Tip: Add id="unique-id" for explicit control over keys
```

### Best Practices for Multiple Forms

```html
<!-- ✅ BEST: Using HTML id (semantic + accessible) -->
<div class="checkout-page">
  <form [formGroup]="billingForm" id="billing">
    <h3>Billing Address</h3>
    <input formControlName="address" />
  </form>

  <form [formGroup]="shippingForm" id="shipping">
    <h3>Shipping Address</h3>
    <input formControlName="address" />
  </form>
</div>

<!-- ✅ ALSO GOOD: Using [persistKey] (explicit) -->
<div class="checkout-page">
  <form [formGroup]="billingForm" [persistKey]="'checkout-billing'">
  <form [formGroup]="shippingForm" [persistKey]="'checkout-shipping'">
</div>

<!-- ✅ WORKS: Deterministic hash with classes -->
<div class="checkout-page">
  <form [formGroup]="billingForm" class="billing-form">
    <!-- Key: form__checkout_5m8k3p9n -->
  </form>
  <form [formGroup]="shippingForm" class="shipping-form">
    <!-- Key: form__checkout_7p2k9m4q -->
    <!-- Different class = different hash -->
  </form>
</div>
```

### Environment-Based Configuration

```typescript
// environment.ts (development)
export const environment = {
  production: false,
  idleTimeout: 60,        // 1 minute for testing
  enableIdleLogging: true
};

// environment.prod.ts (production)
export const environment = {
  production: true,
  idleTimeout: 900,       // 15 minutes
  enableIdleLogging: false
};

// app.component.ts
import { environment } from '../environments/environment';

ngOnInit(): void {
  this.idleService.configure({
    idleTimeoutSeconds: environment.idleTimeout,
    enableLogging: environment.enableIdleLogging
  });
  this.idleService.start();
}
```

### Exclude Sensitive Forms

```html
<!-- Login form - don't persist password -->
<form [formGroup]="loginForm" [autoPersist]="false">
  <input formControlName="username" />
  <input formControlName="password" type="password" />
</form>

<!-- Payment form - don't persist card numbers -->
<form [formGroup]="paymentForm" [autoPersist]="false">
  <input formControlName="cardNumber" />
  <input formControlName="cvv" type="password" />
</form>
```

---

## Advanced Usage

### Custom Re-Authentication UI

Use Material Dialog or any other dialog framework with rich idle events:

```typescript
// idle-dialog.component.ts
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { IdleEvent, IdleUserDecision } from 'angular-msal-session-persistence';

@Component({
  selector: 'app-idle-dialog',
  template: `
    <h2 mat-dialog-title>⏱️ Session Idle</h2>

    <mat-dialog-content>
      <p>Your session has been idle for {{ formatDuration(data.elapsedTimeMs) }}</p>

      <mat-list>
        <mat-list-item>
          <mat-icon matListItemIcon>schedule</mat-icon>
          <div matListItemTitle>Last Active</div>
          <div matListItemLine>{{ data.lastActiveTimestamp | date:'short' }}</div>
        </mat-list-item>

        <mat-list-item *ngIf="data.metadata.formsSaved">
          <mat-icon matListItemIcon>save</mat-icon>
          <div matListItemTitle>Forms Saved</div>
          <div matListItemLine>{{ data.metadata.formCount || 0 }} form(s)</div>
        </mat-list-item>

        <mat-list-item *ngIf="data.metadata.returnReason">
          <mat-icon matListItemIcon>info</mat-icon>
          <div matListItemTitle>Detected via</div>
          <div matListItemLine>{{ formatReturnReason(data.metadata.returnReason) }}</div>
        </mat-list-item>
      </mat-list>

      <mat-hint>Your form data has been saved and will be restored after re-authentication.</mat-hint>
    </mat-dialog-content>

    <mat-dialog-actions align="end">
      <button mat-button (click)="dialogRef.close(IdleUserDecision.SIGN_OUT)">
        Sign Out
      </button>
      <button mat-raised-button color="primary"
              (click)="dialogRef.close(IdleUserDecision.REAUTHENTICATE)">
        Re-authenticate
      </button>
    </mat-dialog-actions>
  `
})
export class IdleDialogComponent {
  IdleUserDecision = IdleUserDecision;

  constructor(
    public dialogRef: MatDialogRef<IdleDialogComponent>,
    @Inject(MAT_DIALOG_DATA) public data: IdleEvent
  ) {}

  formatDuration(ms: number): string {
    const minutes = Math.floor(ms / 60000);
    const seconds = Math.floor((ms % 60000) / 1000);
    return `${minutes}m ${seconds}s`;
  }

  formatReturnReason(reason: string): string {
    const map: Record<string, string> = {
      'tab_visible': 'Tab became visible',
      'window_focused': 'Window gained focus',
      'page_resumed': 'Page resumed from sleep',
      'manual_check': 'Manual check'
    };
    return map[reason] || reason;
  }
}

// In app.component.ts
import { MatDialog } from '@angular/material/dialog';
import { filter } from 'rxjs/operators';

ngOnInit(): void {
  this.idleService.onIdleEvent$
    .pipe(filter(e => e.type === IdleEventType.TIMEOUT_EXCEEDED))
    .subscribe(async (event: IdleEvent) => {
      // Open Material Dialog with event data
      const dialogRef = this.dialog.open(IdleDialogComponent, {
        data: event,
        disableClose: true,
        width: '500px'
      });

      const decision = await dialogRef.afterClosed().toPromise();

      if (decision === IdleUserDecision.REAUTHENTICATE) {
        try {
          await this.authService.loginPopup();
          this.idleService.reset();
        } catch (error) {
          console.error('Re-authentication failed', error);
        }
      } else if (decision === IdleUserDecision.SIGN_OUT) {
        await this.authService.logoutPopup();
      }
    });
}
```

### Callback Pattern for Idle Events

Alternative to Observable pattern - use callback configuration:

```typescript
// Configure callback handler directly in IdleConfig
this.idleService.configure({
  idleTimeoutSeconds: 900,
  enableLogging: false,
  onTimeoutHandler: async (event: IdleEvent) => {
    // Open dialog
    const dialogRef = this.dialog.open(IdleDialogComponent, {
      data: event,
      disableClose: true
    });

    // Return user decision
    const decision = await dialogRef.afterClosed().toPromise();
    return decision;
  }
});

// No need to subscribe to onIdleEvent$ - callback is invoked automatically
this.idleService.start();
```

This approach:
- ✅ Cleaner for simple use cases
- ✅ No need to manage subscriptions
- ✅ Handler is called automatically when timeout occurs
- ✅ Can still use onIdleEvent$ Observable if needed

### Route Guards with Custom Dialogs

Use the built-in `UnsavedChangesGuard` with custom dialog configuration:

```typescript
// Step 1: Configure custom navigation handler in app.component.ts
import {
  GuardConfigService,
  NavigationDecision,
  UnsavedChangesEvent
} from 'angular-msal-session-persistence';

@Component({ /* ... */ })
export class AppComponent implements OnInit {
  private guardConfig = inject(GuardConfigService);

  showUnsavedChangesDialog = false;
  currentNavigationEvent?: UnsavedChangesEvent;
  private navigationDecisionResolve?: (decision: NavigationDecision) => void;

  ngOnInit(): void {
    // Configure custom navigation handler
    this.guardConfig.setNavigationHandler(async (event: UnsavedChangesEvent) => {
      console.log('Navigation blocked:', event);

      // Show custom dialog
      this.currentNavigationEvent = event;
      this.showUnsavedChangesDialog = true;

      // Return promise that resolves when user decides
      return new Promise<NavigationDecision>((resolve) => {
        this.navigationDecisionResolve = resolve;
      });
    });
  }

  onNavigationDialogDecision(decision: NavigationDecision): void {
    this.showUnsavedChangesDialog = false;
    this.currentNavigationEvent = undefined;

    if (this.navigationDecisionResolve) {
      this.navigationDecisionResolve(decision);
      this.navigationDecisionResolve = undefined;
    }
  }
}

// Step 2: Use UnsavedChangesGuard in routes
import { UnsavedChangesGuard } from 'angular-msal-session-persistence';

export const routes: Routes = [
  {
    path: 'form',
    component: FormComponent,
    canDeactivate: [UnsavedChangesGuard]
  }
];

// Step 3: Implement canDeactivate in component
export class FormComponent implements ComponentWithUnsavedChanges {
  canDeactivate(): boolean {
    // Return false to block navigation (triggers custom dialog)
    return !this.form.dirty;
  }
}
```

**With Material Dialog:**

```typescript
// unsaved-changes-dialog.component.ts
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { UnsavedChangesEvent } from 'angular-msal-session-persistence';

@Component({
  selector: 'app-unsaved-changes-dialog',
  template: `
    <h2 mat-dialog-title>⚠️ Unsaved Changes</h2>

    <mat-dialog-content>
      <p>You have unsaved changes. Discard them?</p>

      <mat-card appearance="outlined">
        <mat-card-content>
          <p><strong>From:</strong> <code>{{ data.currentRoute }}</code></p>
          <p><strong>To:</strong> <code>{{ data.targetRoute }}</code></p>
          <p *ngIf="data.componentName">
            <strong>Component:</strong> {{ data.componentName }}
          </p>
        </mat-card-content>
      </mat-card>
    </mat-dialog-content>

    <mat-dialog-actions align="end">
      <button mat-button [mat-dialog-close]="false">
        Stay on Page
      </button>
      <button mat-raised-button color="warn" [mat-dialog-close]="true">
        Discard Changes
      </button>
    </mat-dialog-actions>
  `
})
export class UnsavedChangesDialogComponent {
  constructor(
    @Inject(MAT_DIALOG_DATA) public data: UnsavedChangesEvent
  ) {}
}

// In app.component.ts
this.guardConfig.setNavigationHandler(async (event: UnsavedChangesEvent) => {
  const dialogRef = this.dialog.open(UnsavedChangesDialogComponent, {
    data: event,
    disableClose: true
  });

  const userWantsToProceed = await dialogRef.afterClosed().toPromise();

  return userWantsToProceed
    ? NavigationDecision.PROCEED
    : NavigationDecision.STAY;
});
```

### Check Form State Before Navigation

```typescript
import { FormPersistenceService } from 'angular-msal-session-persistence';

export class MyComponent {
  private formPersistence = inject(FormPersistenceService);

  canDeactivate(): boolean {
    if (this.formPersistence.hasUnsavedChanges(this.myForm)) {
      return confirm('You have unsaved changes. Leave?');
    }
    return true;
  }

  ngOnInit(): void {
    // Check if saved state exists
    if (this.formPersistence.hasSavedState('my-form-key')) {
      console.log('Form has previously saved data');
    }
  }
}
```

---

## Security

### What This Library Does

✅ **Monitors user presence** using Page Visibility API, Window Focus, and Page Lifecycle API
✅ **Detects idle timeout** based on wall-clock time while user is away
✅ **Shows re-auth prompt** when user returns after timeout
✅ **Saves form data** to sessionStorage (cleared on tab close)
✅ **Enforces manual re-auth** via popup (preserves page state)

### What This Library Does NOT Do

❌ **Never** calls `ssoSilent()` or `acquireTokenSilent()` automatically
❌ **Never** refreshes tokens while user is idle/away
❌ **Never** stores tokens in localStorage (only sessionStorage)
❌ **Never** bypasses re-authentication requirement
❌ **Never** persists passwords or sensitive data (unless you explicitly enable it)

### Best Practices

1. **Use sessionStorage** - Tokens and form data cleared when tab closes
2. **Enforce re-authentication** - No automatic token refresh during idle
3. **Exclude sensitive forms** - Use `[autoPersist]="false"` for login/payment forms
4. **Configure appropriate timeout**:
   - Development: 60 seconds (for testing)
   - Production: 900 seconds (15 minutes)
   - High Security: 300 seconds (5 minutes)

### Data Storage

```
sessionStorage (cleared on tab close):
  ├── msal.* (MSAL tokens - managed by MSAL library)
  └── sessionpersist_form_* (Form data - managed by this library)

localStorage:
  └── (Not used by this library)
```

---

## Testing

### Manual Testing Checklist

1. ✓ **Login** → User logs in successfully
2. ✓ **Form Fill** → Fill form, see data auto-save to sessionStorage
3. ✓ **Manual Refresh** → Hit F5, form should clear (fresh start)
4. ✓ **Re-Auth Popup** → Complete auth popup, form should restore
5. ✓ **Form Submit** → Submit form, data should clear from sessionStorage
6. ✓ **Navigate Away** → Navigate to another page, unsaved changes warning (if guard enabled)
7. ✓ **Close Tab** → Close tab, reopen, data should be gone
8. ✓ **Remain Idle** → Wait 15 minutes, timeout dialog should appear
9. ✓ **Re-Authenticate** → After idle, re-auth via popup, form preserved
10. ✓ **Computer Sleep** → Close laptop, wait 15+ min, open, timeout detected

### Testing Idle Detection

```typescript
// Reduce timeout for faster testing
this.idleService.configure({
  idleTimeoutSeconds: 60,  // 1 minute instead of 15
  enableLogging: true      // See console logs
});

this.idleService.start();
```

**Test Sequence:**
1. Fill out a form with some data
2. Switch to another browser tab (or minimize window)
3. Wait 60 seconds
4. Switch back to the tab
5. You should see the re-authentication dialog
6. Click OK and authenticate
7. Form should be restored with your data

**Console Output (with logging enabled):**

```
✅ Idle detection started
👁️ User went away (tab hidden)
⏱️ Starting idle timer
⏱️ Idle timeout detected (60 seconds elapsed)
💾 Saving all forms
🔐 Showing re-authentication dialog
✅ Re-authentication successful
⏱️ Idle timer reset
📝 Form restored from sessionStorage
```

### Debugging

```typescript
// 1. Enable logging
this.idleService.configure({ enableLogging: true });

// 2. Check service state
console.log('Service active:', this.idleService.isActive());
console.log('User present:', this.idleService.isUserPresent());
console.log('Time remaining:', this.idleService.getTimeRemaining(), 'seconds');

// 3. Check sessionStorage
console.log('SessionStorage keys:', Object.keys(sessionStorage));
console.log('Form keys:', Object.keys(sessionStorage).filter(k => k.startsWith('sessionpersist_form_')));

// 4. Subscribe to idle events
this.idleService.onIdleEvent$
  .pipe(filter(e => e.type === IdleEventType.TIMEOUT_EXCEEDED))
  .subscribe((event) => {
    console.log('🔔 Idle event fired!', event);
  });

// 5. Debug navigation tracking
const navTracker = inject(NavigationTrackerService);
console.log('Navigation debug:', navTracker.getDebugInfo());
// Output: { isAuthRedirect: false, navigationType: 'reload', shouldRestore: false }
```

---

## Troubleshooting

### Problem: Form Not Persisting

**Symptom:** Form data not saved or restored

**Check:**
1. ✓ Directive imported: `imports: [AutoPersistFormDirective]`
2. ✓ Form has `[formGroup]`: `<form [formGroup]="myForm">`
3. ✓ Not disabled: Remove `[autoPersist]="false"` if present
4. ✓ sessionStorage enabled in browser

**Solution:**

```typescript
// Ensure directive is imported
@Component({
  imports: [CommonModule, ReactiveFormsModule, AutoPersistFormDirective]
})

// Ensure form has [formGroup]
<form [formGroup]="myForm"> <!-- Required! -->

// Enable persistence if disabled
<form [formGroup]="myForm" [autoPersist]="true">
```

### Problem: Forms Restored on Manual Refresh

**Symptom:** Pressing F5 or Ctrl+R clears form data

**This is expected behavior!** The library intentionally clears forms on manual refresh to give users a fresh start.

**Restoration Behavior:**

| Action | Form Data | Why |
|--------|-----------|-----|
| F5 / Ctrl+R / Browser Refresh | ❌ Cleared | User explicitly requested fresh start |
| Auth popup return | ✅ Restored | Preserve work during re-authentication |
| Browser back/forward | ✅ Restored | Return to previous context |
| SPA navigation | ✅ Restored | Switch between routes |

**Debug Navigation Type:**

```typescript
import { NavigationTrackerService } from 'angular-msal-session-persistence';

constructor(private navTracker: NavigationTrackerService) {}

ngOnInit(): void {
  const debug = this.navTracker.getDebugInfo();
  console.log('Navigation type:', debug.navigationType);
  // 'reload' = manual refresh (forms cleared)
  // 'navigate' = SPA navigation (forms restored)
  // 'back_forward' = browser back/forward (forms restored)

  console.log('Should restore:', debug.shouldRestore);
  console.log('Is auth redirect:', debug.isAuthRedirect);
}
```

### Problem: Idle Detection Not Working

**Symptom:** Timeout never triggers

**Check:**
1. ✓ Service started: `this.idleService.start()`
2. ✓ Subscribed to events: `this.idleService.onIdleEvent$.pipe(filter(...)).subscribe(...)`
3. ✓ Timeout not too long: Test with 60 seconds
4. ✓ User actually away: Must switch tabs or minimize window

**Diagnosis:**

```typescript
// Check if service is started
console.log('Active:', this.idleService.isActive()); // Should be true

// Check if subscription exists
this.subscription = this.idleService.onIdleEvent$
  .pipe(filter(e => e.type === IdleEventType.TIMEOUT_EXCEEDED))
  .subscribe(() => {
    console.log('Event fired!'); // Should see this after timeout
  });

// Test with shorter timeout
this.idleService.configure({ idleTimeoutSeconds: 60 }); // 1 minute

// Check if user is away
console.log('User present:', this.idleService.isUserPresent()); // Should be false when away
```

**Solution:**

```typescript
ngOnInit(): void {
  // 1. Configure
  this.idleService.configure({
    idleTimeoutSeconds: 60,
    enableLogging: true  // Enable logging
  });

  // 2. Subscribe
  this.subscription = this.idleService.onIdleEvent$
    .pipe(filter(e => e.type === IdleEventType.TIMEOUT_EXCEEDED))
    .subscribe((event) => {
      console.log('Idle timeout!', event);
      this.handleIdleTimeout();
    });

  // 3. Start
  this.idleService.start();

  // 4. Verify
  console.log('Service active:', this.idleService.isActive());
}
```

### Problem: Popup Blocked

**Symptom:** Browser blocks authentication popup

**Causes:**
- Popup not triggered by user interaction
- Browser popup blocker enabled
- Third-party extension blocking popups

**Solutions:**

```typescript
// ✅ GOOD: Triggered by user click
<button (click)="login()">Login</button>

login(): void {
  this.authService.loginPopup(); // Works!
}

// ❌ BAD: Triggered automatically
ngOnInit(): void {
  this.authService.loginPopup(); // May be blocked!
}
```

**Workarounds:**
1. Ensure dialog is triggered by user clicking OK/Re-authenticate button
2. Check browser popup blocker settings
3. Add your domain to allowed sites
4. Use a visible button/link to trigger popup

### Problem: Multiple Forms Collision Error

**Symptom:** Error message: `Form key collision detected`

**Cause:** Multiple forms on same page without unique keys

**Solution:**

```html
<!-- Add unique keys to each form -->
<form [formGroup]="billingForm" [persistKey]="'checkout-billing'">...</form>
<form [formGroup]="shippingForm" [persistKey]="'checkout-shipping'">...</form>

<!-- Or use id/name attributes -->
<form [formGroup]="billingForm" id="billing">...</form>
<form [formGroup]="shippingForm" id="shipping">...</form>
```

### Problem: Sleep/Wake Not Detected

**Symptom:** Computer sleep doesn't trigger re-authentication

**Solution:**

The library uses multiple detection methods that work together:

```typescript
// Enable logging to see which events fire
this.idleService.configure({ enableLogging: true });

// Test sequence:
// 1. Close laptop lid
// 2. Wait 2+ minutes
// 3. Open laptop
// 4. Return to browser tab
// 5. Should see re-auth dialog

// Check browser support
console.log('Page Lifecycle API supported:', 'onfreeze' in document);
```

---

## Form Type Support

### Supported: Reactive Forms

✅ **Fully Supported** - All features work with Reactive Forms (`[formGroup]`)

```typescript
// Reactive Forms
export class MyComponent {
  form = this.fb.group({
    firstName: [''],
    lastName: ['']
  });
}
```

```html
<form [formGroup]="form">
  <input formControlName="firstName" />
  <input formControlName="lastName" />
</form>
```

### Not Supported: Template-Driven Forms

❌ **Not Supported** - Template-Driven Forms (`ngForm`) are not currently supported

```html
<!-- This will NOT work -->
<form #myForm="ngForm">
  <input name="firstName" [(ngModel)]="user.firstName" />
  <input name="lastName" [(ngModel)]="user.lastName" />
</form>
```

**Why Not Supported:**
- Uses completely different Angular API (`NgForm` vs `FormGroup`)
- Would require separate directive implementation
- Most modern Angular apps use Reactive Forms

**Future Enhancement:** Template-Driven support may be added in a future release if requested by users.

---

## Design Philosophy

> **This library handles idle detection and form persistence. Your application continues to handle initial authentication (popup or redirect) - we only use popup for idle re-authentication to preserve page state.**

**Why This Approach?**

1. **Separation of Concerns** - Initial auth is your app's responsibility
2. **Page State Preservation** - Popup for idle re-auth keeps forms in memory
3. **Flexibility** - Works with apps using popup OR redirect for initial login
4. **User Experience** - No disruptive full-page reloads during security checks

**Example Scenarios:**

```typescript
// Scenario 1: Your app uses POPUP for initial login
this.msalService.loginPopup({ scopes: ['user.read'] });
// ✅ Package also uses popup for idle re-auth - consistent!

// Scenario 2: Your app uses REDIRECT for initial login
this.msalService.loginRedirect({ scopes: ['user.read'] });
// ✅ Package uses popup for idle re-auth - preserves state!
```

---

## Contributing

Contributions are welcome! Please:

1. Fork the repository
2. Create feature branch: `git checkout -b feature/my-feature`
3. Write tests for your changes
4. Follow coding standards (ESLint + Prettier)
5. Update documentation if needed
6. Submit a pull request

**Development Setup:**

```bash
git clone https://github.com/yourusername/idlesession
cd idlesession
npm install
npm run build:sessionpersist
```

---

## License

MIT License - see [LICENSE](LICENSE) file for details.

## Support

- 📫 [GitHub Issues](https://github.com/yourusername/idlesession/issues)
- 💬 [GitHub Discussions](https://github.com/yourusername/idlesession/discussions)
- 🌟 Star the repo if you find it useful!

## Links

- [npm Package](https://www.npmjs.com/package/angular-msal-session-persistence)
- [GitHub Repository](https://github.com/yourusername/idlesession)
- [Azure AD Documentation](https://docs.microsoft.com/azure/active-directory/)
- [MSAL.js Documentation](https://github.com/AzureAD/microsoft-authentication-library-for-js)

---

**Version**: 2.0.0 | **License**: MIT