valtech-components 2.0.452 → 2.0.453

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/esm2022/lib/services/auth/auth-state.service.mjs +173 -0
  2. package/esm2022/lib/services/auth/auth.service.mjs +454 -0
  3. package/esm2022/lib/services/auth/config.mjs +76 -0
  4. package/esm2022/lib/services/auth/guards.mjs +194 -0
  5. package/esm2022/lib/services/auth/index.mjs +70 -0
  6. package/esm2022/lib/services/auth/interceptor.mjs +98 -0
  7. package/esm2022/lib/services/auth/storage.service.mjs +141 -0
  8. package/esm2022/lib/services/auth/sync.service.mjs +149 -0
  9. package/esm2022/lib/services/auth/token.service.mjs +113 -0
  10. package/esm2022/lib/services/auth/types.mjs +29 -0
  11. package/esm2022/lib/services/firebase/config.mjs +108 -0
  12. package/esm2022/lib/services/firebase/firebase.service.mjs +288 -0
  13. package/esm2022/lib/services/firebase/firestore-collection.mjs +254 -0
  14. package/esm2022/lib/services/firebase/firestore.service.mjs +509 -0
  15. package/esm2022/lib/services/firebase/index.mjs +49 -0
  16. package/esm2022/lib/services/firebase/messaging.service.mjs +512 -0
  17. package/esm2022/lib/services/firebase/shared-config.mjs +138 -0
  18. package/esm2022/lib/services/firebase/storage.service.mjs +422 -0
  19. package/esm2022/lib/services/firebase/types.mjs +8 -0
  20. package/esm2022/lib/services/firebase/utils/path-builder.mjs +195 -0
  21. package/esm2022/lib/services/firebase/utils/query-builder.mjs +302 -0
  22. package/esm2022/public-api.mjs +3 -5
  23. package/fesm2022/valtech-components.mjs +4195 -4
  24. package/fesm2022/valtech-components.mjs.map +1 -1
  25. package/lib/services/auth/auth-state.service.d.ts +85 -0
  26. package/lib/services/auth/auth.service.d.ts +146 -0
  27. package/lib/services/auth/config.d.ts +38 -0
  28. package/lib/services/auth/guards.d.ts +123 -0
  29. package/lib/services/auth/index.d.ts +63 -0
  30. package/lib/services/auth/interceptor.d.ts +22 -0
  31. package/lib/services/auth/storage.service.d.ts +48 -0
  32. package/lib/services/auth/sync.service.d.ts +49 -0
  33. package/lib/services/auth/token.service.d.ts +51 -0
  34. package/lib/services/auth/types.d.ts +315 -0
  35. package/lib/services/firebase/config.d.ts +49 -0
  36. package/lib/services/firebase/firebase.service.d.ts +140 -0
  37. package/lib/services/firebase/firestore-collection.d.ts +175 -0
  38. package/lib/services/firebase/firestore.service.d.ts +304 -0
  39. package/lib/services/firebase/index.d.ts +39 -0
  40. package/lib/services/firebase/messaging.service.d.ts +263 -0
  41. package/lib/services/firebase/shared-config.d.ts +126 -0
  42. package/lib/services/firebase/storage.service.d.ts +206 -0
  43. package/lib/services/firebase/types.d.ts +281 -0
  44. package/lib/services/firebase/utils/path-builder.d.ts +132 -0
  45. package/lib/services/firebase/utils/query-builder.d.ts +210 -0
  46. package/package.json +1 -1
  47. package/public-api.d.ts +2 -0
@@ -1,9 +1,9 @@
1
1
  import * as i0 from '@angular/core';
2
- import { EventEmitter, Component, Input, Output, Injectable, HostListener, inject, Pipe, ChangeDetectionStrategy, ViewChild, ChangeDetectorRef, ElementRef, signal, computed } from '@angular/core';
2
+ import { EventEmitter, Component, Input, Output, Injectable, HostListener, inject, Pipe, ChangeDetectionStrategy, ViewChild, ChangeDetectorRef, ElementRef, signal, computed, InjectionToken, makeEnvironmentProviders, Inject, PLATFORM_ID, Optional, APP_INITIALIZER } from '@angular/core';
3
3
  import * as i2$1 from '@ionic/angular/standalone';
4
4
  import { IonAvatar, IonCard, IonIcon, IonButton, IonSpinner, IonText, IonModal, IonHeader, IonToolbar, IonContent, IonButtons, IonTitle, IonProgressBar, IonSkeletonText, IonFab, IonFabButton, IonFabList, IonLabel, IonCardContent, IonCardHeader, IonCardTitle, IonCardSubtitle, IonCheckbox, IonTextarea, IonDatetime, IonDatetimeButton, IonInput, IonSelect, IonSelectOption, IonRadioGroup, IonRadio, IonRange, IonSearchbar, IonSegment, IonSegmentButton, IonToggle, IonAccordion, IonAccordionGroup, IonItem, IonTabBar, IonTabButton, IonBadge, IonBreadcrumb, IonBreadcrumbs, IonChip, IonPopover, IonList, IonNote, ToastController as ToastController$1, IonCol, IonRow, IonMenuButton, IonFooter, IonListHeader, IonInfiniteScroll, IonInfiniteScrollContent, IonGrid, MenuController, IonMenu, IonMenuToggle, AlertController } from '@ionic/angular/standalone';
5
5
  import * as i1 from '@angular/common';
6
- import { CommonModule, NgStyle, NgFor, NgClass } from '@angular/common';
6
+ import { CommonModule, NgStyle, NgFor, NgClass, isPlatformBrowser } from '@angular/common';
7
7
  import { addIcons } from 'ionicons';
8
8
  import { addOutline, addCircleOutline, alertOutline, alertCircleOutline, arrowBackOutline, arrowForwardOutline, arrowDownOutline, checkmarkCircleOutline, ellipsisHorizontalOutline, notificationsOutline, openOutline, closeOutline, chatbubblesOutline, shareOutline, heart, heartOutline, homeOutline, eyeOffOutline, eyeOutline, scanOutline, chevronDownOutline, chevronForwardOutline, checkmarkOutline, clipboardOutline, copyOutline, filterOutline, locationOutline, calendarOutline, businessOutline, logoTwitter, logoInstagram, logoLinkedin, logoYoutube, logoTiktok, logoFacebook, logoGoogle, createOutline, trashOutline, playOutline, refreshOutline, documentTextOutline, lockClosedOutline, informationCircleOutline, logoNpm, removeOutline, add, close, share, create, trash, star, camera, mic, send, downloadOutline, chevronDown, language, list, grid, apps, menu, settings, home, search, person, helpCircle, informationCircle, documentText, notifications, mail, calendar, folder, chevronForward, ellipsisHorizontal, chevronBack, playBack, playForward, checkmark, ellipse, starOutline, starHalf, heartHalf, checkmarkCircle, timeOutline, flag, trendingUp, trendingDown, remove, analytics, people, cash, cart, eye, chatbubbleOutline, thumbsUpOutline, thumbsUp, happyOutline, happy, sadOutline, sad, chevronUp, pin, pencil, callOutline, shuffleOutline, logoWhatsapp, paperPlaneOutline, mailOutline, trophyOutline, ticketOutline, giftOutline, personOutline, ellipsisVertical, closeCircle, chevronBackOutline, sendOutline, chatbubbleEllipsesOutline, swapVerticalOutline, chevronUpOutline, documentOutline } from 'ionicons/icons';
9
9
  import * as i1$1 from '@angular/router';
@@ -13,7 +13,7 @@ import * as i1$2 from '@angular/platform-browser';
13
13
  import QRCodeStyling from 'qr-code-styling';
14
14
  import * as i1$3 from '@angular/forms';
15
15
  import { ReactiveFormsModule, FormsModule, FormControl, Validators } from '@angular/forms';
16
- import { BehaviorSubject, filter } from 'rxjs';
16
+ import { BehaviorSubject, filter, map, distinctUntilChanged, Subject, firstValueFrom, throwError, of } from 'rxjs';
17
17
  import * as i1$4 from 'ng-otp-input';
18
18
  import { NgOtpInputComponent, NgOtpInputModule } from 'ng-otp-input';
19
19
  import * as i2 from '@ionic/angular';
@@ -27,6 +27,18 @@ import 'prismjs/components/prism-typescript';
27
27
  import 'prismjs/components/prism-bash';
28
28
  import Swiper from 'swiper';
29
29
  import { Navigation, Pagination, EffectFade, EffectCube, EffectCoverflow, EffectFlip, Autoplay } from 'swiper/modules';
30
+ import { provideFirebaseApp, initializeApp } from '@angular/fire/app';
31
+ import * as i1$5 from '@angular/fire/auth';
32
+ import { provideAuth, getAuth, connectAuthEmulator, authState, signInWithCustomToken, signOut } from '@angular/fire/auth';
33
+ import * as i1$6 from '@angular/fire/firestore';
34
+ import { provideFirestore, getFirestore, connectFirestoreEmulator, enableIndexedDbPersistence, doc, getDoc, collection, query as query$1, getDocs, limit, docData, collectionData, serverTimestamp, addDoc, setDoc, updateDoc, deleteDoc, writeBatch, arrayUnion, arrayRemove, increment, where, orderBy, startAfter, startAt, endBefore, endAt, Timestamp } from '@angular/fire/firestore';
35
+ import * as i1$8 from '@angular/fire/messaging';
36
+ import { provideMessaging, getMessaging, getToken, deleteToken, onMessage } from '@angular/fire/messaging';
37
+ import * as i1$7 from '@angular/fire/storage';
38
+ import { provideStorage, getStorage, connectStorageEmulator, ref, uploadBytesResumable, getDownloadURL, getMetadata, deleteObject, listAll } from '@angular/fire/storage';
39
+ import * as i1$9 from '@angular/common/http';
40
+ import { provideHttpClient, withInterceptors } from '@angular/common/http';
41
+ import { tap, catchError, switchMap, finalize, filter as filter$1, take } from 'rxjs/operators';
30
42
 
31
43
  /**
32
44
  * val-avatar
@@ -21128,6 +21140,4185 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
21128
21140
  }]
21129
21141
  }], ctorParameters: () => [{ type: i2$1.ModalController }] });
21130
21142
 
21143
+ /**
21144
+ * Firebase Types
21145
+ *
21146
+ * Tipos e interfaces para la integración de Firebase en valtech-components.
21147
+ * Todos los modelos de Firestore deben extender FirestoreDocument.
21148
+ */
21149
+
21150
+ /**
21151
+ * Firebase Configuration
21152
+ *
21153
+ * Configuración e inicialización de Firebase para aplicaciones Angular.
21154
+ * Usa provideValtechFirebase() en el bootstrap de tu aplicación.
21155
+ */
21156
+ /**
21157
+ * Token de inyección para la configuración de Firebase.
21158
+ * Usado internamente por los servicios de Firebase.
21159
+ */
21160
+ const VALTECH_FIREBASE_CONFIG = new InjectionToken('ValtechFirebaseConfig');
21161
+ /**
21162
+ * Provee Firebase a la aplicación Angular.
21163
+ *
21164
+ * @param config - Configuración de Firebase
21165
+ * @returns EnvironmentProviders para usar en bootstrapApplication
21166
+ *
21167
+ * @example
21168
+ * ```typescript
21169
+ * // main.ts
21170
+ * import { bootstrapApplication } from '@angular/platform-browser';
21171
+ * import { provideValtechFirebase } from 'valtech-components';
21172
+ * import { environment } from './environments/environment';
21173
+ *
21174
+ * bootstrapApplication(AppComponent, {
21175
+ * providers: [
21176
+ * provideValtechFirebase({
21177
+ * firebase: environment.firebase,
21178
+ * persistence: true,
21179
+ * emulator: environment.useEmulators ? {
21180
+ * firestore: { host: 'localhost', port: 8080 },
21181
+ * auth: { host: 'localhost', port: 9099 },
21182
+ * storage: { host: 'localhost', port: 9199 },
21183
+ * } : undefined,
21184
+ * }),
21185
+ * ],
21186
+ * });
21187
+ * ```
21188
+ */
21189
+ function provideValtechFirebase(config) {
21190
+ // Construir array de providers base
21191
+ const providers = [
21192
+ // Guardar configuración para uso en servicios
21193
+ { provide: VALTECH_FIREBASE_CONFIG, useValue: config },
21194
+ // Inicializar Firebase App
21195
+ provideFirebaseApp(() => initializeApp(config.firebase)),
21196
+ // Firestore con soporte para emuladores y persistencia
21197
+ provideFirestore(() => {
21198
+ const firestore = getFirestore();
21199
+ // Conectar a emulador si está configurado
21200
+ if (config.emulator?.firestore) {
21201
+ connectFirestoreEmulator(firestore, config.emulator.firestore.host, config.emulator.firestore.port);
21202
+ }
21203
+ // Habilitar persistencia offline si está configurada
21204
+ if (config.persistence) {
21205
+ enableIndexedDbPersistence(firestore).catch((err) => {
21206
+ if (err.code === 'failed-precondition') {
21207
+ console.warn('[ValtechFirebase] Persistencia no disponible: múltiples pestañas abiertas');
21208
+ }
21209
+ else if (err.code === 'unimplemented') {
21210
+ console.warn('[ValtechFirebase] Persistencia no soportada en este navegador');
21211
+ }
21212
+ });
21213
+ }
21214
+ return firestore;
21215
+ }),
21216
+ // Auth con soporte para emulador
21217
+ provideAuth(() => {
21218
+ const auth = getAuth();
21219
+ // Conectar a emulador si está configurado
21220
+ if (config.emulator?.auth) {
21221
+ connectAuthEmulator(auth, `http://${config.emulator.auth.host}:${config.emulator.auth.port}`, { disableWarnings: true });
21222
+ }
21223
+ return auth;
21224
+ }),
21225
+ // Storage con soporte para emulador
21226
+ provideStorage(() => {
21227
+ const storage = getStorage();
21228
+ // Conectar a emulador si está configurado
21229
+ if (config.emulator?.storage) {
21230
+ connectStorageEmulator(storage, config.emulator.storage.host, config.emulator.storage.port);
21231
+ }
21232
+ return storage;
21233
+ }),
21234
+ ];
21235
+ // Messaging (FCM) - solo si está explícitamente habilitado
21236
+ // Requiere Service Worker configurado, puede congelar la app si no está disponible
21237
+ if (config.enableMessaging) {
21238
+ providers.push(provideMessaging(() => getMessaging()));
21239
+ }
21240
+ return makeEnvironmentProviders(providers);
21241
+ }
21242
+ /**
21243
+ * Verifica si los emuladores están configurados.
21244
+ *
21245
+ * @param config - Configuración de Firebase
21246
+ * @returns true si hay al menos un emulador configurado
21247
+ */
21248
+ function hasEmulators(config) {
21249
+ return !!(config.emulator?.firestore || config.emulator?.auth || config.emulator?.storage);
21250
+ }
21251
+
21252
+ /**
21253
+ * Firebase Shared Configuration
21254
+ *
21255
+ * Configuración base de Firebase compartida entre todas las apps del monorepo.
21256
+ * Los secrets (apiKey, appId) se inyectan en build time via environment.
21257
+ */
21258
+ const APP_IDS = {
21259
+ DEMO: 'demo',
21260
+ SHOWCASE: 'showcase',
21261
+ ADMIN_PORTAL: 'admin-portal',
21262
+ APP: 'app',
21263
+ };
21264
+ // ============================================================================
21265
+ // FIREBASE PROJECT IDS
21266
+ // ============================================================================
21267
+ /**
21268
+ * IDs de los proyectos Firebase por ambiente.
21269
+ * Deben coincidir con los proyectos creados en Firebase Console.
21270
+ */
21271
+ const FIREBASE_PROJECTS = {
21272
+ development: 'myvaltech-dev',
21273
+ production: 'myvaltech-prod',
21274
+ };
21275
+ // ============================================================================
21276
+ // EMULATOR CONFIGURATION (shared across all apps)
21277
+ // ============================================================================
21278
+ /**
21279
+ * Configuración de emuladores compartida.
21280
+ * Todos los puertos deben coincidir con frontend/firebase/firebase.json
21281
+ */
21282
+ const SHARED_EMULATOR_CONFIG = {
21283
+ firestore: { host: 'localhost', port: 9080 },
21284
+ storage: { host: 'localhost', port: 9199 },
21285
+ auth: { host: 'localhost', port: 9099 },
21286
+ };
21287
+ // ============================================================================
21288
+ // STORAGE PATH BUILDERS
21289
+ // ============================================================================
21290
+ /**
21291
+ * Genera paths de Storage con namespace por app.
21292
+ *
21293
+ * @example
21294
+ * // Path específico de la app
21295
+ * storagePaths.forApp('showcase', 'uploads', 'image.jpg')
21296
+ * // => 'showcase/uploads/image.jpg'
21297
+ *
21298
+ * // Path compartido
21299
+ * storagePaths.shared.profilePhoto('user123', 'avatar.jpg')
21300
+ * // => 'profile-photos/user123/avatar.jpg'
21301
+ */
21302
+ const storagePaths = {
21303
+ /** Carpeta específica de la app: {appId}/{...paths} */
21304
+ forApp: (appId, ...paths) => [appId, ...paths].join('/'),
21305
+ /** Carpetas compartidas (sin namespace) */
21306
+ shared: {
21307
+ /** Foto de perfil de usuario */
21308
+ profilePhoto: (userId, fileName) => `profile-photos/${userId}/${fileName}`,
21309
+ /** Archivos públicos accesibles sin autenticación */
21310
+ public: (...paths) => `public/${paths.join('/')}`,
21311
+ },
21312
+ /** Carpetas de desarrollo (acceso libre en emuladores) */
21313
+ demo: (...paths) => `demo/${paths.join('/')}`,
21314
+ };
21315
+ // ============================================================================
21316
+ // FIRESTORE COLLECTION BUILDERS
21317
+ // ============================================================================
21318
+ /**
21319
+ * Genera paths de colecciones con namespace por app.
21320
+ *
21321
+ * IMPORTANTE: La estructura es /apps/{appId}/{collection}/{docId}
21322
+ * Firestore requiere número impar de segmentos para paths de colección.
21323
+ *
21324
+ * @example
21325
+ * // Colección específica de la app
21326
+ * collections.forApp('showcase', 'items')
21327
+ * // => 'apps/showcase/items'
21328
+ *
21329
+ * // Colección de desarrollo
21330
+ * collections.forApp('demo', 'items')
21331
+ * // => 'apps/demo/items'
21332
+ *
21333
+ * // Colección compartida
21334
+ * collections.shared.users
21335
+ * // => 'users'
21336
+ */
21337
+ const collections = {
21338
+ /** Colección específica de la app: apps/{appId}/{collection} */
21339
+ forApp: (appId, collectionName) => `apps/${appId}/${collectionName}`,
21340
+ /** Colecciones compartidas (sin namespace, nivel raíz) */
21341
+ shared: {
21342
+ /** Usuarios del sistema */
21343
+ users: 'users',
21344
+ /** Perfiles públicos */
21345
+ profiles: 'profiles',
21346
+ /** Notificaciones de usuarios */
21347
+ notifications: 'notifications',
21348
+ },
21349
+ };
21350
+ /**
21351
+ * Crea la configuración completa de Firebase desde variables de entorno.
21352
+ * Usa esto en el environment.ts de cada app.
21353
+ *
21354
+ * @example
21355
+ * // environment.ts
21356
+ * export const environment = {
21357
+ * firebase: createFirebaseConfig(
21358
+ * {
21359
+ * apiKey: 'AIza...',
21360
+ * authDomain: 'myvaltech-dev.firebaseapp.com',
21361
+ * projectId: 'myvaltech-dev',
21362
+ * storageBucket: 'myvaltech-dev.appspot.com',
21363
+ * messagingSenderId: '123456789',
21364
+ * appId: '1:123456789:web:abc123',
21365
+ * },
21366
+ * { useEmulators: true, persistence: true }
21367
+ * ),
21368
+ * };
21369
+ */
21370
+ function createFirebaseConfig(envConfig, options = {}) {
21371
+ const { useEmulators = false, persistence = true, enableMessaging = false, messagingVapidKey } = options;
21372
+ return {
21373
+ firebase: envConfig,
21374
+ persistence,
21375
+ enableMessaging,
21376
+ messagingVapidKey,
21377
+ emulator: useEmulators ? SHARED_EMULATOR_CONFIG : undefined,
21378
+ };
21379
+ }
21380
+ // ============================================================================
21381
+ // UTILITY: Check if running in emulator mode
21382
+ // ============================================================================
21383
+ /**
21384
+ * Verifica si la configuración tiene emuladores habilitados
21385
+ */
21386
+ function isEmulatorMode(config) {
21387
+ return config.emulator !== undefined;
21388
+ }
21389
+
21390
+ /**
21391
+ * Firebase Service
21392
+ *
21393
+ * Servicio principal para la autenticación con Firebase usando Custom Tokens.
21394
+ * Permite que usuarios autenticados con tu backend (Cognito, etc.) accedan
21395
+ * a servicios de Firebase (Firestore, Storage, FCM) de manera segura.
21396
+ */
21397
+ /**
21398
+ * Servicio de autenticación de Firebase.
21399
+ *
21400
+ * Este servicio NO maneja el login de usuarios directamente.
21401
+ * En su lugar, trabaja con Custom Tokens generados por tu backend.
21402
+ *
21403
+ * @example
21404
+ * ```typescript
21405
+ * // Después de autenticarte con tu backend (ej: Cognito)
21406
+ * @Component({...})
21407
+ * export class LoginComponent {
21408
+ * private authService = inject(AuthService); // Tu servicio de auth
21409
+ * private firebase = inject(FirebaseService); // Este servicio
21410
+ *
21411
+ * async login(email: string, password: string) {
21412
+ * // 1. Autenticar con tu backend
21413
+ * const response = await this.authService.login(email, password);
21414
+ *
21415
+ * // 2. El backend devuelve un Firebase Custom Token
21416
+ * if (response.firebaseToken) {
21417
+ * await this.firebase.signInWithCustomToken(response.firebaseToken);
21418
+ * }
21419
+ *
21420
+ * // Ahora el usuario puede acceder a Firestore, Storage, etc.
21421
+ * }
21422
+ *
21423
+ * async logout() {
21424
+ * await this.authService.logout();
21425
+ * await this.firebase.signOut();
21426
+ * }
21427
+ * }
21428
+ * ```
21429
+ */
21430
+ class FirebaseService {
21431
+ constructor(auth, config) {
21432
+ this.auth = auth;
21433
+ this.config = config;
21434
+ /** Estado interno de la sesión */
21435
+ this.sessionState = new BehaviorSubject({
21436
+ user: null,
21437
+ isAuthenticated: false,
21438
+ isLoading: true,
21439
+ error: null,
21440
+ });
21441
+ /** Estado actual de la sesión como Observable */
21442
+ this.state$ = this.sessionState.asObservable();
21443
+ // Inicializar observables que dependen de auth
21444
+ this.user$ = authState(this.auth).pipe(map((user) => (user ? this.mapUser(user) : null)), distinctUntilChanged((a, b) => a?.uid === b?.uid));
21445
+ this.isAuthenticated$ = this.user$.pipe(map((user) => !!user), distinctUntilChanged());
21446
+ // Escuchar cambios en el estado de autenticación
21447
+ authState(this.auth).subscribe({
21448
+ next: (user) => {
21449
+ this.sessionState.next({
21450
+ user: user ? this.mapUser(user) : null,
21451
+ isAuthenticated: !!user,
21452
+ isLoading: false,
21453
+ error: null,
21454
+ });
21455
+ },
21456
+ error: (error) => {
21457
+ this.sessionState.next({
21458
+ user: null,
21459
+ isAuthenticated: false,
21460
+ isLoading: false,
21461
+ error,
21462
+ });
21463
+ },
21464
+ });
21465
+ }
21466
+ // ===========================================================================
21467
+ // AUTENTICACIÓN
21468
+ // ===========================================================================
21469
+ /**
21470
+ * Autentica al usuario con un Custom Token generado por el backend.
21471
+ *
21472
+ * @param token - Firebase Custom Token generado por tu backend
21473
+ * @returns UserCredential con la información del usuario
21474
+ * @throws Error si el token es inválido o expiró
21475
+ *
21476
+ * @example
21477
+ * ```typescript
21478
+ * // Después de login exitoso con tu backend
21479
+ * const { firebaseToken } = await backendAuth.login(email, password);
21480
+ * await firebaseService.signInWithCustomToken(firebaseToken);
21481
+ * ```
21482
+ */
21483
+ async signInWithCustomToken(token) {
21484
+ try {
21485
+ const credential = await signInWithCustomToken(this.auth, token);
21486
+ return credential;
21487
+ }
21488
+ catch (error) {
21489
+ const message = this.getErrorMessage(error);
21490
+ throw new Error(message);
21491
+ }
21492
+ }
21493
+ /**
21494
+ * Cierra la sesión de Firebase.
21495
+ * Llamar junto con el logout de tu sistema de autenticación principal.
21496
+ *
21497
+ * @example
21498
+ * ```typescript
21499
+ * async logout() {
21500
+ * await this.backendAuth.logout(); // Tu auth
21501
+ * await this.firebase.signOut(); // Firebase
21502
+ * }
21503
+ * ```
21504
+ */
21505
+ async signOut() {
21506
+ try {
21507
+ await signOut(this.auth);
21508
+ }
21509
+ catch (error) {
21510
+ const message = this.getErrorMessage(error);
21511
+ throw new Error(message);
21512
+ }
21513
+ }
21514
+ // ===========================================================================
21515
+ // GETTERS SÍNCRONOS
21516
+ // ===========================================================================
21517
+ /**
21518
+ * Obtiene el usuario actual de Firebase (síncrono).
21519
+ * Retorna null si no hay usuario autenticado.
21520
+ */
21521
+ get currentUser() {
21522
+ const user = this.auth.currentUser;
21523
+ return user ? this.mapUser(user) : null;
21524
+ }
21525
+ /**
21526
+ * Obtiene el UID del usuario actual.
21527
+ * Retorna null si no hay usuario autenticado.
21528
+ */
21529
+ get uid() {
21530
+ return this.auth.currentUser?.uid ?? null;
21531
+ }
21532
+ /**
21533
+ * Indica si hay un usuario autenticado actualmente.
21534
+ */
21535
+ get isAuthenticated() {
21536
+ return !!this.auth.currentUser;
21537
+ }
21538
+ // ===========================================================================
21539
+ // TOKENS
21540
+ // ===========================================================================
21541
+ /**
21542
+ * Obtiene el ID Token de Firebase para el usuario actual.
21543
+ * Útil para validar el usuario en tu backend.
21544
+ *
21545
+ * @param forceRefresh - Si true, fuerza la renovación del token
21546
+ * @returns ID Token o null si no hay usuario
21547
+ */
21548
+ async getIdToken(forceRefresh = false) {
21549
+ const user = this.auth.currentUser;
21550
+ if (!user)
21551
+ return null;
21552
+ try {
21553
+ return await user.getIdToken(forceRefresh);
21554
+ }
21555
+ catch {
21556
+ return null;
21557
+ }
21558
+ }
21559
+ /**
21560
+ * Obtiene los claims personalizados del token del usuario.
21561
+ * Los claims son establecidos por tu backend al crear el Custom Token.
21562
+ *
21563
+ * @returns Objeto con los claims o vacío si no hay usuario
21564
+ */
21565
+ async getClaims() {
21566
+ const user = this.auth.currentUser;
21567
+ if (!user)
21568
+ return {};
21569
+ try {
21570
+ const result = await user.getIdTokenResult();
21571
+ return result.claims;
21572
+ }
21573
+ catch {
21574
+ return {};
21575
+ }
21576
+ }
21577
+ /**
21578
+ * Verifica si el usuario tiene un rol específico.
21579
+ * El rol debe estar definido en los claims del Custom Token.
21580
+ *
21581
+ * @param role - Nombre del rol a verificar
21582
+ * @returns true si el usuario tiene el rol
21583
+ */
21584
+ async hasRole(role) {
21585
+ const claims = await this.getClaims();
21586
+ return claims['role'] === role || (Array.isArray(claims['roles']) && claims['roles'].includes(role));
21587
+ }
21588
+ // ===========================================================================
21589
+ // UTILIDADES
21590
+ // ===========================================================================
21591
+ /**
21592
+ * Espera a que el estado de autenticación esté determinado.
21593
+ * Útil en guards o al inicializar la app.
21594
+ *
21595
+ * @returns Usuario actual o null
21596
+ */
21597
+ waitForAuth() {
21598
+ return new Promise((resolve) => {
21599
+ const subscription = this.state$.subscribe((state) => {
21600
+ if (!state.isLoading) {
21601
+ subscription.unsubscribe();
21602
+ resolve(state.user);
21603
+ }
21604
+ });
21605
+ });
21606
+ }
21607
+ /**
21608
+ * Obtiene la configuración actual de Firebase.
21609
+ */
21610
+ getConfig() {
21611
+ return this.config;
21612
+ }
21613
+ /**
21614
+ * Indica si los emuladores están habilitados.
21615
+ */
21616
+ isUsingEmulators() {
21617
+ return !!(this.config.emulator?.firestore || this.config.emulator?.auth || this.config.emulator?.storage);
21618
+ }
21619
+ // ===========================================================================
21620
+ // MÉTODOS PRIVADOS
21621
+ // ===========================================================================
21622
+ /**
21623
+ * Mapea un User de Firebase a nuestra interface FirebaseUser
21624
+ */
21625
+ mapUser(user) {
21626
+ return {
21627
+ uid: user.uid,
21628
+ email: user.email,
21629
+ displayName: user.displayName,
21630
+ photoURL: user.photoURL,
21631
+ emailVerified: user.emailVerified,
21632
+ isAnonymous: user.isAnonymous,
21633
+ providerId: user.providerId,
21634
+ };
21635
+ }
21636
+ /**
21637
+ * Convierte errores de Firebase a mensajes en español
21638
+ */
21639
+ getErrorMessage(error) {
21640
+ if (error instanceof Error) {
21641
+ const code = error.code;
21642
+ switch (code) {
21643
+ case 'auth/invalid-custom-token':
21644
+ return 'Token de autenticación inválido';
21645
+ case 'auth/custom-token-mismatch':
21646
+ return 'El token no corresponde a este proyecto';
21647
+ case 'auth/network-request-failed':
21648
+ return 'Error de conexión. Verifica tu conexión a internet';
21649
+ case 'auth/too-many-requests':
21650
+ return 'Demasiados intentos. Intenta de nuevo más tarde';
21651
+ case 'auth/user-disabled':
21652
+ return 'Esta cuenta ha sido deshabilitada';
21653
+ case 'auth/user-not-found':
21654
+ return 'Usuario no encontrado';
21655
+ default:
21656
+ return error.message || 'Error de autenticación desconocido';
21657
+ }
21658
+ }
21659
+ return 'Error de autenticación desconocido';
21660
+ }
21661
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FirebaseService, deps: [{ token: i1$5.Auth }, { token: VALTECH_FIREBASE_CONFIG }], target: i0.ɵɵFactoryTarget.Injectable }); }
21662
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FirebaseService, providedIn: 'root' }); }
21663
+ }
21664
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FirebaseService, decorators: [{
21665
+ type: Injectable,
21666
+ args: [{ providedIn: 'root' }]
21667
+ }], ctorParameters: () => [{ type: i1$5.Auth }, { type: undefined, decorators: [{
21668
+ type: Inject,
21669
+ args: [VALTECH_FIREBASE_CONFIG]
21670
+ }] }] });
21671
+
21672
+ /**
21673
+ * Path Builder
21674
+ *
21675
+ * Utilidades para construir rutas de Firestore con templates.
21676
+ * Soporta rutas multi-nivel y anidadas.
21677
+ */
21678
+ /**
21679
+ * Construye una ruta de Firestore reemplazando placeholders.
21680
+ *
21681
+ * @param template - Template con placeholders en formato {param}
21682
+ * @param params - Objeto con los valores a reemplazar
21683
+ * @returns Ruta construida
21684
+ * @throws Error si faltan parámetros requeridos
21685
+ *
21686
+ * @example
21687
+ * ```typescript
21688
+ * // Ruta simple
21689
+ * buildPath('users/{userId}', { userId: 'abc123' });
21690
+ * // => 'users/abc123'
21691
+ *
21692
+ * // Ruta anidada
21693
+ * buildPath('users/{userId}/documents/{docId}', {
21694
+ * userId: 'abc123',
21695
+ * docId: 'doc456'
21696
+ * });
21697
+ * // => 'users/abc123/documents/doc456'
21698
+ *
21699
+ * // Múltiples niveles
21700
+ * buildPath('orgs/{orgId}/teams/{teamId}/members/{memberId}', {
21701
+ * orgId: 'org1',
21702
+ * teamId: 'team2',
21703
+ * memberId: 'member3'
21704
+ * });
21705
+ * // => 'orgs/org1/teams/team2/members/member3'
21706
+ * ```
21707
+ */
21708
+ function buildPath(template, params) {
21709
+ let result = template;
21710
+ // Encontrar todos los placeholders
21711
+ const placeholders = template.match(/\{([^}]+)\}/g);
21712
+ if (!placeholders) {
21713
+ return template;
21714
+ }
21715
+ for (const placeholder of placeholders) {
21716
+ const key = placeholder.slice(1, -1); // Remover { y }
21717
+ const value = params[key];
21718
+ if (value === undefined || value === null) {
21719
+ throw new Error(`Parámetro requerido '${key}' no proporcionado para la ruta: ${template}`);
21720
+ }
21721
+ if (typeof value !== 'string' || value.trim() === '') {
21722
+ throw new Error(`El parámetro '${key}' debe ser un string no vacío`);
21723
+ }
21724
+ // Validar que no contenga caracteres inválidos para Firestore
21725
+ if (value.includes('/')) {
21726
+ throw new Error(`El parámetro '${key}' no puede contener '/'`);
21727
+ }
21728
+ result = result.replace(placeholder, value);
21729
+ }
21730
+ return result;
21731
+ }
21732
+ /**
21733
+ * Extrae los nombres de los parámetros de un template de ruta.
21734
+ *
21735
+ * @param template - Template de ruta
21736
+ * @returns Array con los nombres de los parámetros
21737
+ *
21738
+ * @example
21739
+ * ```typescript
21740
+ * extractParams('users/{userId}/documents/{docId}');
21741
+ * // => ['userId', 'docId']
21742
+ * ```
21743
+ */
21744
+ function extractPathParams(template) {
21745
+ const matches = template.match(/\{([^}]+)\}/g);
21746
+ if (!matches)
21747
+ return [];
21748
+ return matches.map((m) => m.slice(1, -1));
21749
+ }
21750
+ /**
21751
+ * Valida que una ruta de Firestore sea válida.
21752
+ *
21753
+ * @param path - Ruta a validar
21754
+ * @returns true si la ruta es válida
21755
+ *
21756
+ * @example
21757
+ * ```typescript
21758
+ * isValidPath('users/abc123'); // true
21759
+ * isValidPath('users/abc123/documents'); // true
21760
+ * isValidPath('users//documents'); // false (segmento vacío)
21761
+ * isValidPath(''); // false (vacío)
21762
+ * ```
21763
+ */
21764
+ function isValidPath(path) {
21765
+ if (!path || path.trim() === '')
21766
+ return false;
21767
+ const segments = path.split('/');
21768
+ // No puede tener segmentos vacíos
21769
+ if (segments.some((s) => s.trim() === ''))
21770
+ return false;
21771
+ // No puede empezar o terminar con /
21772
+ if (path.startsWith('/') || path.endsWith('/'))
21773
+ return false;
21774
+ return true;
21775
+ }
21776
+ /**
21777
+ * Obtiene la ruta de la colección padre de un documento.
21778
+ *
21779
+ * @param documentPath - Ruta completa del documento
21780
+ * @returns Ruta de la colección padre
21781
+ *
21782
+ * @example
21783
+ * ```typescript
21784
+ * getCollectionPath('users/abc123');
21785
+ * // => 'users'
21786
+ *
21787
+ * getCollectionPath('users/abc123/documents/doc456');
21788
+ * // => 'users/abc123/documents'
21789
+ * ```
21790
+ */
21791
+ function getCollectionPath(documentPath) {
21792
+ const segments = documentPath.split('/');
21793
+ if (segments.length < 2) {
21794
+ throw new Error(`Ruta de documento inválida: ${documentPath}`);
21795
+ }
21796
+ return segments.slice(0, -1).join('/');
21797
+ }
21798
+ /**
21799
+ * Obtiene el ID del documento de una ruta.
21800
+ *
21801
+ * @param documentPath - Ruta completa del documento
21802
+ * @returns ID del documento
21803
+ *
21804
+ * @example
21805
+ * ```typescript
21806
+ * getDocumentId('users/abc123');
21807
+ * // => 'abc123'
21808
+ *
21809
+ * getDocumentId('users/abc123/documents/doc456');
21810
+ * // => 'doc456'
21811
+ * ```
21812
+ */
21813
+ function getDocumentId(documentPath) {
21814
+ const segments = documentPath.split('/');
21815
+ if (segments.length < 2 || segments.length % 2 !== 0) {
21816
+ throw new Error(`Ruta de documento inválida: ${documentPath}`);
21817
+ }
21818
+ return segments[segments.length - 1];
21819
+ }
21820
+ /**
21821
+ * Verifica si una ruta apunta a un documento (número par de segmentos).
21822
+ *
21823
+ * @param path - Ruta a verificar
21824
+ * @returns true si es una ruta de documento
21825
+ *
21826
+ * @example
21827
+ * ```typescript
21828
+ * isDocumentPath('users/abc123'); // true
21829
+ * isDocumentPath('users'); // false (colección)
21830
+ * isDocumentPath('users/abc123/documents'); // false (colección)
21831
+ * ```
21832
+ */
21833
+ function isDocumentPath(path) {
21834
+ const segments = path.split('/').filter((s) => s.trim() !== '');
21835
+ return segments.length > 0 && segments.length % 2 === 0;
21836
+ }
21837
+ /**
21838
+ * Verifica si una ruta apunta a una colección (número impar de segmentos).
21839
+ *
21840
+ * @param path - Ruta a verificar
21841
+ * @returns true si es una ruta de colección
21842
+ */
21843
+ function isCollectionPath(path) {
21844
+ const segments = path.split('/').filter((s) => s.trim() !== '');
21845
+ return segments.length > 0 && segments.length % 2 !== 0;
21846
+ }
21847
+ /**
21848
+ * Combina una ruta base con segmentos adicionales.
21849
+ *
21850
+ * @param basePath - Ruta base
21851
+ * @param segments - Segmentos adicionales
21852
+ * @returns Ruta combinada
21853
+ *
21854
+ * @example
21855
+ * ```typescript
21856
+ * joinPath('users', 'abc123', 'documents');
21857
+ * // => 'users/abc123/documents'
21858
+ * ```
21859
+ */
21860
+ function joinPath(...segments) {
21861
+ return segments
21862
+ .filter((s) => s && s.trim() !== '')
21863
+ .map((s) => s.replace(/^\/+|\/+$/g, '')) // Remover / al inicio y final
21864
+ .join('/');
21865
+ }
21866
+
21867
+ /**
21868
+ * Firestore Service
21869
+ *
21870
+ * Servicio genérico para operaciones CRUD en Firestore.
21871
+ * Soporta lecturas one-time, subscripciones real-time, paginación y queries complejas.
21872
+ */
21873
+ /**
21874
+ * Servicio para operaciones CRUD en Firestore.
21875
+ *
21876
+ * @example
21877
+ * ```typescript
21878
+ * interface User extends FirestoreDocument {
21879
+ * name: string;
21880
+ * email: string;
21881
+ * role: 'admin' | 'user';
21882
+ * }
21883
+ *
21884
+ * @Component({...})
21885
+ * export class UsersComponent {
21886
+ * private firestore = inject(FirestoreService);
21887
+ *
21888
+ * // Lectura one-time
21889
+ * async loadUser(id: string) {
21890
+ * const user = await this.firestore.getDoc<User>('users', id);
21891
+ * }
21892
+ *
21893
+ * // Subscripción real-time
21894
+ * users$ = this.firestore.collectionChanges<User>('users', {
21895
+ * where: [{ field: 'role', operator: '==', value: 'admin' }],
21896
+ * orderBy: [{ field: 'name', direction: 'asc' }]
21897
+ * });
21898
+ *
21899
+ * // Crear documento
21900
+ * async createUser(data: Omit<User, 'id'>) {
21901
+ * const user = await this.firestore.addDoc<User>('users', data);
21902
+ * }
21903
+ * }
21904
+ * ```
21905
+ */
21906
+ class FirestoreService {
21907
+ constructor(firestore) {
21908
+ this.firestore = firestore;
21909
+ }
21910
+ // ===========================================================================
21911
+ // LECTURAS ONE-TIME (Promise)
21912
+ // ===========================================================================
21913
+ /**
21914
+ * Obtiene un documento por ID (lectura única).
21915
+ *
21916
+ * @param collectionPath - Ruta de la colección
21917
+ * @param docId - ID del documento
21918
+ * @returns Documento o null si no existe
21919
+ *
21920
+ * @example
21921
+ * ```typescript
21922
+ * const user = await firestoreService.getDoc<User>('users', 'abc123');
21923
+ * if (user) {
21924
+ * console.log(user.name);
21925
+ * }
21926
+ * ```
21927
+ */
21928
+ async getDoc(collectionPath, docId) {
21929
+ const docRef = doc(this.firestore, collectionPath, docId);
21930
+ const snapshot = await getDoc(docRef);
21931
+ if (!snapshot.exists()) {
21932
+ return null;
21933
+ }
21934
+ return this.mapDocument(snapshot);
21935
+ }
21936
+ /**
21937
+ * Obtiene múltiples documentos con opciones de query.
21938
+ *
21939
+ * @param collectionPath - Ruta de la colección
21940
+ * @param options - Opciones de query (where, orderBy, limit)
21941
+ * @returns Array de documentos
21942
+ *
21943
+ * @example
21944
+ * ```typescript
21945
+ * // Todos los usuarios activos ordenados por nombre
21946
+ * const users = await firestoreService.getDocs<User>('users', {
21947
+ * where: [{ field: 'active', operator: '==', value: true }],
21948
+ * orderBy: [{ field: 'name', direction: 'asc' }],
21949
+ * limit: 50
21950
+ * });
21951
+ * ```
21952
+ */
21953
+ async getDocs(collectionPath, options) {
21954
+ const collectionRef = collection(this.firestore, collectionPath);
21955
+ const constraints = this.buildQueryConstraints(options);
21956
+ const q = query$1(collectionRef, ...constraints);
21957
+ const snapshot = await getDocs(q);
21958
+ return snapshot.docs.map((doc) => this.mapDocument(doc));
21959
+ }
21960
+ /**
21961
+ * Obtiene documentos con paginación basada en cursores.
21962
+ *
21963
+ * @param collectionPath - Ruta de la colección
21964
+ * @param options - Opciones de query (debe incluir limit)
21965
+ * @returns Resultado paginado con cursor para la siguiente página
21966
+ *
21967
+ * @example
21968
+ * ```typescript
21969
+ * // Primera página
21970
+ * const page1 = await firestoreService.getPaginated<User>('users', {
21971
+ * orderBy: [{ field: 'createdAt', direction: 'desc' }],
21972
+ * limit: 10
21973
+ * });
21974
+ *
21975
+ * // Siguiente página
21976
+ * if (page1.hasMore) {
21977
+ * const page2 = await firestoreService.getPaginated<User>('users', {
21978
+ * orderBy: [{ field: 'createdAt', direction: 'desc' }],
21979
+ * limit: 10,
21980
+ * startAfter: page1.lastDoc
21981
+ * });
21982
+ * }
21983
+ * ```
21984
+ */
21985
+ async getPaginated(collectionPath, options) {
21986
+ const collectionRef = collection(this.firestore, collectionPath);
21987
+ const constraints = this.buildQueryConstraints(options);
21988
+ // Pedir uno más para saber si hay más páginas
21989
+ const q = query$1(collectionRef, ...constraints, limit(options.limit + 1));
21990
+ const snapshot = await getDocs(q);
21991
+ const docs = snapshot.docs;
21992
+ const hasMore = docs.length > options.limit;
21993
+ // Si hay más, remover el documento extra
21994
+ const resultDocs = hasMore ? docs.slice(0, -1) : docs;
21995
+ const lastDoc = resultDocs.length > 0 ? resultDocs[resultDocs.length - 1] : null;
21996
+ return {
21997
+ data: resultDocs.map((doc) => this.mapDocument(doc)),
21998
+ hasMore,
21999
+ lastDoc,
22000
+ };
22001
+ }
22002
+ /**
22003
+ * Verifica si un documento existe.
22004
+ *
22005
+ * @param collectionPath - Ruta de la colección
22006
+ * @param docId - ID del documento
22007
+ * @returns true si el documento existe
22008
+ */
22009
+ async exists(collectionPath, docId) {
22010
+ const docRef = doc(this.firestore, collectionPath, docId);
22011
+ const snapshot = await getDoc(docRef);
22012
+ return snapshot.exists();
22013
+ }
22014
+ // ===========================================================================
22015
+ // SUBSCRIPCIONES REAL-TIME (Observable)
22016
+ // ===========================================================================
22017
+ /**
22018
+ * Suscribe a cambios de un documento (real-time).
22019
+ *
22020
+ * @param collectionPath - Ruta de la colección
22021
+ * @param docId - ID del documento
22022
+ * @returns Observable que emite cuando el documento cambia
22023
+ *
22024
+ * @example
22025
+ * ```typescript
22026
+ * // En el componente
22027
+ * user$ = this.firestoreService.docChanges<User>('users', this.userId);
22028
+ *
22029
+ * // En el template
22030
+ * @if (user$ | async; as user) {
22031
+ * <p>{{ user.name }}</p>
22032
+ * }
22033
+ * ```
22034
+ */
22035
+ docChanges(collectionPath, docId) {
22036
+ const docRef = doc(this.firestore, collectionPath, docId);
22037
+ return docData(docRef, { idField: 'id' }).pipe(map((data) => {
22038
+ if (!data)
22039
+ return null;
22040
+ return this.convertTimestamps(data);
22041
+ }));
22042
+ }
22043
+ /**
22044
+ * Suscribe a cambios de una colección (real-time).
22045
+ *
22046
+ * @param collectionPath - Ruta de la colección
22047
+ * @param options - Opciones de query
22048
+ * @returns Observable que emite cuando la colección cambia
22049
+ *
22050
+ * @example
22051
+ * ```typescript
22052
+ * // Usuarios activos en tiempo real
22053
+ * activeUsers$ = this.firestoreService.collectionChanges<User>('users', {
22054
+ * where: [{ field: 'status', operator: '==', value: 'online' }]
22055
+ * });
22056
+ * ```
22057
+ */
22058
+ collectionChanges(collectionPath, options) {
22059
+ const collectionRef = collection(this.firestore, collectionPath);
22060
+ const constraints = this.buildQueryConstraints(options);
22061
+ const q = query$1(collectionRef, ...constraints);
22062
+ return collectionData(q, { idField: 'id' }).pipe(map((docs) => docs.map((doc) => this.convertTimestamps(doc))));
22063
+ }
22064
+ // ===========================================================================
22065
+ // ESCRITURA
22066
+ // ===========================================================================
22067
+ /**
22068
+ * Agrega un documento con ID auto-generado.
22069
+ *
22070
+ * @param collectionPath - Ruta de la colección
22071
+ * @param data - Datos del documento (sin id, createdAt, updatedAt)
22072
+ * @returns Documento creado con su ID
22073
+ *
22074
+ * @example
22075
+ * ```typescript
22076
+ * const newUser = await firestoreService.addDoc<User>('users', {
22077
+ * name: 'John Doe',
22078
+ * email: 'john@example.com',
22079
+ * role: 'user'
22080
+ * });
22081
+ * console.log('Created user with ID:', newUser.id);
22082
+ * ```
22083
+ */
22084
+ async addDoc(collectionPath, data) {
22085
+ const collectionRef = collection(this.firestore, collectionPath);
22086
+ const timestamp = serverTimestamp();
22087
+ const docData = {
22088
+ ...data,
22089
+ createdAt: timestamp,
22090
+ updatedAt: timestamp,
22091
+ };
22092
+ const docRef = await addDoc(collectionRef, docData);
22093
+ // Obtener el documento creado para retornarlo con timestamps resueltos
22094
+ const snapshot = await getDoc(docRef);
22095
+ return this.mapDocument(snapshot);
22096
+ }
22097
+ /**
22098
+ * Crea o sobrescribe un documento con ID específico.
22099
+ *
22100
+ * @param collectionPath - Ruta de la colección
22101
+ * @param docId - ID del documento
22102
+ * @param data - Datos del documento
22103
+ * @param options - Opciones (merge: true para merge en lugar de sobrescribir)
22104
+ *
22105
+ * @example
22106
+ * ```typescript
22107
+ * // Sobrescribir completamente
22108
+ * await firestoreService.setDoc<User>('users', 'user123', userData);
22109
+ *
22110
+ * // Merge con datos existentes
22111
+ * await firestoreService.setDoc<User>('users', 'user123', { name: 'New Name' }, { merge: true });
22112
+ * ```
22113
+ */
22114
+ async setDoc(collectionPath, docId, data, options) {
22115
+ const docRef = doc(this.firestore, collectionPath, docId);
22116
+ const timestamp = serverTimestamp();
22117
+ const docData = {
22118
+ ...data,
22119
+ updatedAt: timestamp,
22120
+ ...(options?.merge ? {} : { createdAt: timestamp }),
22121
+ };
22122
+ await setDoc(docRef, docData, { merge: options?.merge ?? false });
22123
+ }
22124
+ /**
22125
+ * Actualiza campos específicos de un documento.
22126
+ *
22127
+ * @param collectionPath - Ruta de la colección
22128
+ * @param docId - ID del documento
22129
+ * @param data - Campos a actualizar
22130
+ *
22131
+ * @example
22132
+ * ```typescript
22133
+ * await firestoreService.updateDoc<User>('users', 'user123', {
22134
+ * name: 'Updated Name',
22135
+ * lastLogin: new Date()
22136
+ * });
22137
+ * ```
22138
+ */
22139
+ async updateDoc(collectionPath, docId, data) {
22140
+ const docRef = doc(this.firestore, collectionPath, docId);
22141
+ await updateDoc(docRef, {
22142
+ ...data,
22143
+ updatedAt: serverTimestamp(),
22144
+ });
22145
+ }
22146
+ /**
22147
+ * Elimina un documento.
22148
+ *
22149
+ * @param collectionPath - Ruta de la colección
22150
+ * @param docId - ID del documento
22151
+ *
22152
+ * @example
22153
+ * ```typescript
22154
+ * await firestoreService.deleteDoc('users', 'user123');
22155
+ * ```
22156
+ */
22157
+ async deleteDoc(collectionPath, docId) {
22158
+ const docRef = doc(this.firestore, collectionPath, docId);
22159
+ await deleteDoc(docRef);
22160
+ }
22161
+ // ===========================================================================
22162
+ // OPERACIONES EN LOTE
22163
+ // ===========================================================================
22164
+ /**
22165
+ * Ejecuta múltiples operaciones de escritura de forma atómica.
22166
+ *
22167
+ * @param operations - Función que recibe el batch y agrega operaciones
22168
+ *
22169
+ * @example
22170
+ * ```typescript
22171
+ * await firestoreService.batch((batch) => {
22172
+ * batch.set('users/user1', { name: 'User 1' });
22173
+ * batch.update('users/user2', { status: 'inactive' });
22174
+ * batch.delete('users/user3');
22175
+ * });
22176
+ * ```
22177
+ */
22178
+ async batch(operations) {
22179
+ const batch = writeBatch(this.firestore);
22180
+ const batchApi = {
22181
+ set: (path, data) => {
22182
+ const [collectionPath, docId] = this.splitPath(path);
22183
+ const docRef = doc(this.firestore, collectionPath, docId);
22184
+ batch.set(docRef, {
22185
+ ...data,
22186
+ createdAt: serverTimestamp(),
22187
+ updatedAt: serverTimestamp(),
22188
+ });
22189
+ },
22190
+ update: (path, data) => {
22191
+ const [collectionPath, docId] = this.splitPath(path);
22192
+ const docRef = doc(this.firestore, collectionPath, docId);
22193
+ batch.update(docRef, {
22194
+ ...data,
22195
+ updatedAt: serverTimestamp(),
22196
+ });
22197
+ },
22198
+ delete: (path) => {
22199
+ const [collectionPath, docId] = this.splitPath(path);
22200
+ const docRef = doc(this.firestore, collectionPath, docId);
22201
+ batch.delete(docRef);
22202
+ },
22203
+ };
22204
+ operations(batchApi);
22205
+ await batch.commit();
22206
+ }
22207
+ // ===========================================================================
22208
+ // UTILIDADES
22209
+ // ===========================================================================
22210
+ /**
22211
+ * Construye una ruta a partir de un template.
22212
+ *
22213
+ * @param template - Template con placeholders {param}
22214
+ * @param params - Valores para los placeholders
22215
+ * @returns Ruta construida
22216
+ *
22217
+ * @example
22218
+ * ```typescript
22219
+ * const path = firestoreService.buildPath('users/{userId}/documents/{docId}', {
22220
+ * userId: 'user123',
22221
+ * docId: 'doc456'
22222
+ * });
22223
+ * // => 'users/user123/documents/doc456'
22224
+ * ```
22225
+ */
22226
+ buildPath(template, params) {
22227
+ return buildPath(template, params);
22228
+ }
22229
+ /**
22230
+ * Genera un ID único para un documento (sin crearlo).
22231
+ *
22232
+ * @param collectionPath - Ruta de la colección
22233
+ * @returns ID único generado por Firestore
22234
+ */
22235
+ generateId(collectionPath) {
22236
+ const collectionRef = collection(this.firestore, collectionPath);
22237
+ return doc(collectionRef).id;
22238
+ }
22239
+ /**
22240
+ * Retorna un valor de timestamp del servidor.
22241
+ * Usar en campos de fecha para que Firestore asigne el timestamp.
22242
+ */
22243
+ serverTimestamp() {
22244
+ return serverTimestamp();
22245
+ }
22246
+ /**
22247
+ * Retorna un valor para agregar elementos a un array.
22248
+ *
22249
+ * @example
22250
+ * ```typescript
22251
+ * await firestoreService.updateDoc('users', 'user123', {
22252
+ * tags: firestoreService.arrayUnion('new-tag')
22253
+ * });
22254
+ * ```
22255
+ */
22256
+ arrayUnion(...elements) {
22257
+ return arrayUnion(...elements);
22258
+ }
22259
+ /**
22260
+ * Retorna un valor para remover elementos de un array.
22261
+ */
22262
+ arrayRemove(...elements) {
22263
+ return arrayRemove(...elements);
22264
+ }
22265
+ /**
22266
+ * Retorna un valor para incrementar un campo numérico.
22267
+ *
22268
+ * @example
22269
+ * ```typescript
22270
+ * await firestoreService.updateDoc('users', 'user123', {
22271
+ * loginCount: firestoreService.increment(1)
22272
+ * });
22273
+ * ```
22274
+ */
22275
+ increment(n) {
22276
+ return increment(n);
22277
+ }
22278
+ // ===========================================================================
22279
+ // MÉTODOS PRIVADOS
22280
+ // ===========================================================================
22281
+ /**
22282
+ * Construye los QueryConstraints a partir de QueryOptions
22283
+ */
22284
+ buildQueryConstraints(options) {
22285
+ const constraints = [];
22286
+ if (!options)
22287
+ return constraints;
22288
+ // Where clauses
22289
+ if (options.where) {
22290
+ for (const clause of options.where) {
22291
+ constraints.push(where(clause.field, clause.operator, clause.value));
22292
+ }
22293
+ }
22294
+ // OrderBy clauses
22295
+ if (options.orderBy) {
22296
+ for (const clause of options.orderBy) {
22297
+ constraints.push(orderBy(clause.field, clause.direction));
22298
+ }
22299
+ }
22300
+ // Cursors para paginación
22301
+ if (options.startAfter) {
22302
+ constraints.push(startAfter(options.startAfter));
22303
+ }
22304
+ if (options.startAt) {
22305
+ constraints.push(startAt(options.startAt));
22306
+ }
22307
+ if (options.endBefore) {
22308
+ constraints.push(endBefore(options.endBefore));
22309
+ }
22310
+ if (options.endAt) {
22311
+ constraints.push(endAt(options.endAt));
22312
+ }
22313
+ // Limit (se agrega al final)
22314
+ if (options.limit) {
22315
+ constraints.push(limit(options.limit));
22316
+ }
22317
+ return constraints;
22318
+ }
22319
+ /**
22320
+ * Mapea un DocumentSnapshot a nuestro tipo
22321
+ */
22322
+ mapDocument(snapshot) {
22323
+ const data = snapshot.data();
22324
+ if (!data) {
22325
+ throw new Error('Documento no tiene datos');
22326
+ }
22327
+ return {
22328
+ id: snapshot.id,
22329
+ ...this.convertTimestamps(data),
22330
+ };
22331
+ }
22332
+ /**
22333
+ * Convierte Timestamps de Firestore a Date de JavaScript
22334
+ */
22335
+ convertTimestamps(data) {
22336
+ const result = {};
22337
+ for (const [key, value] of Object.entries(data)) {
22338
+ if (value instanceof Timestamp) {
22339
+ result[key] = value.toDate();
22340
+ }
22341
+ else if (value && typeof value === 'object' && !Array.isArray(value)) {
22342
+ result[key] = this.convertTimestamps(value);
22343
+ }
22344
+ else {
22345
+ result[key] = value;
22346
+ }
22347
+ }
22348
+ return result;
22349
+ }
22350
+ /**
22351
+ * Divide una ruta de documento en colección e ID
22352
+ */
22353
+ splitPath(path) {
22354
+ const segments = path.split('/');
22355
+ if (segments.length < 2 || segments.length % 2 !== 0) {
22356
+ throw new Error(`Ruta de documento inválida: ${path}`);
22357
+ }
22358
+ const docId = segments.pop();
22359
+ const collectionPath = segments.join('/');
22360
+ return [collectionPath, docId];
22361
+ }
22362
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FirestoreService, deps: [{ token: i1$6.Firestore }], target: i0.ɵɵFactoryTarget.Injectable }); }
22363
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FirestoreService, providedIn: 'root' }); }
22364
+ }
22365
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FirestoreService, decorators: [{
22366
+ type: Injectable,
22367
+ args: [{ providedIn: 'root' }]
22368
+ }], ctorParameters: () => [{ type: i1$6.Firestore }] });
22369
+
22370
+ /**
22371
+ * Firestore Collection Factory
22372
+ *
22373
+ * Patrón factory para crear instancias de colección tipadas.
22374
+ * Reemplaza la clase abstracta para evitar problemas con inject() en clases no-injectable.
22375
+ */
22376
+ /**
22377
+ * Factory para crear instancias de colección tipadas.
22378
+ *
22379
+ * @example
22380
+ * ```typescript
22381
+ * @Injectable({ providedIn: 'root' })
22382
+ * export class UsersService {
22383
+ * private users = inject(FirestoreCollectionFactory).create<User>('users');
22384
+ *
22385
+ * getAll = () => this.users.getAll();
22386
+ * getById = (id: string) => this.users.getById(id);
22387
+ * create = (data: Omit<User, 'id'>) => this.users.create(data);
22388
+ *
22389
+ * // Métodos personalizados
22390
+ * async getActiveUsers(): Promise<User[]> {
22391
+ * return this.users.query({
22392
+ * where: [{ field: 'active', operator: '==', value: true }]
22393
+ * });
22394
+ * }
22395
+ * }
22396
+ * ```
22397
+ */
22398
+ class FirestoreCollectionFactory {
22399
+ constructor(firestore) {
22400
+ this.firestore = firestore;
22401
+ }
22402
+ /**
22403
+ * Crea una instancia de colección tipada.
22404
+ *
22405
+ * @param collectionPath - Ruta de la colección en Firestore
22406
+ * @param options - Opciones de configuración
22407
+ * @returns Instancia de TypedCollection
22408
+ */
22409
+ create(collectionPath, options) {
22410
+ return new TypedCollection(this.firestore, collectionPath, options);
22411
+ }
22412
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FirestoreCollectionFactory, deps: [{ token: FirestoreService }], target: i0.ɵɵFactoryTarget.Injectable }); }
22413
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FirestoreCollectionFactory, providedIn: 'root' }); }
22414
+ }
22415
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FirestoreCollectionFactory, decorators: [{
22416
+ type: Injectable,
22417
+ args: [{ providedIn: 'root' }]
22418
+ }], ctorParameters: () => [{ type: FirestoreService }] });
22419
+ /**
22420
+ * Colección tipada con métodos CRUD.
22421
+ *
22422
+ * NO usa inject() - recibe FirestoreService por constructor.
22423
+ * Esto evita el error NG0203.
22424
+ */
22425
+ class TypedCollection {
22426
+ constructor(firestore, collectionPath, options = {}) {
22427
+ this.firestore = firestore;
22428
+ this.collectionPath = collectionPath;
22429
+ this.options = {
22430
+ softDelete: false,
22431
+ timestamps: true,
22432
+ ...options,
22433
+ };
22434
+ }
22435
+ // ===========================================================================
22436
+ // LECTURAS ONE-TIME
22437
+ // ===========================================================================
22438
+ /**
22439
+ * Obtiene un documento por ID.
22440
+ */
22441
+ async getById(id) {
22442
+ return this.firestore.getDoc(this.collectionPath, id);
22443
+ }
22444
+ /**
22445
+ * Obtiene todos los documentos de la colección.
22446
+ */
22447
+ async getAll(options) {
22448
+ const queryOptions = this.applyDefaultFilters(options);
22449
+ return this.firestore.getDocs(this.collectionPath, queryOptions);
22450
+ }
22451
+ /**
22452
+ * Ejecuta una query personalizada.
22453
+ */
22454
+ async query(options) {
22455
+ const queryOptions = this.applyDefaultFilters(options);
22456
+ return this.firestore.getDocs(this.collectionPath, queryOptions);
22457
+ }
22458
+ /**
22459
+ * Obtiene documentos con paginación.
22460
+ */
22461
+ async paginate(options) {
22462
+ const queryOptions = this.applyDefaultFilters(options);
22463
+ return this.firestore.getPaginated(this.collectionPath, queryOptions);
22464
+ }
22465
+ /**
22466
+ * Obtiene el primer documento que coincida con la query.
22467
+ */
22468
+ async getFirst(options) {
22469
+ const queryOptions = this.applyDefaultFilters({
22470
+ ...options,
22471
+ limit: 1,
22472
+ });
22473
+ const results = await this.firestore.getDocs(this.collectionPath, queryOptions);
22474
+ return results[0] ?? null;
22475
+ }
22476
+ /**
22477
+ * Cuenta los documentos que coinciden con la query.
22478
+ * Nota: Esto carga todos los documentos, usar con cuidado en colecciones grandes.
22479
+ */
22480
+ async count(options) {
22481
+ const queryOptions = this.applyDefaultFilters(options);
22482
+ const results = await this.firestore.getDocs(this.collectionPath, queryOptions);
22483
+ return results.length;
22484
+ }
22485
+ /**
22486
+ * Verifica si un documento existe.
22487
+ */
22488
+ async exists(id) {
22489
+ return this.firestore.exists(this.collectionPath, id);
22490
+ }
22491
+ // ===========================================================================
22492
+ // SUBSCRIPCIONES REAL-TIME
22493
+ // ===========================================================================
22494
+ /**
22495
+ * Suscribe a cambios de un documento.
22496
+ */
22497
+ watch(id) {
22498
+ return this.firestore.docChanges(this.collectionPath, id);
22499
+ }
22500
+ /**
22501
+ * Suscribe a cambios de la colección.
22502
+ */
22503
+ watchAll(options) {
22504
+ const queryOptions = this.applyDefaultFilters(options);
22505
+ return this.firestore.collectionChanges(this.collectionPath, queryOptions);
22506
+ }
22507
+ /**
22508
+ * Suscribe a una query personalizada.
22509
+ */
22510
+ watchQuery(options) {
22511
+ const queryOptions = this.applyDefaultFilters(options);
22512
+ return this.firestore.collectionChanges(this.collectionPath, queryOptions);
22513
+ }
22514
+ // ===========================================================================
22515
+ // ESCRITURA
22516
+ // ===========================================================================
22517
+ /**
22518
+ * Crea un nuevo documento con ID auto-generado.
22519
+ */
22520
+ async create(data) {
22521
+ return this.firestore.addDoc(this.collectionPath, data);
22522
+ }
22523
+ /**
22524
+ * Crea un documento con ID específico.
22525
+ */
22526
+ async createWithId(id, data) {
22527
+ return this.firestore.setDoc(this.collectionPath, id, data);
22528
+ }
22529
+ /**
22530
+ * Actualiza campos de un documento.
22531
+ */
22532
+ async update(id, data) {
22533
+ return this.firestore.updateDoc(this.collectionPath, id, data);
22534
+ }
22535
+ /**
22536
+ * Elimina un documento.
22537
+ * Si softDelete está habilitado, marca como eliminado en lugar de borrar.
22538
+ */
22539
+ async delete(id) {
22540
+ if (this.options.softDelete) {
22541
+ return this.firestore.updateDoc(this.collectionPath, id, {
22542
+ deletedAt: new Date(),
22543
+ });
22544
+ }
22545
+ return this.firestore.deleteDoc(this.collectionPath, id);
22546
+ }
22547
+ /**
22548
+ * Restaura un documento soft-deleted.
22549
+ */
22550
+ async restore(id) {
22551
+ if (!this.options.softDelete) {
22552
+ throw new Error('Soft delete no está habilitado para esta colección');
22553
+ }
22554
+ return this.firestore.updateDoc(this.collectionPath, id, {
22555
+ deletedAt: null,
22556
+ });
22557
+ }
22558
+ // ===========================================================================
22559
+ // SUB-COLECCIONES
22560
+ // ===========================================================================
22561
+ /**
22562
+ * Obtiene una referencia a una sub-colección.
22563
+ *
22564
+ * @example
22565
+ * ```typescript
22566
+ * // En UsersService
22567
+ * getUserDocuments(userId: string) {
22568
+ * return this.users.subcollection<Document>(userId, 'documents');
22569
+ * }
22570
+ *
22571
+ * // Uso
22572
+ * const docs = await users.getUserDocuments('user123').getAll();
22573
+ * ```
22574
+ */
22575
+ subcollection(parentId, subcollectionName) {
22576
+ const subPath = `${this.collectionPath}/${parentId}/${subcollectionName}`;
22577
+ return {
22578
+ getById: (id) => this.firestore.getDoc(subPath, id),
22579
+ getAll: (options) => this.firestore.getDocs(subPath, options),
22580
+ watch: (id) => this.firestore.docChanges(subPath, id),
22581
+ watchAll: (options) => this.firestore.collectionChanges(subPath, options),
22582
+ create: (data) => this.firestore.addDoc(subPath, data),
22583
+ update: (id, data) => this.firestore.updateDoc(subPath, id, data),
22584
+ delete: (id) => this.firestore.deleteDoc(subPath, id),
22585
+ };
22586
+ }
22587
+ // ===========================================================================
22588
+ // MÉTODOS PRIVADOS
22589
+ // ===========================================================================
22590
+ /**
22591
+ * Aplica filtros por defecto a las queries.
22592
+ */
22593
+ applyDefaultFilters(options) {
22594
+ if (!this.options.softDelete) {
22595
+ return options ?? {};
22596
+ }
22597
+ // Excluir documentos soft-deleted por defecto
22598
+ const whereClause = { field: 'deletedAt', operator: '==', value: null };
22599
+ return {
22600
+ ...options,
22601
+ where: [...(options?.where ?? []), whereClause],
22602
+ };
22603
+ }
22604
+ // ===========================================================================
22605
+ // UTILIDADES
22606
+ // ===========================================================================
22607
+ /**
22608
+ * Genera un nuevo ID sin crear el documento.
22609
+ */
22610
+ generateId() {
22611
+ return this.firestore.generateId(this.collectionPath);
22612
+ }
22613
+ /**
22614
+ * Obtiene la ruta de la colección.
22615
+ */
22616
+ getPath() {
22617
+ return this.collectionPath;
22618
+ }
22619
+ }
22620
+
22621
+ /**
22622
+ * Query Builder
22623
+ *
22624
+ * Builder fluido para construir queries de Firestore de manera legible.
22625
+ * Alternativa más expresiva a pasar objetos QueryOptions directamente.
22626
+ */
22627
+ /**
22628
+ * Builder fluido para queries de Firestore.
22629
+ *
22630
+ * @example
22631
+ * ```typescript
22632
+ * // Construir query con builder
22633
+ * const options = new QueryBuilder()
22634
+ * .where('status', '==', 'active')
22635
+ * .where('age', '>=', 18)
22636
+ * .orderBy('createdAt', 'desc')
22637
+ * .limit(10)
22638
+ * .build();
22639
+ *
22640
+ * // Usar con FirestoreService
22641
+ * const users = await firestoreService.getDocs<User>('users', options);
22642
+ *
22643
+ * // O con método estático
22644
+ * const options2 = QueryBuilder.create()
22645
+ * .where('category', '==', 'electronics')
22646
+ * .orderBy('price', 'asc')
22647
+ * .build();
22648
+ * ```
22649
+ */
22650
+ class QueryBuilder {
22651
+ constructor() {
22652
+ this.whereConditions = [];
22653
+ this.orderByConditions = [];
22654
+ }
22655
+ /**
22656
+ * Crea una nueva instancia del builder (método estático alternativo).
22657
+ */
22658
+ static create() {
22659
+ return new QueryBuilder();
22660
+ }
22661
+ /**
22662
+ * Agrega una condición where.
22663
+ *
22664
+ * @param field - Campo a filtrar
22665
+ * @param operator - Operador de comparación
22666
+ * @param value - Valor a comparar
22667
+ *
22668
+ * @example
22669
+ * ```typescript
22670
+ * builder.where('status', '==', 'active')
22671
+ * builder.where('price', '>=', 100)
22672
+ * builder.where('tags', 'array-contains', 'featured')
22673
+ * builder.where('category', 'in', ['electronics', 'books'])
22674
+ * ```
22675
+ */
22676
+ where(field, operator, value) {
22677
+ this.whereConditions.push({ field, operator, value });
22678
+ return this;
22679
+ }
22680
+ /**
22681
+ * Shortcut para where con operador '=='.
22682
+ *
22683
+ * @example
22684
+ * ```typescript
22685
+ * builder.whereEquals('status', 'active')
22686
+ * // equivalente a: builder.where('status', '==', 'active')
22687
+ * ```
22688
+ */
22689
+ whereEquals(field, value) {
22690
+ return this.where(field, '==', value);
22691
+ }
22692
+ /**
22693
+ * Shortcut para where con operador '!='.
22694
+ */
22695
+ whereNotEquals(field, value) {
22696
+ return this.where(field, '!=', value);
22697
+ }
22698
+ /**
22699
+ * Shortcut para where con operador '>'.
22700
+ */
22701
+ whereGreaterThan(field, value) {
22702
+ return this.where(field, '>', value);
22703
+ }
22704
+ /**
22705
+ * Shortcut para where con operador '>='.
22706
+ */
22707
+ whereGreaterOrEqual(field, value) {
22708
+ return this.where(field, '>=', value);
22709
+ }
22710
+ /**
22711
+ * Shortcut para where con operador '<'.
22712
+ */
22713
+ whereLessThan(field, value) {
22714
+ return this.where(field, '<', value);
22715
+ }
22716
+ /**
22717
+ * Shortcut para where con operador '<='.
22718
+ */
22719
+ whereLessOrEqual(field, value) {
22720
+ return this.where(field, '<=', value);
22721
+ }
22722
+ /**
22723
+ * Shortcut para where con operador 'array-contains'.
22724
+ *
22725
+ * @example
22726
+ * ```typescript
22727
+ * builder.whereArrayContains('tags', 'featured')
22728
+ * ```
22729
+ */
22730
+ whereArrayContains(field, value) {
22731
+ return this.where(field, 'array-contains', value);
22732
+ }
22733
+ /**
22734
+ * Shortcut para where con operador 'array-contains-any'.
22735
+ *
22736
+ * @example
22737
+ * ```typescript
22738
+ * builder.whereArrayContainsAny('tags', ['featured', 'new'])
22739
+ * ```
22740
+ */
22741
+ whereArrayContainsAny(field, values) {
22742
+ return this.where(field, 'array-contains-any', values);
22743
+ }
22744
+ /**
22745
+ * Shortcut para where con operador 'in'.
22746
+ *
22747
+ * @example
22748
+ * ```typescript
22749
+ * builder.whereIn('status', ['active', 'pending'])
22750
+ * ```
22751
+ */
22752
+ whereIn(field, values) {
22753
+ return this.where(field, 'in', values);
22754
+ }
22755
+ /**
22756
+ * Shortcut para where con operador 'not-in'.
22757
+ */
22758
+ whereNotIn(field, values) {
22759
+ return this.where(field, 'not-in', values);
22760
+ }
22761
+ /**
22762
+ * Agrega ordenamiento por un campo.
22763
+ *
22764
+ * @param field - Campo por el cual ordenar
22765
+ * @param direction - Dirección: 'asc' o 'desc' (default: 'asc')
22766
+ *
22767
+ * @example
22768
+ * ```typescript
22769
+ * builder.orderBy('createdAt', 'desc')
22770
+ * builder.orderBy('name') // asc por defecto
22771
+ * ```
22772
+ */
22773
+ orderBy(field, direction = 'asc') {
22774
+ this.orderByConditions.push({ field, direction });
22775
+ return this;
22776
+ }
22777
+ /**
22778
+ * Shortcut para orderBy descendente.
22779
+ */
22780
+ orderByDesc(field) {
22781
+ return this.orderBy(field, 'desc');
22782
+ }
22783
+ /**
22784
+ * Shortcut para orderBy ascendente.
22785
+ */
22786
+ orderByAsc(field) {
22787
+ return this.orderBy(field, 'asc');
22788
+ }
22789
+ /**
22790
+ * Limita el número de resultados.
22791
+ *
22792
+ * @param count - Número máximo de documentos
22793
+ *
22794
+ * @example
22795
+ * ```typescript
22796
+ * builder.limit(10)
22797
+ * ```
22798
+ */
22799
+ limit(count) {
22800
+ if (count <= 0) {
22801
+ throw new Error('El límite debe ser mayor a 0');
22802
+ }
22803
+ this.limitValue = count;
22804
+ return this;
22805
+ }
22806
+ /**
22807
+ * Cursor para paginación: empezar después de un documento.
22808
+ *
22809
+ * @param cursor - Documento o snapshot desde donde continuar
22810
+ *
22811
+ * @example
22812
+ * ```typescript
22813
+ * // Primera página
22814
+ * const page1 = await service.getPaginated('users', builder.limit(10).build());
22815
+ *
22816
+ * // Siguiente página
22817
+ * const page2 = await service.getPaginated('users',
22818
+ * builder.startAfter(page1.lastDoc).limit(10).build()
22819
+ * );
22820
+ * ```
22821
+ */
22822
+ startAfter(cursor) {
22823
+ this.startAfterValue = cursor;
22824
+ return this;
22825
+ }
22826
+ /**
22827
+ * Cursor para paginación: empezar en un documento.
22828
+ */
22829
+ startAt(cursor) {
22830
+ this.startAtValue = cursor;
22831
+ return this;
22832
+ }
22833
+ /**
22834
+ * Cursor para paginación: terminar antes de un documento.
22835
+ */
22836
+ endBefore(cursor) {
22837
+ this.endBeforeValue = cursor;
22838
+ return this;
22839
+ }
22840
+ /**
22841
+ * Cursor para paginación: terminar en un documento.
22842
+ */
22843
+ endAt(cursor) {
22844
+ this.endAtValue = cursor;
22845
+ return this;
22846
+ }
22847
+ /**
22848
+ * Construye el objeto QueryOptions.
22849
+ *
22850
+ * @returns QueryOptions para usar con FirestoreService
22851
+ */
22852
+ build() {
22853
+ const options = {};
22854
+ if (this.whereConditions.length > 0) {
22855
+ options.where = [...this.whereConditions];
22856
+ }
22857
+ if (this.orderByConditions.length > 0) {
22858
+ options.orderBy = [...this.orderByConditions];
22859
+ }
22860
+ if (this.limitValue !== undefined) {
22861
+ options.limit = this.limitValue;
22862
+ }
22863
+ if (this.startAfterValue !== undefined) {
22864
+ options.startAfter = this.startAfterValue;
22865
+ }
22866
+ if (this.startAtValue !== undefined) {
22867
+ options.startAt = this.startAtValue;
22868
+ }
22869
+ if (this.endBeforeValue !== undefined) {
22870
+ options.endBefore = this.endBeforeValue;
22871
+ }
22872
+ if (this.endAtValue !== undefined) {
22873
+ options.endAt = this.endAtValue;
22874
+ }
22875
+ return options;
22876
+ }
22877
+ /**
22878
+ * Resetea el builder para reutilización.
22879
+ */
22880
+ reset() {
22881
+ this.whereConditions = [];
22882
+ this.orderByConditions = [];
22883
+ this.limitValue = undefined;
22884
+ this.startAfterValue = undefined;
22885
+ this.startAtValue = undefined;
22886
+ this.endBeforeValue = undefined;
22887
+ this.endAtValue = undefined;
22888
+ return this;
22889
+ }
22890
+ /**
22891
+ * Clona el builder actual.
22892
+ */
22893
+ clone() {
22894
+ const cloned = new QueryBuilder();
22895
+ cloned.whereConditions = [...this.whereConditions];
22896
+ cloned.orderByConditions = [...this.orderByConditions];
22897
+ cloned.limitValue = this.limitValue;
22898
+ cloned.startAfterValue = this.startAfterValue;
22899
+ cloned.startAtValue = this.startAtValue;
22900
+ cloned.endBeforeValue = this.endBeforeValue;
22901
+ cloned.endAtValue = this.endAtValue;
22902
+ return cloned;
22903
+ }
22904
+ }
22905
+ /**
22906
+ * Función helper para crear un QueryBuilder.
22907
+ *
22908
+ * @example
22909
+ * ```typescript
22910
+ * import { query } from 'valtech-components';
22911
+ *
22912
+ * const options = query()
22913
+ * .where('status', '==', 'active')
22914
+ * .orderBy('createdAt', 'desc')
22915
+ * .limit(10)
22916
+ * .build();
22917
+ * ```
22918
+ */
22919
+ function query() {
22920
+ return new QueryBuilder();
22921
+ }
22922
+
22923
+ /**
22924
+ * Storage Service
22925
+ *
22926
+ * Servicio para operaciones de Firebase Storage.
22927
+ * Soporta upload con tracking de progreso, download y gestión de archivos.
22928
+ */
22929
+ /**
22930
+ * Servicio para Firebase Storage.
22931
+ *
22932
+ * @example
22933
+ * ```typescript
22934
+ * @Component({...})
22935
+ * export class FileUploadComponent {
22936
+ * private storage = inject(StorageService);
22937
+ *
22938
+ * uploadProgress = signal<number>(0);
22939
+ * downloadUrl = signal<string | null>(null);
22940
+ *
22941
+ * async onFileSelected(event: Event) {
22942
+ * const file = (event.target as HTMLInputElement).files?.[0];
22943
+ * if (!file) return;
22944
+ *
22945
+ * // Upload con progreso
22946
+ * this.storage.upload(`uploads/${file.name}`, file).subscribe({
22947
+ * next: (progress) => this.uploadProgress.set(progress.percentage),
22948
+ * complete: async () => {
22949
+ * const url = await this.storage.getDownloadUrl(`uploads/${file.name}`);
22950
+ * this.downloadUrl.set(url);
22951
+ * }
22952
+ * });
22953
+ * }
22954
+ * }
22955
+ * ```
22956
+ */
22957
+ class StorageService {
22958
+ constructor(storage) {
22959
+ this.storage = storage;
22960
+ }
22961
+ // ===========================================================================
22962
+ // UPLOAD
22963
+ // ===========================================================================
22964
+ /**
22965
+ * Sube un archivo con tracking de progreso.
22966
+ *
22967
+ * @param path - Ruta en Storage donde guardar el archivo
22968
+ * @param file - Archivo a subir (File o Blob)
22969
+ * @param metadata - Metadata opcional (contentType, customMetadata)
22970
+ * @returns Observable que emite el progreso y completa cuando termina
22971
+ *
22972
+ * @example
22973
+ * ```typescript
22974
+ * // Upload básico
22975
+ * storage.upload('images/photo.jpg', file).subscribe({
22976
+ * next: (progress) => console.log(`${progress.percentage}%`),
22977
+ * complete: () => console.log('Upload completado')
22978
+ * });
22979
+ *
22980
+ * // Con metadata
22981
+ * storage.upload('docs/report.pdf', file, {
22982
+ * contentType: 'application/pdf',
22983
+ * customMetadata: { uploadedBy: 'user123' }
22984
+ * }).subscribe(...);
22985
+ * ```
22986
+ */
22987
+ upload(path, file, metadata) {
22988
+ const storageRef = ref(this.storage, path);
22989
+ const uploadMetadata = {
22990
+ contentType: metadata?.contentType || (file instanceof File ? file.type : undefined),
22991
+ customMetadata: metadata?.customMetadata,
22992
+ cacheControl: metadata?.cacheControl,
22993
+ };
22994
+ const task = uploadBytesResumable(storageRef, file, uploadMetadata);
22995
+ const progress$ = new BehaviorSubject({
22996
+ bytesTransferred: 0,
22997
+ totalBytes: file.size,
22998
+ percentage: 0,
22999
+ state: 'running',
23000
+ });
23001
+ task.on('state_changed', (snapshot) => {
23002
+ progress$.next({
23003
+ bytesTransferred: snapshot.bytesTransferred,
23004
+ totalBytes: snapshot.totalBytes,
23005
+ percentage: Math.round((snapshot.bytesTransferred / snapshot.totalBytes) * 100),
23006
+ state: this.mapTaskState(snapshot.state),
23007
+ });
23008
+ }, (error) => {
23009
+ progress$.next({
23010
+ bytesTransferred: 0,
23011
+ totalBytes: file.size,
23012
+ percentage: 0,
23013
+ state: 'error',
23014
+ });
23015
+ progress$.error(this.getErrorMessage(error));
23016
+ }, () => {
23017
+ progress$.next({
23018
+ bytesTransferred: file.size,
23019
+ totalBytes: file.size,
23020
+ percentage: 100,
23021
+ state: 'success',
23022
+ });
23023
+ progress$.complete();
23024
+ });
23025
+ return progress$.asObservable();
23026
+ }
23027
+ /**
23028
+ * Sube un archivo y retorna la URL de descarga al completar.
23029
+ *
23030
+ * @param path - Ruta en Storage
23031
+ * @param file - Archivo a subir
23032
+ * @param metadata - Metadata opcional
23033
+ * @returns Resultado del upload con URL de descarga
23034
+ *
23035
+ * @example
23036
+ * ```typescript
23037
+ * const result = await storage.uploadAndGetUrl('avatars/user123.jpg', file);
23038
+ * console.log('URL:', result.downloadUrl);
23039
+ * ```
23040
+ */
23041
+ async uploadAndGetUrl(path, file, metadata) {
23042
+ return new Promise((resolve, reject) => {
23043
+ this.upload(path, file, metadata).subscribe({
23044
+ complete: async () => {
23045
+ try {
23046
+ const storageRef = ref(this.storage, path);
23047
+ const downloadUrl = await getDownloadURL(storageRef);
23048
+ const storedMetadata = await getMetadata(storageRef);
23049
+ resolve({
23050
+ downloadUrl,
23051
+ fullPath: storedMetadata.fullPath,
23052
+ name: storedMetadata.name,
23053
+ size: storedMetadata.size,
23054
+ contentType: storedMetadata.contentType || 'application/octet-stream',
23055
+ metadata: storedMetadata.customMetadata || {},
23056
+ });
23057
+ }
23058
+ catch (error) {
23059
+ reject(this.getErrorMessage(error));
23060
+ }
23061
+ },
23062
+ error: (error) => reject(error),
23063
+ });
23064
+ });
23065
+ }
23066
+ /**
23067
+ * Sube un archivo desde una Data URL (base64).
23068
+ *
23069
+ * @param path - Ruta en Storage
23070
+ * @param dataUrl - Data URL (ej: 'data:image/png;base64,...')
23071
+ * @param metadata - Metadata opcional
23072
+ * @returns Resultado del upload
23073
+ *
23074
+ * @example
23075
+ * ```typescript
23076
+ * // Desde canvas
23077
+ * const dataUrl = canvas.toDataURL('image/png');
23078
+ * const result = await storage.uploadFromDataUrl('images/drawing.png', dataUrl);
23079
+ * ```
23080
+ */
23081
+ async uploadFromDataUrl(path, dataUrl, metadata) {
23082
+ // Extraer content type y datos base64
23083
+ const matches = dataUrl.match(/^data:([^;]+);base64,(.+)$/);
23084
+ if (!matches) {
23085
+ throw new Error('Data URL inválida');
23086
+ }
23087
+ const contentType = matches[1];
23088
+ const base64Data = matches[2];
23089
+ // Convertir base64 a Blob
23090
+ const byteCharacters = atob(base64Data);
23091
+ const byteNumbers = new Array(byteCharacters.length);
23092
+ for (let i = 0; i < byteCharacters.length; i++) {
23093
+ byteNumbers[i] = byteCharacters.charCodeAt(i);
23094
+ }
23095
+ const byteArray = new Uint8Array(byteNumbers);
23096
+ const blob = new Blob([byteArray], { type: contentType });
23097
+ return this.uploadAndGetUrl(path, blob, {
23098
+ contentType,
23099
+ ...metadata,
23100
+ });
23101
+ }
23102
+ // ===========================================================================
23103
+ // DOWNLOAD
23104
+ // ===========================================================================
23105
+ /**
23106
+ * Obtiene la URL de descarga de un archivo.
23107
+ *
23108
+ * @param path - Ruta del archivo en Storage
23109
+ * @returns URL de descarga
23110
+ *
23111
+ * @example
23112
+ * ```typescript
23113
+ * const url = await storage.getDownloadUrl('images/photo.jpg');
23114
+ * // Usar en <img [src]="url">
23115
+ * ```
23116
+ */
23117
+ async getDownloadUrl(path) {
23118
+ try {
23119
+ const storageRef = ref(this.storage, path);
23120
+ return await getDownloadURL(storageRef);
23121
+ }
23122
+ catch (error) {
23123
+ throw new Error(this.getErrorMessage(error));
23124
+ }
23125
+ }
23126
+ /**
23127
+ * Obtiene la metadata de un archivo.
23128
+ *
23129
+ * @param path - Ruta del archivo
23130
+ * @returns Metadata del archivo
23131
+ */
23132
+ async getMetadata(path) {
23133
+ try {
23134
+ const storageRef = ref(this.storage, path);
23135
+ const metadata = await getMetadata(storageRef);
23136
+ return {
23137
+ contentType: metadata.contentType,
23138
+ customMetadata: metadata.customMetadata,
23139
+ cacheControl: metadata.cacheControl,
23140
+ size: metadata.size,
23141
+ name: metadata.name,
23142
+ };
23143
+ }
23144
+ catch (error) {
23145
+ throw new Error(this.getErrorMessage(error));
23146
+ }
23147
+ }
23148
+ // ===========================================================================
23149
+ // DELETE
23150
+ // ===========================================================================
23151
+ /**
23152
+ * Elimina un archivo.
23153
+ *
23154
+ * @param path - Ruta del archivo a eliminar
23155
+ *
23156
+ * @example
23157
+ * ```typescript
23158
+ * await storage.delete('images/old-photo.jpg');
23159
+ * ```
23160
+ */
23161
+ async delete(path) {
23162
+ try {
23163
+ const storageRef = ref(this.storage, path);
23164
+ await deleteObject(storageRef);
23165
+ }
23166
+ catch (error) {
23167
+ throw new Error(this.getErrorMessage(error));
23168
+ }
23169
+ }
23170
+ /**
23171
+ * Elimina múltiples archivos.
23172
+ *
23173
+ * @param paths - Array de rutas a eliminar
23174
+ *
23175
+ * @example
23176
+ * ```typescript
23177
+ * await storage.deleteMultiple([
23178
+ * 'images/photo1.jpg',
23179
+ * 'images/photo2.jpg'
23180
+ * ]);
23181
+ * ```
23182
+ */
23183
+ async deleteMultiple(paths) {
23184
+ await Promise.all(paths.map((path) => this.delete(path)));
23185
+ }
23186
+ // ===========================================================================
23187
+ // LIST
23188
+ // ===========================================================================
23189
+ /**
23190
+ * Lista archivos en un directorio.
23191
+ *
23192
+ * @param path - Ruta del directorio
23193
+ * @returns Lista de rutas de archivos
23194
+ *
23195
+ * @example
23196
+ * ```typescript
23197
+ * const result = await storage.list('images/');
23198
+ * console.log(result.items); // ['images/photo1.jpg', 'images/photo2.jpg']
23199
+ * ```
23200
+ */
23201
+ async list(path) {
23202
+ try {
23203
+ const storageRef = ref(this.storage, path);
23204
+ const result = await listAll(storageRef);
23205
+ return {
23206
+ items: result.items.map((item) => item.fullPath),
23207
+ nextPageToken: undefined, // listAll no soporta paginación
23208
+ };
23209
+ }
23210
+ catch (error) {
23211
+ throw new Error(this.getErrorMessage(error));
23212
+ }
23213
+ }
23214
+ // ===========================================================================
23215
+ // UTILIDADES
23216
+ // ===========================================================================
23217
+ /**
23218
+ * Genera un nombre de archivo único con timestamp.
23219
+ *
23220
+ * @param originalName - Nombre original del archivo
23221
+ * @param prefix - Prefijo opcional
23222
+ * @returns Nombre único
23223
+ *
23224
+ * @example
23225
+ * ```typescript
23226
+ * const uniqueName = storage.generateFileName('photo.jpg', 'user123');
23227
+ * // => 'user123_1703091234567_photo.jpg'
23228
+ * ```
23229
+ */
23230
+ generateFileName(originalName, prefix) {
23231
+ const timestamp = Date.now();
23232
+ const sanitizedName = originalName.replace(/[^a-zA-Z0-9.-]/g, '_');
23233
+ if (prefix) {
23234
+ return `${prefix}_${timestamp}_${sanitizedName}`;
23235
+ }
23236
+ return `${timestamp}_${sanitizedName}`;
23237
+ }
23238
+ /**
23239
+ * Genera una ruta única para un archivo.
23240
+ *
23241
+ * @param directory - Directorio base
23242
+ * @param originalName - Nombre original
23243
+ * @param prefix - Prefijo opcional
23244
+ * @returns Ruta completa única
23245
+ *
23246
+ * @example
23247
+ * ```typescript
23248
+ * const path = storage.generatePath('uploads', 'photo.jpg', 'user123');
23249
+ * // => 'uploads/user123_1703091234567_photo.jpg'
23250
+ * ```
23251
+ */
23252
+ generatePath(directory, originalName, prefix) {
23253
+ const fileName = this.generateFileName(originalName, prefix);
23254
+ const cleanDir = directory.replace(/\/+$/, ''); // Remover / final
23255
+ return `${cleanDir}/${fileName}`;
23256
+ }
23257
+ /**
23258
+ * Obtiene la extensión de un archivo.
23259
+ *
23260
+ * @param filename - Nombre del archivo
23261
+ * @returns Extensión (sin el punto)
23262
+ */
23263
+ getExtension(filename) {
23264
+ const parts = filename.split('.');
23265
+ return parts.length > 1 ? parts.pop().toLowerCase() : '';
23266
+ }
23267
+ /**
23268
+ * Verifica si un archivo es una imagen basándose en su extensión.
23269
+ */
23270
+ isImage(filename) {
23271
+ const ext = this.getExtension(filename);
23272
+ return ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp'].includes(ext);
23273
+ }
23274
+ /**
23275
+ * Verifica si un archivo es un documento.
23276
+ */
23277
+ isDocument(filename) {
23278
+ const ext = this.getExtension(filename);
23279
+ return ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt'].includes(ext);
23280
+ }
23281
+ // ===========================================================================
23282
+ // MÉTODOS PRIVADOS
23283
+ // ===========================================================================
23284
+ /**
23285
+ * Mapea el estado de la tarea de upload
23286
+ */
23287
+ mapTaskState(state) {
23288
+ switch (state) {
23289
+ case 'running':
23290
+ return 'running';
23291
+ case 'paused':
23292
+ return 'paused';
23293
+ case 'success':
23294
+ return 'success';
23295
+ case 'canceled':
23296
+ return 'canceled';
23297
+ case 'error':
23298
+ return 'error';
23299
+ default:
23300
+ return 'running';
23301
+ }
23302
+ }
23303
+ /**
23304
+ * Convierte errores de Storage a mensajes en español
23305
+ */
23306
+ getErrorMessage(error) {
23307
+ if (error instanceof Error) {
23308
+ const code = error.code;
23309
+ switch (code) {
23310
+ case 'storage/object-not-found':
23311
+ return 'El archivo no existe';
23312
+ case 'storage/unauthorized':
23313
+ return 'No tienes permiso para acceder a este archivo';
23314
+ case 'storage/canceled':
23315
+ return 'La operación fue cancelada';
23316
+ case 'storage/quota-exceeded':
23317
+ return 'Se ha excedido la cuota de almacenamiento';
23318
+ case 'storage/invalid-checksum':
23319
+ return 'El archivo está corrupto';
23320
+ case 'storage/retry-limit-exceeded':
23321
+ return 'Error de conexión. Intenta de nuevo';
23322
+ case 'storage/invalid-url':
23323
+ return 'URL de archivo inválida';
23324
+ case 'storage/invalid-argument':
23325
+ return 'Argumento inválido';
23326
+ default:
23327
+ return error.message || 'Error de almacenamiento desconocido';
23328
+ }
23329
+ }
23330
+ return 'Error de almacenamiento desconocido';
23331
+ }
23332
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: StorageService, deps: [{ token: i1$7.Storage }], target: i0.ɵɵFactoryTarget.Injectable }); }
23333
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: StorageService, providedIn: 'root' }); }
23334
+ }
23335
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: StorageService, decorators: [{
23336
+ type: Injectable,
23337
+ args: [{ providedIn: 'root' }]
23338
+ }], ctorParameters: () => [{ type: i1$7.Storage }] });
23339
+
23340
+ /**
23341
+ * Messaging Service (FCM)
23342
+ *
23343
+ * Servicio para Firebase Cloud Messaging (Push Notifications).
23344
+ * Permite solicitar permisos, obtener tokens, escuchar mensajes y manejar
23345
+ * navegación (deep linking) cuando el usuario toca una notificación.
23346
+ */
23347
+ /**
23348
+ * Servicio para Firebase Cloud Messaging (FCM).
23349
+ *
23350
+ * Permite recibir notificaciones push en la aplicación web.
23351
+ * Requiere VAPID key configurada en ValtechFirebaseConfig.
23352
+ *
23353
+ * @example
23354
+ * ```typescript
23355
+ * @Component({...})
23356
+ * export class NotificationComponent {
23357
+ * private messaging = inject(MessagingService);
23358
+ *
23359
+ * token = signal<string | null>(null);
23360
+ *
23361
+ * async enableNotifications() {
23362
+ * // Solicitar permiso y obtener token
23363
+ * const token = await this.messaging.requestPermission();
23364
+ *
23365
+ * if (token) {
23366
+ * this.token.set(token);
23367
+ * // Enviar token a tu backend para almacenarlo
23368
+ * await this.backend.registerDeviceToken(token);
23369
+ * }
23370
+ * }
23371
+ *
23372
+ * // Escuchar mensajes en foreground
23373
+ * messages$ = this.messaging.onMessage();
23374
+ * }
23375
+ * ```
23376
+ */
23377
+ class MessagingService {
23378
+ constructor(messaging, config, platformId, ngZone) {
23379
+ this.messaging = messaging;
23380
+ this.config = config;
23381
+ this.platformId = platformId;
23382
+ this.ngZone = ngZone;
23383
+ this.messageSubject = new Subject();
23384
+ this.notificationClickSubject = new Subject();
23385
+ this.stateSubject = new BehaviorSubject({
23386
+ token: null,
23387
+ permission: 'default',
23388
+ isSupported: false,
23389
+ });
23390
+ this.initializeMessaging();
23391
+ }
23392
+ // ===========================================================================
23393
+ // INICIALIZACIÓN
23394
+ // ===========================================================================
23395
+ /**
23396
+ * Inicializa el servicio de messaging
23397
+ */
23398
+ async initializeMessaging() {
23399
+ if (!isPlatformBrowser(this.platformId)) {
23400
+ return;
23401
+ }
23402
+ const supported = await this.checkSupport();
23403
+ const permission = this.getPermissionState();
23404
+ this.stateSubject.next({
23405
+ ...this.stateSubject.value,
23406
+ isSupported: supported,
23407
+ permission,
23408
+ });
23409
+ // Si ya tiene permiso, configurar listeners
23410
+ if (supported && permission === 'granted') {
23411
+ this.setupMessageListener();
23412
+ }
23413
+ // Escuchar mensajes del Service Worker (clicks en notificaciones background)
23414
+ this.setupServiceWorkerListener();
23415
+ }
23416
+ /**
23417
+ * Configura listener para mensajes del Service Worker.
23418
+ * Recibe eventos cuando el usuario hace click en una notificación background.
23419
+ */
23420
+ setupServiceWorkerListener() {
23421
+ if (!isPlatformBrowser(this.platformId) || !('serviceWorker' in navigator)) {
23422
+ return;
23423
+ }
23424
+ navigator.serviceWorker.addEventListener('message', (event) => {
23425
+ // Verificar que es un mensaje de notificación click
23426
+ if (event.data?.type === 'NOTIFICATION_CLICK') {
23427
+ this.ngZone.run(() => {
23428
+ const notification = event.data.notification;
23429
+ const action = this.extractActionFromData(notification.data);
23430
+ this.notificationClickSubject.next({
23431
+ notification,
23432
+ action,
23433
+ timestamp: new Date(),
23434
+ });
23435
+ });
23436
+ }
23437
+ });
23438
+ }
23439
+ /**
23440
+ * Verifica si FCM está soportado en el navegador actual
23441
+ */
23442
+ async checkSupport() {
23443
+ if (!isPlatformBrowser(this.platformId)) {
23444
+ return false;
23445
+ }
23446
+ // Verificar APIs necesarias
23447
+ if (!('Notification' in window)) {
23448
+ return false;
23449
+ }
23450
+ if (!('serviceWorker' in navigator)) {
23451
+ return false;
23452
+ }
23453
+ // Verificar que messaging esté disponible
23454
+ if (!this.messaging) {
23455
+ return false;
23456
+ }
23457
+ return true;
23458
+ }
23459
+ // ===========================================================================
23460
+ // PERMISOS Y TOKEN
23461
+ // ===========================================================================
23462
+ /**
23463
+ * Solicita permiso de notificaciones y obtiene el token FCM.
23464
+ *
23465
+ * @returns Token FCM si se otorgó permiso, null si se denegó
23466
+ *
23467
+ * @example
23468
+ * ```typescript
23469
+ * const token = await messaging.requestPermission();
23470
+ * if (token) {
23471
+ * console.log('Token FCM:', token);
23472
+ * // Enviar a backend
23473
+ * } else {
23474
+ * console.log('Permiso denegado o no soportado');
23475
+ * }
23476
+ * ```
23477
+ */
23478
+ async requestPermission() {
23479
+ if (!await this.isSupported()) {
23480
+ console.warn('FCM no está soportado en este navegador');
23481
+ return null;
23482
+ }
23483
+ try {
23484
+ // Solicitar permiso de notificaciones
23485
+ const permission = await Notification.requestPermission();
23486
+ this.stateSubject.next({
23487
+ ...this.stateSubject.value,
23488
+ permission: permission,
23489
+ });
23490
+ if (permission !== 'granted') {
23491
+ console.warn('Permiso de notificaciones denegado');
23492
+ return null;
23493
+ }
23494
+ // Obtener token FCM
23495
+ const token = await this.getToken();
23496
+ if (token) {
23497
+ // Configurar listener de mensajes
23498
+ this.setupMessageListener();
23499
+ }
23500
+ return token;
23501
+ }
23502
+ catch (error) {
23503
+ console.error('Error solicitando permiso de notificaciones:', error);
23504
+ return null;
23505
+ }
23506
+ }
23507
+ /**
23508
+ * Obtiene el token FCM actual (sin solicitar permiso).
23509
+ *
23510
+ * @returns Token FCM si está disponible, null si no
23511
+ *
23512
+ * @example
23513
+ * ```typescript
23514
+ * const token = await messaging.getToken();
23515
+ * ```
23516
+ */
23517
+ async getToken() {
23518
+ if (!this.messaging) {
23519
+ return null;
23520
+ }
23521
+ const vapidKey = this.config.messagingVapidKey;
23522
+ if (!vapidKey) {
23523
+ console.warn('VAPID key no configurada. FCM no funcionará.');
23524
+ return null;
23525
+ }
23526
+ try {
23527
+ const token = await getToken(this.messaging, { vapidKey });
23528
+ this.stateSubject.next({
23529
+ ...this.stateSubject.value,
23530
+ token,
23531
+ });
23532
+ return token;
23533
+ }
23534
+ catch (error) {
23535
+ console.error('Error obteniendo token FCM:', error);
23536
+ return null;
23537
+ }
23538
+ }
23539
+ /**
23540
+ * Elimina el token FCM actual (unsubscribe de notificaciones).
23541
+ *
23542
+ * @example
23543
+ * ```typescript
23544
+ * await messaging.deleteToken();
23545
+ * console.log('Token eliminado, no recibirá más notificaciones');
23546
+ * ```
23547
+ */
23548
+ async deleteToken() {
23549
+ if (!this.messaging) {
23550
+ return;
23551
+ }
23552
+ try {
23553
+ await deleteToken(this.messaging);
23554
+ this.stateSubject.next({
23555
+ ...this.stateSubject.value,
23556
+ token: null,
23557
+ });
23558
+ // Limpiar listener de mensajes
23559
+ if (this.unsubscribeOnMessage) {
23560
+ this.unsubscribeOnMessage();
23561
+ this.unsubscribeOnMessage = undefined;
23562
+ }
23563
+ }
23564
+ catch (error) {
23565
+ console.error('Error eliminando token FCM:', error);
23566
+ throw new Error('No se pudo eliminar el token de notificaciones');
23567
+ }
23568
+ }
23569
+ // ===========================================================================
23570
+ // MENSAJES
23571
+ // ===========================================================================
23572
+ /**
23573
+ * Observable de mensajes recibidos en foreground.
23574
+ *
23575
+ * IMPORTANTE: Los mensajes en background son manejados por el Service Worker.
23576
+ *
23577
+ * @returns Observable que emite cuando llega un mensaje en foreground
23578
+ *
23579
+ * @example
23580
+ * ```typescript
23581
+ * messaging.onMessage().subscribe(payload => {
23582
+ * console.log('Mensaje recibido:', payload);
23583
+ * // Mostrar notificación custom o actualizar UI
23584
+ * });
23585
+ * ```
23586
+ */
23587
+ onMessage() {
23588
+ return this.messageSubject.asObservable();
23589
+ }
23590
+ /**
23591
+ * Configura el listener de mensajes en foreground
23592
+ */
23593
+ setupMessageListener() {
23594
+ if (!this.messaging || this.unsubscribeOnMessage) {
23595
+ return;
23596
+ }
23597
+ this.unsubscribeOnMessage = onMessage(this.messaging, (payload) => {
23598
+ const notification = {
23599
+ title: payload.notification?.title,
23600
+ body: payload.notification?.body,
23601
+ image: payload.notification?.image,
23602
+ data: payload.data,
23603
+ messageId: payload.messageId,
23604
+ };
23605
+ this.messageSubject.next(notification);
23606
+ });
23607
+ }
23608
+ // ===========================================================================
23609
+ // ESTADO Y UTILIDADES
23610
+ // ===========================================================================
23611
+ /**
23612
+ * Obtiene el estado actual del permiso de notificaciones.
23613
+ *
23614
+ * @returns 'granted' | 'denied' | 'default'
23615
+ *
23616
+ * @example
23617
+ * ```typescript
23618
+ * const permission = messaging.getPermissionState();
23619
+ * if (permission === 'granted') {
23620
+ * // Ya tiene permiso
23621
+ * } else if (permission === 'default') {
23622
+ * // Puede solicitar permiso
23623
+ * } else {
23624
+ * // Denegado, debe habilitar manualmente
23625
+ * }
23626
+ * ```
23627
+ */
23628
+ getPermissionState() {
23629
+ if (!isPlatformBrowser(this.platformId)) {
23630
+ return 'default';
23631
+ }
23632
+ if (!('Notification' in window)) {
23633
+ return 'denied';
23634
+ }
23635
+ return Notification.permission;
23636
+ }
23637
+ /**
23638
+ * Verifica si FCM está soportado en el navegador actual.
23639
+ *
23640
+ * @returns true si FCM está soportado
23641
+ *
23642
+ * @example
23643
+ * ```typescript
23644
+ * if (await messaging.isSupported()) {
23645
+ * // Puede usar notificaciones push
23646
+ * } else {
23647
+ * // Navegador no soporta o no tiene Service Worker
23648
+ * }
23649
+ * ```
23650
+ */
23651
+ async isSupported() {
23652
+ return this.checkSupport();
23653
+ }
23654
+ /**
23655
+ * Obtiene el token actual sin hacer request.
23656
+ *
23657
+ * @returns Token almacenado o null
23658
+ */
23659
+ get currentToken() {
23660
+ return this.stateSubject.value.token;
23661
+ }
23662
+ /**
23663
+ * Observable del estado completo del servicio de messaging.
23664
+ */
23665
+ get state$() {
23666
+ return this.stateSubject.asObservable();
23667
+ }
23668
+ /**
23669
+ * Verifica si el usuario ya otorgó permiso de notificaciones.
23670
+ */
23671
+ get hasPermission() {
23672
+ return this.stateSubject.value.permission === 'granted';
23673
+ }
23674
+ // ===========================================================================
23675
+ // DEEP LINKING / NAVEGACIÓN
23676
+ // ===========================================================================
23677
+ /**
23678
+ * Observable de clicks en notificaciones.
23679
+ *
23680
+ * Emite cuando el usuario hace click en una notificación (foreground o background).
23681
+ * Usa este observable para navegar a la página correspondiente.
23682
+ *
23683
+ * @returns Observable que emite NotificationClickEvent
23684
+ *
23685
+ * @example
23686
+ * ```typescript
23687
+ * @Component({...})
23688
+ * export class AppComponent {
23689
+ * private messaging = inject(MessagingService);
23690
+ * private router = inject(Router);
23691
+ *
23692
+ * constructor() {
23693
+ * this.messaging.onNotificationClick().subscribe(event => {
23694
+ * if (event.action.route) {
23695
+ * this.router.navigate([event.action.route], {
23696
+ * queryParams: event.action.queryParams
23697
+ * });
23698
+ * }
23699
+ * });
23700
+ * }
23701
+ * }
23702
+ * ```
23703
+ */
23704
+ onNotificationClick() {
23705
+ return this.notificationClickSubject.asObservable();
23706
+ }
23707
+ /**
23708
+ * Extrae la acción de navegación de los datos de una notificación.
23709
+ *
23710
+ * Busca campos específicos en el payload de datos:
23711
+ * - `route`: Ruta interna de la app (ej: '/orders/123')
23712
+ * - `url`: URL externa (ej: 'https://example.com')
23713
+ * - `action_type`: Tipo de acción personalizada
23714
+ * - Campos con prefijo `action_`: Datos adicionales
23715
+ *
23716
+ * @param data - Datos del payload de la notificación
23717
+ * @returns Acción de navegación extraída
23718
+ *
23719
+ * @example
23720
+ * ```typescript
23721
+ * // Payload desde el backend:
23722
+ * // { route: '/orders/123', action_type: 'view_order', action_orderId: '123' }
23723
+ *
23724
+ * const action = messaging.extractActionFromData(notification.data);
23725
+ * // { route: '/orders/123', actionType: 'view_order', actionData: { orderId: '123' } }
23726
+ * ```
23727
+ */
23728
+ extractActionFromData(data) {
23729
+ if (!data) {
23730
+ return {};
23731
+ }
23732
+ const action = {};
23733
+ // Ruta interna
23734
+ if (data['route']) {
23735
+ action.route = data['route'];
23736
+ }
23737
+ // URL externa
23738
+ if (data['url']) {
23739
+ action.url = data['url'];
23740
+ }
23741
+ // Tipo de acción
23742
+ if (data['action_type']) {
23743
+ action.actionType = data['action_type'];
23744
+ }
23745
+ // Query params (puede venir como JSON string)
23746
+ if (data['query_params']) {
23747
+ try {
23748
+ action.queryParams = JSON.parse(data['query_params']);
23749
+ }
23750
+ catch {
23751
+ // Si no es JSON válido, intentar parsear como key=value
23752
+ action.queryParams = this.parseQueryString(data['query_params']);
23753
+ }
23754
+ }
23755
+ // Datos adicionales con prefijo action_
23756
+ const actionData = {};
23757
+ for (const [key, value] of Object.entries(data)) {
23758
+ if (key.startsWith('action_') && key !== 'action_type') {
23759
+ const cleanKey = key.replace('action_', '');
23760
+ // Intentar parsear JSON si es posible
23761
+ try {
23762
+ actionData[cleanKey] = JSON.parse(value);
23763
+ }
23764
+ catch {
23765
+ actionData[cleanKey] = value;
23766
+ }
23767
+ }
23768
+ }
23769
+ if (Object.keys(actionData).length > 0) {
23770
+ action.actionData = actionData;
23771
+ }
23772
+ return action;
23773
+ }
23774
+ /**
23775
+ * Emite manualmente un evento de click en notificación.
23776
+ *
23777
+ * Útil para manejar clicks en notificaciones foreground donde
23778
+ * la app decide mostrar un banner custom.
23779
+ *
23780
+ * @param notification - Payload de la notificación
23781
+ *
23782
+ * @example
23783
+ * ```typescript
23784
+ * messaging.onMessage().subscribe(notification => {
23785
+ * // Mostrar banner custom
23786
+ * this.showBanner(notification, () => {
23787
+ * // Usuario hizo click en el banner
23788
+ * messaging.handleNotificationClick(notification);
23789
+ * });
23790
+ * });
23791
+ * ```
23792
+ */
23793
+ handleNotificationClick(notification) {
23794
+ const action = this.extractActionFromData(notification.data);
23795
+ this.notificationClickSubject.next({
23796
+ notification,
23797
+ action,
23798
+ timestamp: new Date(),
23799
+ });
23800
+ }
23801
+ /**
23802
+ * Verifica si una notificación tiene acción de navegación.
23803
+ *
23804
+ * @param data - Datos del payload
23805
+ * @returns true si tiene route o url
23806
+ */
23807
+ hasNavigationAction(data) {
23808
+ if (!data)
23809
+ return false;
23810
+ return !!(data['route'] || data['url']);
23811
+ }
23812
+ /**
23813
+ * Parsea un query string en un objeto.
23814
+ */
23815
+ parseQueryString(queryString) {
23816
+ const params = {};
23817
+ if (!queryString)
23818
+ return params;
23819
+ // Remover ? inicial si existe
23820
+ const cleanQuery = queryString.startsWith('?') ? queryString.slice(1) : queryString;
23821
+ for (const pair of cleanQuery.split('&')) {
23822
+ const [key, value] = pair.split('=');
23823
+ if (key) {
23824
+ params[decodeURIComponent(key)] = decodeURIComponent(value || '');
23825
+ }
23826
+ }
23827
+ return params;
23828
+ }
23829
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MessagingService, deps: [{ token: i1$8.Messaging, optional: true }, { token: VALTECH_FIREBASE_CONFIG }, { token: PLATFORM_ID }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Injectable }); }
23830
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MessagingService, providedIn: 'root' }); }
23831
+ }
23832
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MessagingService, decorators: [{
23833
+ type: Injectable,
23834
+ args: [{ providedIn: 'root' }]
23835
+ }], ctorParameters: () => [{ type: i1$8.Messaging, decorators: [{
23836
+ type: Optional
23837
+ }] }, { type: undefined, decorators: [{
23838
+ type: Inject,
23839
+ args: [VALTECH_FIREBASE_CONFIG]
23840
+ }] }, { type: Object, decorators: [{
23841
+ type: Inject,
23842
+ args: [PLATFORM_ID]
23843
+ }] }, { type: i0.NgZone }] });
23844
+
23845
+ /**
23846
+ * Firebase Services
23847
+ *
23848
+ * Servicios reutilizables para integración con Firebase.
23849
+ *
23850
+ * @example
23851
+ * ```typescript
23852
+ * // En main.ts
23853
+ * import { provideValtechFirebase } from 'valtech-components';
23854
+ *
23855
+ * bootstrapApplication(AppComponent, {
23856
+ * providers: [
23857
+ * provideValtechFirebase({
23858
+ * firebase: environment.firebase,
23859
+ * persistence: true,
23860
+ * }),
23861
+ * ],
23862
+ * });
23863
+ *
23864
+ * // En componentes
23865
+ * import { FirebaseService, FirestoreService } from 'valtech-components';
23866
+ *
23867
+ * @Component({...})
23868
+ * export class MyComponent {
23869
+ * private firebase = inject(FirebaseService);
23870
+ * private firestore = inject(FirestoreService);
23871
+ * }
23872
+ * ```
23873
+ */
23874
+ // Tipos
23875
+
23876
+ /**
23877
+ * Tipos e interfaces para el servicio de autenticación de Valtech.
23878
+ * Alineados con el backend AuthV2.
23879
+ */
23880
+ /**
23881
+ * Estado inicial de autenticación.
23882
+ */
23883
+ const INITIAL_AUTH_STATE = {
23884
+ isAuthenticated: false,
23885
+ isLoading: true,
23886
+ accessToken: null,
23887
+ refreshToken: null,
23888
+ userId: null,
23889
+ email: null,
23890
+ roles: [],
23891
+ permissions: [],
23892
+ isSuperAdmin: false,
23893
+ expiresAt: null,
23894
+ error: null,
23895
+ };
23896
+ /**
23897
+ * Estado inicial de MFA.
23898
+ */
23899
+ const INITIAL_MFA_STATE = {
23900
+ required: false,
23901
+ mfaToken: null,
23902
+ method: null,
23903
+ };
23904
+
23905
+ /**
23906
+ * Servicio para manejo de estado de autenticación con Angular Signals.
23907
+ * Proporciona estado reactivo inmutable.
23908
+ */
23909
+ class AuthStateService {
23910
+ constructor() {
23911
+ // Estado interno (mutable solo dentro del servicio)
23912
+ this._state = signal(INITIAL_AUTH_STATE);
23913
+ this._mfaPending = signal(INITIAL_MFA_STATE);
23914
+ // =============================================
23915
+ // Signals públicos (readonly)
23916
+ // =============================================
23917
+ /** Estado completo de autenticación */
23918
+ this.state = this._state.asReadonly();
23919
+ /** Estado de MFA pendiente */
23920
+ this.mfaPending = this._mfaPending.asReadonly();
23921
+ /** Usuario está autenticado */
23922
+ this.isAuthenticated = computed(() => this._state().isAuthenticated);
23923
+ /** Estado de carga */
23924
+ this.isLoading = computed(() => this._state().isLoading);
23925
+ /** Token de acceso */
23926
+ this.accessToken = computed(() => this._state().accessToken);
23927
+ /** Roles del usuario */
23928
+ this.roles = computed(() => this._state().roles);
23929
+ /** Permisos del usuario */
23930
+ this.permissions = computed(() => this._state().permissions);
23931
+ /** Usuario es super admin */
23932
+ this.isSuperAdmin = computed(() => this._state().isSuperAdmin);
23933
+ /** Error actual */
23934
+ this.error = computed(() => this._state().error);
23935
+ /** Información del usuario */
23936
+ this.user = computed(() => {
23937
+ const state = this._state();
23938
+ if (!state.isAuthenticated || !state.userId) {
23939
+ return null;
23940
+ }
23941
+ return {
23942
+ userId: state.userId,
23943
+ email: state.email || '',
23944
+ roles: state.roles,
23945
+ permissions: state.permissions,
23946
+ isSuperAdmin: state.isSuperAdmin,
23947
+ };
23948
+ });
23949
+ }
23950
+ // =============================================
23951
+ // Métodos de actualización
23952
+ // =============================================
23953
+ /**
23954
+ * Establece el estado de carga.
23955
+ */
23956
+ setLoading(isLoading) {
23957
+ this._state.update((s) => ({ ...s, isLoading }));
23958
+ }
23959
+ /**
23960
+ * Establece el estado de autenticación exitosa.
23961
+ */
23962
+ setAuthenticated(data) {
23963
+ this._state.set({
23964
+ isAuthenticated: true,
23965
+ isLoading: false,
23966
+ accessToken: data.accessToken,
23967
+ refreshToken: data.refreshToken,
23968
+ userId: data.userId || null,
23969
+ email: data.email || null,
23970
+ roles: data.roles,
23971
+ permissions: data.permissions,
23972
+ isSuperAdmin: data.isSuperAdmin,
23973
+ expiresAt: data.expiresAt,
23974
+ error: null,
23975
+ });
23976
+ }
23977
+ /**
23978
+ * Actualiza solo el access token (después de refresh).
23979
+ */
23980
+ updateAccessToken(accessToken, expiresIn) {
23981
+ const expiresAt = Date.now() + expiresIn * 1000;
23982
+ this._state.update((s) => ({
23983
+ ...s,
23984
+ accessToken,
23985
+ expiresAt,
23986
+ }));
23987
+ }
23988
+ /**
23989
+ * Actualiza los permisos.
23990
+ */
23991
+ updatePermissions(roles, permissions, isSuperAdmin) {
23992
+ this._state.update((s) => ({
23993
+ ...s,
23994
+ roles,
23995
+ permissions,
23996
+ isSuperAdmin,
23997
+ }));
23998
+ }
23999
+ /**
24000
+ * Establece un error de autenticación.
24001
+ */
24002
+ setError(error) {
24003
+ this._state.update((s) => ({
24004
+ ...s,
24005
+ error,
24006
+ isLoading: false,
24007
+ }));
24008
+ }
24009
+ /**
24010
+ * Limpia el error.
24011
+ */
24012
+ clearError() {
24013
+ this._state.update((s) => ({
24014
+ ...s,
24015
+ error: null,
24016
+ }));
24017
+ }
24018
+ /**
24019
+ * Establece estado de MFA pendiente.
24020
+ */
24021
+ setMFAPending(mfaState) {
24022
+ this._mfaPending.set(mfaState);
24023
+ }
24024
+ /**
24025
+ * Limpia el estado de MFA pendiente.
24026
+ */
24027
+ clearMFAPending() {
24028
+ this._mfaPending.set(INITIAL_MFA_STATE);
24029
+ }
24030
+ /**
24031
+ * Resetea todo el estado a valores iniciales.
24032
+ */
24033
+ reset() {
24034
+ this._state.set(INITIAL_AUTH_STATE);
24035
+ this._mfaPending.set(INITIAL_MFA_STATE);
24036
+ }
24037
+ /**
24038
+ * Restaura estado desde datos almacenados.
24039
+ */
24040
+ restoreFromStorage(stored) {
24041
+ if (stored.accessToken) {
24042
+ this._state.set({
24043
+ isAuthenticated: true,
24044
+ isLoading: false,
24045
+ accessToken: stored.accessToken,
24046
+ refreshToken: stored.refreshToken || null,
24047
+ userId: null, // Se extraerá del token
24048
+ email: null, // Se extraerá del token
24049
+ roles: stored.roles || [],
24050
+ permissions: stored.permissions || [],
24051
+ isSuperAdmin: stored.isSuperAdmin || false,
24052
+ expiresAt: stored.expiresAt || null,
24053
+ error: null,
24054
+ });
24055
+ }
24056
+ }
24057
+ /**
24058
+ * Actualiza el userId y email (después de parsear el token).
24059
+ */
24060
+ updateUserInfo(userId, email) {
24061
+ this._state.update((s) => ({
24062
+ ...s,
24063
+ userId,
24064
+ email,
24065
+ }));
24066
+ }
24067
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: AuthStateService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
24068
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: AuthStateService, providedIn: 'root' }); }
24069
+ }
24070
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: AuthStateService, decorators: [{
24071
+ type: Injectable,
24072
+ args: [{ providedIn: 'root' }]
24073
+ }] });
24074
+
24075
+ /**
24076
+ * Servicio para manejo de tokens JWT.
24077
+ * Parseo y validación de tokens sin dependencias externas.
24078
+ */
24079
+ class TokenService {
24080
+ /**
24081
+ * Parsea un token JWT y extrae los claims.
24082
+ * @param token - Token JWT
24083
+ * @returns Claims del token o null si es inválido
24084
+ */
24085
+ parseToken(token) {
24086
+ try {
24087
+ const parts = token.split('.');
24088
+ if (parts.length !== 3) {
24089
+ return null;
24090
+ }
24091
+ const payload = parts[1];
24092
+ const decoded = this.base64UrlDecode(payload);
24093
+ return JSON.parse(decoded);
24094
+ }
24095
+ catch {
24096
+ console.warn('[ValtechAuth] Error al parsear token JWT');
24097
+ return null;
24098
+ }
24099
+ }
24100
+ /**
24101
+ * Verifica si un token es válido (no expirado).
24102
+ * @param token - Token JWT
24103
+ * @returns true si el token es válido
24104
+ */
24105
+ isTokenValid(token) {
24106
+ const claims = this.parseToken(token);
24107
+ if (!claims) {
24108
+ return false;
24109
+ }
24110
+ // exp está en segundos, Date.now() en milisegundos
24111
+ const expirationMs = claims.exp * 1000;
24112
+ return Date.now() < expirationMs;
24113
+ }
24114
+ /**
24115
+ * Obtiene el tiempo restante del token en segundos.
24116
+ * @param token - Token JWT
24117
+ * @returns Segundos restantes o 0 si expirado
24118
+ */
24119
+ getTimeToExpiry(token) {
24120
+ const claims = this.parseToken(token);
24121
+ if (!claims) {
24122
+ return 0;
24123
+ }
24124
+ const expirationMs = claims.exp * 1000;
24125
+ const remaining = expirationMs - Date.now();
24126
+ return remaining > 0 ? Math.floor(remaining / 1000) : 0;
24127
+ }
24128
+ /**
24129
+ * Obtiene el timestamp de expiración del token.
24130
+ * @param token - Token JWT
24131
+ * @returns Timestamp en milisegundos o null
24132
+ */
24133
+ getExpirationTime(token) {
24134
+ const claims = this.parseToken(token);
24135
+ if (!claims) {
24136
+ return null;
24137
+ }
24138
+ return claims.exp * 1000;
24139
+ }
24140
+ /**
24141
+ * Extrae el user ID del token.
24142
+ * @param token - Token JWT
24143
+ * @returns User ID o null
24144
+ */
24145
+ getUserId(token) {
24146
+ const claims = this.parseToken(token);
24147
+ return claims?.uid || null;
24148
+ }
24149
+ /**
24150
+ * Extrae el email del token.
24151
+ * @param token - Token JWT
24152
+ * @returns Email o null
24153
+ */
24154
+ getEmail(token) {
24155
+ const claims = this.parseToken(token);
24156
+ return claims?.email || null;
24157
+ }
24158
+ /**
24159
+ * Decodifica base64url a string.
24160
+ * Base64url usa - y _ en lugar de + y /
24161
+ */
24162
+ base64UrlDecode(str) {
24163
+ // Reemplazar caracteres base64url por base64 estándar
24164
+ let base64 = str.replace(/-/g, '+').replace(/_/g, '/');
24165
+ // Agregar padding si es necesario
24166
+ const padding = base64.length % 4;
24167
+ if (padding) {
24168
+ base64 += '='.repeat(4 - padding);
24169
+ }
24170
+ // Decodificar
24171
+ const decoded = atob(base64);
24172
+ // Manejar caracteres UTF-8
24173
+ return decodeURIComponent(decoded
24174
+ .split('')
24175
+ .map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
24176
+ .join(''));
24177
+ }
24178
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TokenService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
24179
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TokenService, providedIn: 'root' }); }
24180
+ }
24181
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TokenService, decorators: [{
24182
+ type: Injectable,
24183
+ args: [{ providedIn: 'root' }]
24184
+ }] });
24185
+
24186
+ /**
24187
+ * Servicio para persistencia de estado de autenticación en localStorage.
24188
+ */
24189
+ class AuthStorageService {
24190
+ constructor(config) {
24191
+ this.config = config;
24192
+ const prefix = this.config.storagePrefix || 'valtech_auth_';
24193
+ this.keys = {
24194
+ ACCESS_TOKEN: `${prefix}access_token`,
24195
+ REFRESH_TOKEN: `${prefix}refresh_token`,
24196
+ ROLES: `${prefix}roles`,
24197
+ PERMISSIONS: `${prefix}permissions`,
24198
+ IS_SUPER_ADMIN: `${prefix}is_super_admin`,
24199
+ EXPIRES_AT: `${prefix}expires_at`,
24200
+ };
24201
+ }
24202
+ /**
24203
+ * Guarda el estado completo de autenticación.
24204
+ */
24205
+ saveState(state) {
24206
+ try {
24207
+ localStorage.setItem(this.keys.ACCESS_TOKEN, state.accessToken);
24208
+ localStorage.setItem(this.keys.REFRESH_TOKEN, state.refreshToken);
24209
+ localStorage.setItem(this.keys.ROLES, JSON.stringify(state.roles));
24210
+ localStorage.setItem(this.keys.PERMISSIONS, JSON.stringify(state.permissions));
24211
+ localStorage.setItem(this.keys.IS_SUPER_ADMIN, String(state.isSuperAdmin));
24212
+ if (state.expiresAt) {
24213
+ localStorage.setItem(this.keys.EXPIRES_AT, String(state.expiresAt));
24214
+ }
24215
+ }
24216
+ catch (e) {
24217
+ console.warn('[ValtechAuth] Error guardando estado en storage:', e);
24218
+ }
24219
+ }
24220
+ /**
24221
+ * Carga el estado de autenticación desde storage.
24222
+ */
24223
+ loadState() {
24224
+ try {
24225
+ const accessToken = localStorage.getItem(this.keys.ACCESS_TOKEN);
24226
+ const refreshToken = localStorage.getItem(this.keys.REFRESH_TOKEN);
24227
+ const rolesJson = localStorage.getItem(this.keys.ROLES);
24228
+ const permissionsJson = localStorage.getItem(this.keys.PERMISSIONS);
24229
+ const isSuperAdmin = localStorage.getItem(this.keys.IS_SUPER_ADMIN) === 'true';
24230
+ const expiresAtStr = localStorage.getItem(this.keys.EXPIRES_AT);
24231
+ return {
24232
+ accessToken: accessToken || undefined,
24233
+ refreshToken: refreshToken || undefined,
24234
+ roles: rolesJson ? JSON.parse(rolesJson) : [],
24235
+ permissions: permissionsJson ? JSON.parse(permissionsJson) : [],
24236
+ isSuperAdmin,
24237
+ expiresAt: expiresAtStr ? Number(expiresAtStr) : undefined,
24238
+ };
24239
+ }
24240
+ catch (e) {
24241
+ console.warn('[ValtechAuth] Error cargando estado desde storage:', e);
24242
+ return {};
24243
+ }
24244
+ }
24245
+ /**
24246
+ * Guarda solo el access token.
24247
+ */
24248
+ saveAccessToken(token, expiresAt) {
24249
+ try {
24250
+ localStorage.setItem(this.keys.ACCESS_TOKEN, token);
24251
+ if (expiresAt) {
24252
+ localStorage.setItem(this.keys.EXPIRES_AT, String(expiresAt));
24253
+ }
24254
+ }
24255
+ catch (e) {
24256
+ console.warn('[ValtechAuth] Error guardando access token:', e);
24257
+ }
24258
+ }
24259
+ /**
24260
+ * Guarda los permisos actualizados.
24261
+ */
24262
+ savePermissions(response) {
24263
+ try {
24264
+ localStorage.setItem(this.keys.ROLES, JSON.stringify(response.roles));
24265
+ localStorage.setItem(this.keys.PERMISSIONS, JSON.stringify(response.permissions));
24266
+ localStorage.setItem(this.keys.IS_SUPER_ADMIN, String(response.isSuperAdmin));
24267
+ }
24268
+ catch (e) {
24269
+ console.warn('[ValtechAuth] Error guardando permisos:', e);
24270
+ }
24271
+ }
24272
+ /**
24273
+ * Carga los permisos desde storage.
24274
+ */
24275
+ loadPermissions() {
24276
+ try {
24277
+ const rolesJson = localStorage.getItem(this.keys.ROLES);
24278
+ const permissionsJson = localStorage.getItem(this.keys.PERMISSIONS);
24279
+ const isSuperAdmin = localStorage.getItem(this.keys.IS_SUPER_ADMIN) === 'true';
24280
+ return {
24281
+ roles: rolesJson ? JSON.parse(rolesJson) : [],
24282
+ permissions: permissionsJson ? JSON.parse(permissionsJson) : [],
24283
+ isSuperAdmin,
24284
+ };
24285
+ }
24286
+ catch {
24287
+ return { roles: [], permissions: [], isSuperAdmin: false };
24288
+ }
24289
+ }
24290
+ /**
24291
+ * Obtiene el refresh token.
24292
+ */
24293
+ getRefreshToken() {
24294
+ return localStorage.getItem(this.keys.REFRESH_TOKEN);
24295
+ }
24296
+ /**
24297
+ * Limpia todo el estado de autenticación.
24298
+ */
24299
+ clear() {
24300
+ try {
24301
+ Object.values(this.keys).forEach((key) => localStorage.removeItem(key));
24302
+ }
24303
+ catch (e) {
24304
+ console.warn('[ValtechAuth] Error limpiando storage:', e);
24305
+ }
24306
+ }
24307
+ /**
24308
+ * Verifica si hay estado guardado.
24309
+ */
24310
+ hasStoredState() {
24311
+ return !!localStorage.getItem(this.keys.ACCESS_TOKEN);
24312
+ }
24313
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: AuthStorageService, deps: [{ token: VALTECH_AUTH_CONFIG }], target: i0.ɵɵFactoryTarget.Injectable }); }
24314
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: AuthStorageService, providedIn: 'root' }); }
24315
+ }
24316
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: AuthStorageService, decorators: [{
24317
+ type: Injectable,
24318
+ args: [{ providedIn: 'root' }]
24319
+ }], ctorParameters: () => [{ type: undefined, decorators: [{
24320
+ type: Inject,
24321
+ args: [VALTECH_AUTH_CONFIG]
24322
+ }] }] });
24323
+
24324
+ /**
24325
+ * Servicio para sincronización de estado de autenticación entre pestañas.
24326
+ * Usa BroadcastChannel API con fallback a storage events.
24327
+ */
24328
+ class AuthSyncService {
24329
+ constructor(config) {
24330
+ this.config = config;
24331
+ this.channel = null;
24332
+ this.eventSubject = new Subject();
24333
+ this.storageListener = null;
24334
+ /** Observable de eventos de sincronización */
24335
+ this.onEvent$ = this.eventSubject.asObservable();
24336
+ const prefix = this.config.storagePrefix || 'valtech_auth_';
24337
+ this.channelName = `${prefix}sync_channel`;
24338
+ }
24339
+ /**
24340
+ * Inicia la sincronización entre pestañas.
24341
+ */
24342
+ start() {
24343
+ if (!this.config.enableTabSync) {
24344
+ return;
24345
+ }
24346
+ // Intentar usar BroadcastChannel API (mejor rendimiento)
24347
+ if (typeof BroadcastChannel !== 'undefined') {
24348
+ this.initBroadcastChannel();
24349
+ }
24350
+ else {
24351
+ // Fallback a storage events
24352
+ this.initStorageEvents();
24353
+ }
24354
+ }
24355
+ /**
24356
+ * Detiene la sincronización.
24357
+ */
24358
+ stop() {
24359
+ if (this.channel) {
24360
+ this.channel.close();
24361
+ this.channel = null;
24362
+ }
24363
+ if (this.storageListener) {
24364
+ window.removeEventListener('storage', this.storageListener);
24365
+ this.storageListener = null;
24366
+ }
24367
+ }
24368
+ /**
24369
+ * Envía un evento a otras pestañas.
24370
+ */
24371
+ broadcast(event) {
24372
+ if (!this.config.enableTabSync) {
24373
+ return;
24374
+ }
24375
+ const fullEvent = {
24376
+ ...event,
24377
+ timestamp: Date.now(),
24378
+ };
24379
+ if (this.channel) {
24380
+ this.channel.postMessage(fullEvent);
24381
+ }
24382
+ else {
24383
+ // Fallback: usar localStorage para notificar
24384
+ this.broadcastViaStorage(fullEvent);
24385
+ }
24386
+ }
24387
+ ngOnDestroy() {
24388
+ this.stop();
24389
+ this.eventSubject.complete();
24390
+ }
24391
+ /**
24392
+ * Inicializa BroadcastChannel API.
24393
+ */
24394
+ initBroadcastChannel() {
24395
+ try {
24396
+ this.channel = new BroadcastChannel(this.channelName);
24397
+ this.channel.onmessage = (event) => {
24398
+ this.handleEvent(event.data);
24399
+ };
24400
+ this.channel.onmessageerror = () => {
24401
+ console.warn('[ValtechAuth] Error en BroadcastChannel, usando fallback');
24402
+ this.channel?.close();
24403
+ this.channel = null;
24404
+ this.initStorageEvents();
24405
+ };
24406
+ }
24407
+ catch {
24408
+ // BroadcastChannel no disponible, usar fallback
24409
+ this.initStorageEvents();
24410
+ }
24411
+ }
24412
+ /**
24413
+ * Inicializa fallback con storage events.
24414
+ */
24415
+ initStorageEvents() {
24416
+ const storageKey = `${this.config.storagePrefix}sync_event`;
24417
+ this.storageListener = (event) => {
24418
+ if (event.key === storageKey && event.newValue) {
24419
+ try {
24420
+ const syncEvent = JSON.parse(event.newValue);
24421
+ this.handleEvent(syncEvent);
24422
+ }
24423
+ catch {
24424
+ // Ignorar eventos mal formados
24425
+ }
24426
+ }
24427
+ };
24428
+ window.addEventListener('storage', this.storageListener);
24429
+ }
24430
+ /**
24431
+ * Envía evento via localStorage (fallback).
24432
+ */
24433
+ broadcastViaStorage(event) {
24434
+ const storageKey = `${this.config.storagePrefix}sync_event`;
24435
+ try {
24436
+ // Escribir y luego limpiar para permitir múltiples eventos del mismo tipo
24437
+ localStorage.setItem(storageKey, JSON.stringify(event));
24438
+ // Usar setTimeout para permitir que otras pestañas lean el valor
24439
+ setTimeout(() => {
24440
+ localStorage.removeItem(storageKey);
24441
+ }, 100);
24442
+ }
24443
+ catch {
24444
+ console.warn('[ValtechAuth] Error enviando evento via storage');
24445
+ }
24446
+ }
24447
+ /**
24448
+ * Maneja un evento recibido.
24449
+ */
24450
+ handleEvent(event) {
24451
+ // Verificar que el evento no sea muy antiguo (más de 5 segundos)
24452
+ const age = Date.now() - event.timestamp;
24453
+ if (age > 5000) {
24454
+ return;
24455
+ }
24456
+ this.eventSubject.next(event);
24457
+ }
24458
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: AuthSyncService, deps: [{ token: VALTECH_AUTH_CONFIG }], target: i0.ɵɵFactoryTarget.Injectable }); }
24459
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: AuthSyncService, providedIn: 'root' }); }
24460
+ }
24461
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: AuthSyncService, decorators: [{
24462
+ type: Injectable,
24463
+ args: [{ providedIn: 'root' }]
24464
+ }], ctorParameters: () => [{ type: undefined, decorators: [{
24465
+ type: Inject,
24466
+ args: [VALTECH_AUTH_CONFIG]
24467
+ }] }] });
24468
+
24469
+ /**
24470
+ * Servicio principal de autenticación.
24471
+ *
24472
+ * @example
24473
+ * ```typescript
24474
+ * import { AuthService } from 'valtech-components';
24475
+ *
24476
+ * @Component({...})
24477
+ * export class LoginComponent {
24478
+ * private auth = inject(AuthService);
24479
+ *
24480
+ * async login() {
24481
+ * await firstValueFrom(this.auth.signin({ email, password }));
24482
+ * if (this.auth.mfaPending().required) {
24483
+ * // Mostrar UI de MFA
24484
+ * } else {
24485
+ * this.router.navigate(['/']);
24486
+ * }
24487
+ * }
24488
+ * }
24489
+ * ```
24490
+ */
24491
+ class AuthService {
24492
+ constructor(config, http, router, stateService, tokenService, storageService, syncService, firebaseService) {
24493
+ this.config = config;
24494
+ this.http = http;
24495
+ this.router = router;
24496
+ this.stateService = stateService;
24497
+ this.tokenService = tokenService;
24498
+ this.storageService = storageService;
24499
+ this.syncService = syncService;
24500
+ this.firebaseService = firebaseService;
24501
+ // Timer para refresh proactivo
24502
+ this.refreshTimerId = null;
24503
+ this.syncSubscription = null;
24504
+ // =============================================
24505
+ // ESTADO PÚBLICO (Signals readonly)
24506
+ // =============================================
24507
+ /** Estado completo de autenticación */
24508
+ this.state = this.stateService.state;
24509
+ /** Usuario está autenticado */
24510
+ this.isAuthenticated = this.stateService.isAuthenticated;
24511
+ /** Estado de carga */
24512
+ this.isLoading = this.stateService.isLoading;
24513
+ /** Información del usuario */
24514
+ this.user = this.stateService.user;
24515
+ /** Token de acceso */
24516
+ this.accessToken = this.stateService.accessToken;
24517
+ /** Roles del usuario */
24518
+ this.roles = this.stateService.roles;
24519
+ /** Permisos del usuario */
24520
+ this.permissions = this.stateService.permissions;
24521
+ /** Usuario es super admin */
24522
+ this.isSuperAdmin = this.stateService.isSuperAdmin;
24523
+ /** Estado de MFA pendiente */
24524
+ this.mfaPending = this.stateService.mfaPending;
24525
+ /** Error actual */
24526
+ this.error = this.stateService.error;
24527
+ }
24528
+ // =============================================
24529
+ // INICIALIZACIÓN
24530
+ // =============================================
24531
+ /**
24532
+ * Inicializa el servicio de autenticación.
24533
+ * Llamado automáticamente por provideValtechAuth.
24534
+ */
24535
+ async initialize() {
24536
+ // 1. Cargar estado desde storage
24537
+ const storedState = this.storageService.loadState();
24538
+ if (storedState.accessToken) {
24539
+ // 2. Verificar si token es válido
24540
+ if (this.tokenService.isTokenValid(storedState.accessToken)) {
24541
+ this.stateService.restoreFromStorage(storedState);
24542
+ // Extraer info del token
24543
+ const claims = this.tokenService.parseToken(storedState.accessToken);
24544
+ if (claims) {
24545
+ this.stateService.updateUserInfo(claims.uid, claims.email);
24546
+ }
24547
+ // 3. Iniciar timer de refresco proactivo
24548
+ this.startRefreshTimer();
24549
+ }
24550
+ else if (storedState.refreshToken) {
24551
+ // 4. Token expirado pero hay refresh token - intentar refrescar
24552
+ try {
24553
+ await firstValueFrom(this.refreshAccessToken());
24554
+ }
24555
+ catch {
24556
+ this.clearState();
24557
+ }
24558
+ }
24559
+ else {
24560
+ this.clearState();
24561
+ }
24562
+ }
24563
+ // 5. Iniciar sincronización entre pestañas
24564
+ if (this.config.enableTabSync) {
24565
+ this.syncService.start();
24566
+ this.syncSubscription = this.syncService.onEvent$.subscribe(event => this.handleSyncEvent(event));
24567
+ }
24568
+ this.stateService.setLoading(false);
24569
+ }
24570
+ ngOnDestroy() {
24571
+ this.stopRefreshTimer();
24572
+ this.syncSubscription?.unsubscribe();
24573
+ }
24574
+ // =============================================
24575
+ // AUTENTICACIÓN
24576
+ // =============================================
24577
+ /**
24578
+ * Inicia sesión con email y contraseña.
24579
+ */
24580
+ signin(request) {
24581
+ this.stateService.clearError();
24582
+ return this.http.post(`${this.baseUrl}/signin`, request).pipe(tap(response => {
24583
+ if (response.mfaRequired) {
24584
+ // MFA requerido - guardar estado temporal
24585
+ this.stateService.setMFAPending({
24586
+ required: true,
24587
+ mfaToken: response.mfaToken,
24588
+ method: response.mfaMethod,
24589
+ });
24590
+ }
24591
+ else if (response.accessToken) {
24592
+ // Login exitoso sin MFA
24593
+ this.handleSuccessfulAuth(response);
24594
+ }
24595
+ }), catchError(error => this.handleAuthError(error)));
24596
+ }
24597
+ /**
24598
+ * Registra un nuevo usuario.
24599
+ * El usuario queda en estado PENDING hasta verificar su email.
24600
+ */
24601
+ signup(request) {
24602
+ this.stateService.clearError();
24603
+ return this.http
24604
+ .post(`${this.baseUrl}/signup`, request)
24605
+ .pipe(catchError(error => this.handleAuthError(error)));
24606
+ }
24607
+ /**
24608
+ * Verifica email con código de 6 dígitos.
24609
+ * Si es exitoso, hace auto-login y retorna tokens.
24610
+ */
24611
+ verifyEmail(request) {
24612
+ this.stateService.clearError();
24613
+ return this.http.post(`${this.baseUrl}/verify-email`, request).pipe(tap(response => {
24614
+ if (response.verified && response.accessToken) {
24615
+ // Auto-login: guardar tokens y actualizar estado
24616
+ this.handleSuccessfulAuth(response);
24617
+ }
24618
+ }), catchError(error => this.handleAuthError(error)));
24619
+ }
24620
+ /**
24621
+ * Reenvía código de verificación al email.
24622
+ */
24623
+ resendCode(request) {
24624
+ return this.http
24625
+ .post(`${this.baseUrl}/resend-code`, request)
24626
+ .pipe(catchError(error => this.handleAuthError(error)));
24627
+ }
24628
+ /**
24629
+ * Verifica código MFA.
24630
+ */
24631
+ verifyMFA(code) {
24632
+ const mfaState = this.mfaPending();
24633
+ if (!mfaState.mfaToken) {
24634
+ return throwError(() => ({
24635
+ code: 'MFA_NOT_PENDING',
24636
+ message: 'No hay verificación MFA pendiente',
24637
+ }));
24638
+ }
24639
+ return this.http
24640
+ .post(`${this.baseUrl}/mfa/verify`, {
24641
+ mfaToken: mfaState.mfaToken,
24642
+ code,
24643
+ })
24644
+ .pipe(tap(response => {
24645
+ this.stateService.clearMFAPending();
24646
+ this.handleSuccessfulAuth(response);
24647
+ }), catchError(error => this.handleAuthError(error)));
24648
+ }
24649
+ /**
24650
+ * Refresca el token de acceso.
24651
+ */
24652
+ refreshAccessToken() {
24653
+ const refreshToken = this.state().refreshToken;
24654
+ if (!refreshToken) {
24655
+ return throwError(() => ({
24656
+ code: 'NO_REFRESH_TOKEN',
24657
+ message: 'No hay token de refresco',
24658
+ }));
24659
+ }
24660
+ return this.http.post(`${this.baseUrl}/refresh`, { refreshToken }).pipe(tap(response => {
24661
+ const expiresAt = Date.now() + response.expiresIn * 1000;
24662
+ this.stateService.updateAccessToken(response.accessToken, response.expiresIn);
24663
+ this.storageService.saveAccessToken(response.accessToken, expiresAt);
24664
+ this.startRefreshTimer();
24665
+ this.syncService.broadcast({
24666
+ type: 'TOKEN_REFRESH',
24667
+ payload: { accessToken: response.accessToken, expiresAt },
24668
+ });
24669
+ }), catchError(error => {
24670
+ this.logout();
24671
+ return throwError(() => error);
24672
+ }));
24673
+ }
24674
+ /**
24675
+ * Cierra sesión.
24676
+ */
24677
+ logout() {
24678
+ const refreshToken = this.state().refreshToken;
24679
+ // Notificar al backend (fire and forget)
24680
+ if (refreshToken) {
24681
+ this.http
24682
+ .post(`${this.baseUrl}/logout`, { refreshToken })
24683
+ .pipe(catchError(() => of(null)))
24684
+ .subscribe();
24685
+ }
24686
+ // Cerrar sesión de Firebase si está integrado
24687
+ this.signOutFirebase();
24688
+ this.clearState();
24689
+ this.syncService.broadcast({ type: 'LOGOUT' });
24690
+ this.router.navigate([this.config.loginRoute]);
24691
+ }
24692
+ // =============================================
24693
+ // MFA SETUP (usuario autenticado)
24694
+ // =============================================
24695
+ /**
24696
+ * Configura MFA para el usuario.
24697
+ */
24698
+ setupMFA(method, phone) {
24699
+ return this.http
24700
+ .post(`${this.baseUrl}/mfa/setup`, { method, phone })
24701
+ .pipe(catchError(error => this.handleAuthError(error)));
24702
+ }
24703
+ /**
24704
+ * Confirma la configuración de MFA.
24705
+ */
24706
+ confirmMFA(code) {
24707
+ return this.http
24708
+ .post(`${this.baseUrl}/mfa/confirm`, { code })
24709
+ .pipe(catchError(error => this.handleAuthError(error)));
24710
+ }
24711
+ /**
24712
+ * Deshabilita MFA.
24713
+ */
24714
+ disableMFA(password) {
24715
+ return this.http
24716
+ .post(`${this.baseUrl}/mfa/disable`, { password })
24717
+ .pipe(catchError(error => this.handleAuthError(error)));
24718
+ }
24719
+ // =============================================
24720
+ // PERMISOS
24721
+ // =============================================
24722
+ /**
24723
+ * Obtiene los permisos actualizados del backend.
24724
+ */
24725
+ fetchPermissions() {
24726
+ return this.http.get(`${this.baseUrl}/permissions`).pipe(tap(response => {
24727
+ this.stateService.updatePermissions(response.roles, response.permissions, response.isSuperAdmin);
24728
+ this.storageService.savePermissions(response);
24729
+ this.syncService.broadcast({ type: 'PERMISSIONS_UPDATE' });
24730
+ }), catchError(error => this.handleAuthError(error)));
24731
+ }
24732
+ /**
24733
+ * Verifica si el usuario tiene un permiso específico.
24734
+ * Formato: "resource:action" (ej: "templates:edit")
24735
+ */
24736
+ hasPermission(permission) {
24737
+ if (this.isSuperAdmin())
24738
+ return true;
24739
+ const [resource, action] = permission.split(':');
24740
+ return this.permissions().some(p => {
24741
+ const [pResource, pAction] = p.split(':');
24742
+ return ((pResource === '*' || pResource === resource) && (pAction === '*' || pAction === action));
24743
+ });
24744
+ }
24745
+ /**
24746
+ * Verifica si el usuario tiene alguno de los permisos dados.
24747
+ */
24748
+ hasAnyPermission(permissions) {
24749
+ return permissions.some(p => this.hasPermission(p));
24750
+ }
24751
+ /**
24752
+ * Verifica si el usuario tiene todos los permisos dados.
24753
+ */
24754
+ hasAllPermissions(permissions) {
24755
+ return permissions.every(p => this.hasPermission(p));
24756
+ }
24757
+ /**
24758
+ * Verifica si el usuario tiene un rol específico.
24759
+ */
24760
+ hasRole(role) {
24761
+ return this.roles().some(r => r.toLowerCase() === role.toLowerCase());
24762
+ }
24763
+ // =============================================
24764
+ // PRIVATE METHODS
24765
+ // =============================================
24766
+ get baseUrl() {
24767
+ return `${this.config.apiUrl}${this.config.authPrefix}`;
24768
+ }
24769
+ handleSuccessfulAuth(response) {
24770
+ const expiresAt = Date.now() + response.expiresIn * 1000;
24771
+ const tokenData = this.tokenService.parseToken(response.accessToken);
24772
+ this.stateService.setAuthenticated({
24773
+ accessToken: response.accessToken,
24774
+ refreshToken: response.refreshToken,
24775
+ userId: tokenData?.uid,
24776
+ email: tokenData?.email,
24777
+ roles: response.roles || [],
24778
+ permissions: response.permissions || [],
24779
+ isSuperAdmin: response.permissions?.includes('*:*') || false,
24780
+ expiresAt,
24781
+ });
24782
+ this.storageService.saveState({
24783
+ accessToken: response.accessToken,
24784
+ refreshToken: response.refreshToken,
24785
+ roles: response.roles || [],
24786
+ permissions: response.permissions || [],
24787
+ isSuperAdmin: response.permissions?.includes('*:*') || false,
24788
+ expiresAt,
24789
+ });
24790
+ this.startRefreshTimer();
24791
+ this.syncService.broadcast({ type: 'LOGIN' });
24792
+ // Integración con Firebase
24793
+ if (this.config.enableFirebaseIntegration &&
24794
+ 'firebaseToken' in response &&
24795
+ response.firebaseToken) {
24796
+ this.signInWithFirebase(response.firebaseToken);
24797
+ }
24798
+ }
24799
+ clearState() {
24800
+ this.stopRefreshTimer();
24801
+ this.stateService.reset();
24802
+ this.storageService.clear();
24803
+ }
24804
+ startRefreshTimer() {
24805
+ this.stopRefreshTimer();
24806
+ const state = this.stateService.state();
24807
+ if (!state.expiresAt)
24808
+ return;
24809
+ const refreshBeforeMs = (this.config.refreshBeforeExpiry || 60) * 1000;
24810
+ const refreshAt = state.expiresAt - refreshBeforeMs;
24811
+ const delay = refreshAt - Date.now();
24812
+ if (delay > 0) {
24813
+ this.refreshTimerId = setTimeout(() => {
24814
+ this.refreshAccessToken().subscribe({
24815
+ error: () => this.logout(),
24816
+ });
24817
+ }, delay);
24818
+ }
24819
+ else if (state.refreshToken) {
24820
+ // Token ya debería refrescarse, intentar ahora
24821
+ this.refreshAccessToken().subscribe({
24822
+ error: () => this.logout(),
24823
+ });
24824
+ }
24825
+ }
24826
+ stopRefreshTimer() {
24827
+ if (this.refreshTimerId) {
24828
+ clearTimeout(this.refreshTimerId);
24829
+ this.refreshTimerId = null;
24830
+ }
24831
+ }
24832
+ handleSyncEvent(event) {
24833
+ switch (event.type) {
24834
+ case 'LOGIN':
24835
+ case 'TOKEN_REFRESH': {
24836
+ // Recargar estado desde storage
24837
+ const state = this.storageService.loadState();
24838
+ if (state.accessToken) {
24839
+ this.stateService.restoreFromStorage(state);
24840
+ const claims = this.tokenService.parseToken(state.accessToken);
24841
+ if (claims) {
24842
+ this.stateService.updateUserInfo(claims.uid, claims.email);
24843
+ }
24844
+ this.startRefreshTimer();
24845
+ }
24846
+ break;
24847
+ }
24848
+ case 'LOGOUT':
24849
+ this.stateService.reset();
24850
+ this.stopRefreshTimer();
24851
+ this.router.navigate([this.config.loginRoute]);
24852
+ break;
24853
+ case 'PERMISSIONS_UPDATE': {
24854
+ const perms = this.storageService.loadPermissions();
24855
+ this.stateService.updatePermissions(perms.roles, perms.permissions, perms.isSuperAdmin);
24856
+ break;
24857
+ }
24858
+ }
24859
+ }
24860
+ handleAuthError(error) {
24861
+ const authError = {
24862
+ code: error.error?.code || 'UNKNOWN_ERROR',
24863
+ message: error.error?.message || 'Error de autenticación desconocido',
24864
+ };
24865
+ this.stateService.setError(authError);
24866
+ return throwError(() => authError);
24867
+ }
24868
+ // =============================================
24869
+ // FIREBASE INTEGRATION
24870
+ // =============================================
24871
+ async signInWithFirebase(firebaseToken) {
24872
+ try {
24873
+ if (this.firebaseService) {
24874
+ await this.firebaseService.signInWithCustomToken(firebaseToken);
24875
+ console.log('[ValtechAuth] Firebase signin successful');
24876
+ }
24877
+ else {
24878
+ console.warn('[ValtechAuth] FirebaseService not provided. Add provideValtechFirebase() to your providers.');
24879
+ }
24880
+ }
24881
+ catch (error) {
24882
+ // No bloquear el login principal si Firebase falla
24883
+ console.error('[ValtechAuth] Firebase signin failed:', error);
24884
+ }
24885
+ }
24886
+ async signOutFirebase() {
24887
+ if (!this.config.enableFirebaseIntegration)
24888
+ return;
24889
+ try {
24890
+ if (this.firebaseService) {
24891
+ await this.firebaseService.signOut();
24892
+ console.log('[ValtechAuth] Firebase signout successful');
24893
+ }
24894
+ }
24895
+ catch (error) {
24896
+ // Ignorar errores de Firebase signout
24897
+ console.warn('[ValtechAuth] Firebase signout failed:', error);
24898
+ }
24899
+ }
24900
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: AuthService, deps: [{ token: VALTECH_AUTH_CONFIG }, { token: i1$9.HttpClient }, { token: i1$1.Router }, { token: AuthStateService }, { token: TokenService }, { token: AuthStorageService }, { token: AuthSyncService }, { token: FirebaseService }], target: i0.ɵɵFactoryTarget.Injectable }); }
24901
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: AuthService, providedIn: 'root' }); }
24902
+ }
24903
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: AuthService, decorators: [{
24904
+ type: Injectable,
24905
+ args: [{ providedIn: 'root' }]
24906
+ }], ctorParameters: () => [{ type: undefined, decorators: [{
24907
+ type: Inject,
24908
+ args: [VALTECH_AUTH_CONFIG]
24909
+ }] }, { type: i1$9.HttpClient }, { type: i1$1.Router }, { type: AuthStateService }, { type: TokenService }, { type: AuthStorageService }, { type: AuthSyncService }, { type: FirebaseService }] });
24910
+
24911
+ // Control de estado de refresco (singleton a nivel de módulo)
24912
+ let isRefreshing = false;
24913
+ const refreshTokenSubject = new BehaviorSubject(null);
24914
+ /**
24915
+ * Interceptor HTTP que:
24916
+ * 1. Agrega header Authorization con Bearer token a requests API
24917
+ * 2. Maneja errores 401 refrescando el token automáticamente
24918
+ * 3. Encola requests durante el refresco para evitar múltiples refresh
24919
+ *
24920
+ * @example
24921
+ * ```typescript
24922
+ * // Incluido automáticamente por provideValtechAuth()
24923
+ * // Para uso manual:
24924
+ * import { provideHttpClient, withInterceptors } from '@angular/common/http';
24925
+ * import { authInterceptor } from 'valtech-components';
24926
+ *
24927
+ * bootstrapApplication(AppComponent, {
24928
+ * providers: [
24929
+ * provideHttpClient(withInterceptors([authInterceptor])),
24930
+ * ],
24931
+ * });
24932
+ * ```
24933
+ */
24934
+ const authInterceptor = (request, next) => {
24935
+ const authService = inject(AuthService);
24936
+ const config = inject(VALTECH_AUTH_CONFIG);
24937
+ // Omitir requests que no son a nuestra API
24938
+ if (!isApiRequest(request, config.apiUrl)) {
24939
+ return next(request);
24940
+ }
24941
+ // Omitir endpoints de auth que no necesitan token
24942
+ if (isAuthEndpoint(request, config.authPrefix)) {
24943
+ return next(request);
24944
+ }
24945
+ const accessToken = authService.accessToken();
24946
+ // Agregar header de autorización si hay token
24947
+ if (accessToken) {
24948
+ request = addAuthHeader(request, accessToken);
24949
+ }
24950
+ return next(request).pipe(catchError((error) => {
24951
+ if (error.status === 401 && !isAuthEndpoint(request, config.authPrefix)) {
24952
+ return handle401Error(request, next, authService);
24953
+ }
24954
+ if (error.status === 403) {
24955
+ console.error('[ValtechAuth] Permiso denegado:', error.error?.message || 'Acceso prohibido');
24956
+ }
24957
+ return throwError(() => error);
24958
+ }));
24959
+ };
24960
+ /**
24961
+ * Agrega header de autorización a la request.
24962
+ */
24963
+ function addAuthHeader(request, token) {
24964
+ return request.clone({
24965
+ setHeaders: {
24966
+ Authorization: `Bearer ${token}`,
24967
+ },
24968
+ });
24969
+ }
24970
+ /**
24971
+ * Verifica si la request es a nuestra API.
24972
+ */
24973
+ function isApiRequest(request, apiUrl) {
24974
+ return request.url.startsWith(apiUrl) || request.url.includes('/v2/auth');
24975
+ }
24976
+ /**
24977
+ * Verifica si la request es a un endpoint de auth que no debe reintentar.
24978
+ */
24979
+ function isAuthEndpoint(request, authPrefix) {
24980
+ const authEndpoints = ['/signin', '/signup', '/refresh', '/logout', '/mfa/verify'];
24981
+ return authEndpoints.some((endpoint) => request.url.includes(`${authPrefix}${endpoint}`));
24982
+ }
24983
+ /**
24984
+ * Maneja errores 401 refrescando el token.
24985
+ */
24986
+ function handle401Error(request, next, authService) {
24987
+ if (!isRefreshing) {
24988
+ isRefreshing = true;
24989
+ refreshTokenSubject.next(null);
24990
+ return authService.refreshAccessToken().pipe(switchMap((response) => {
24991
+ refreshTokenSubject.next(response.accessToken);
24992
+ return next(addAuthHeader(request, response.accessToken));
24993
+ }), catchError((error) => {
24994
+ authService.logout();
24995
+ return throwError(() => error);
24996
+ }), finalize(() => {
24997
+ isRefreshing = false;
24998
+ }));
24999
+ }
25000
+ // Esperar a que termine el refresco en curso
25001
+ return refreshTokenSubject.pipe(filter$1((token) => token !== null), take(1), switchMap((token) => next(addAuthHeader(request, token))));
25002
+ }
25003
+
25004
+ /**
25005
+ * Token de inyección para la configuración de Auth.
25006
+ */
25007
+ const VALTECH_AUTH_CONFIG = new InjectionToken('ValtechAuthConfig');
25008
+ /**
25009
+ * Configuración por defecto.
25010
+ */
25011
+ const DEFAULT_AUTH_CONFIG = {
25012
+ authPrefix: '/v2/auth',
25013
+ storagePrefix: 'valtech_auth_',
25014
+ refreshBeforeExpiry: 60,
25015
+ enableTabSync: true,
25016
+ loginRoute: '/login',
25017
+ homeRoute: '/',
25018
+ unauthorizedRoute: '/unauthorized',
25019
+ enableFirebaseIntegration: false,
25020
+ };
25021
+ /**
25022
+ * Factory para inicializar el AuthService.
25023
+ */
25024
+ function initializeAuth(authService) {
25025
+ return () => authService.initialize();
25026
+ }
25027
+ /**
25028
+ * Provee el servicio de autenticación a la aplicación Angular.
25029
+ *
25030
+ * @param config - Configuración de autenticación
25031
+ * @returns EnvironmentProviders para usar en bootstrapApplication
25032
+ *
25033
+ * @example
25034
+ * ```typescript
25035
+ * // main.ts
25036
+ * import { bootstrapApplication } from '@angular/platform-browser';
25037
+ * import { provideValtechAuth } from 'valtech-components';
25038
+ * import { environment } from './environments/environment';
25039
+ *
25040
+ * bootstrapApplication(AppComponent, {
25041
+ * providers: [
25042
+ * provideValtechAuth({
25043
+ * apiUrl: environment.apiUrl,
25044
+ * enableFirebaseIntegration: true,
25045
+ * }),
25046
+ * ],
25047
+ * });
25048
+ * ```
25049
+ */
25050
+ function provideValtechAuth(config) {
25051
+ const mergedConfig = {
25052
+ ...DEFAULT_AUTH_CONFIG,
25053
+ ...config,
25054
+ };
25055
+ return makeEnvironmentProviders([
25056
+ { provide: VALTECH_AUTH_CONFIG, useValue: mergedConfig },
25057
+ provideHttpClient(withInterceptors([authInterceptor])),
25058
+ // Inicializar AuthService al arrancar la app
25059
+ {
25060
+ provide: APP_INITIALIZER,
25061
+ useFactory: initializeAuth,
25062
+ deps: [AuthService],
25063
+ multi: true,
25064
+ },
25065
+ ]);
25066
+ }
25067
+ /**
25068
+ * Provee solo el interceptor (para apps que ya tienen AuthService configurado manualmente).
25069
+ */
25070
+ function provideValtechAuthInterceptor() {
25071
+ return makeEnvironmentProviders([
25072
+ provideHttpClient(withInterceptors([authInterceptor])),
25073
+ ]);
25074
+ }
25075
+
25076
+ /**
25077
+ * Guard que verifica si el usuario está autenticado.
25078
+ * Redirige a loginRoute si no está autenticado.
25079
+ *
25080
+ * @example
25081
+ * ```typescript
25082
+ * import { authGuard } from 'valtech-components';
25083
+ *
25084
+ * const routes: Routes = [
25085
+ * {
25086
+ * path: 'dashboard',
25087
+ * canActivate: [authGuard],
25088
+ * loadComponent: () => import('./dashboard.page'),
25089
+ * },
25090
+ * ];
25091
+ * ```
25092
+ */
25093
+ const authGuard = () => {
25094
+ const authService = inject(AuthService);
25095
+ const router = inject(Router);
25096
+ const config = inject(VALTECH_AUTH_CONFIG);
25097
+ if (authService.isAuthenticated()) {
25098
+ return true;
25099
+ }
25100
+ return router.createUrlTree([config.loginRoute]);
25101
+ };
25102
+ /**
25103
+ * Guard que verifica si el usuario NO está autenticado.
25104
+ * Redirige a homeRoute si ya está autenticado.
25105
+ * Útil para páginas de login/registro.
25106
+ *
25107
+ * @example
25108
+ * ```typescript
25109
+ * import { guestGuard } from 'valtech-components';
25110
+ *
25111
+ * const routes: Routes = [
25112
+ * {
25113
+ * path: 'login',
25114
+ * canActivate: [guestGuard],
25115
+ * loadComponent: () => import('./login.page'),
25116
+ * },
25117
+ * ];
25118
+ * ```
25119
+ */
25120
+ const guestGuard = () => {
25121
+ const authService = inject(AuthService);
25122
+ const router = inject(Router);
25123
+ const config = inject(VALTECH_AUTH_CONFIG);
25124
+ if (!authService.isAuthenticated()) {
25125
+ return true;
25126
+ }
25127
+ return router.createUrlTree([config.homeRoute]);
25128
+ };
25129
+ /**
25130
+ * Factory para crear guard de permisos.
25131
+ * Verifica si el usuario tiene el permiso especificado.
25132
+ *
25133
+ * @param permissions - Permiso o lista de permisos requeridos (OR)
25134
+ * @returns Guard function
25135
+ *
25136
+ * @example
25137
+ * ```typescript
25138
+ * import { authGuard, permissionGuard } from 'valtech-components';
25139
+ *
25140
+ * const routes: Routes = [
25141
+ * {
25142
+ * path: 'templates',
25143
+ * canActivate: [authGuard, permissionGuard('templates:read')],
25144
+ * loadComponent: () => import('./templates.page'),
25145
+ * },
25146
+ * {
25147
+ * path: 'admin',
25148
+ * canActivate: [authGuard, permissionGuard(['admin:*', 'super_admin'])],
25149
+ * loadComponent: () => import('./admin.page'),
25150
+ * },
25151
+ * ];
25152
+ * ```
25153
+ */
25154
+ function permissionGuard(permissions) {
25155
+ return () => {
25156
+ const authService = inject(AuthService);
25157
+ const router = inject(Router);
25158
+ const config = inject(VALTECH_AUTH_CONFIG);
25159
+ const permArray = Array.isArray(permissions) ? permissions : [permissions];
25160
+ if (authService.hasAnyPermission(permArray)) {
25161
+ return true;
25162
+ }
25163
+ console.warn(`[ValtechAuth] Permiso denegado. Requerido: ${permArray.join(' o ')}`);
25164
+ return router.createUrlTree([config.unauthorizedRoute]);
25165
+ };
25166
+ }
25167
+ /**
25168
+ * Guard que lee permisos desde route.data.
25169
+ * Permite configurar permisos directamente en la definición de rutas.
25170
+ *
25171
+ * @example
25172
+ * ```typescript
25173
+ * import { authGuard, permissionGuardFromRoute } from 'valtech-components';
25174
+ *
25175
+ * const routes: Routes = [
25176
+ * {
25177
+ * path: 'admin/users',
25178
+ * canActivate: [authGuard, permissionGuardFromRoute],
25179
+ * data: {
25180
+ * permissions: ['users:read', 'users:manage'],
25181
+ * requireAll: false // true = AND, false = OR (default)
25182
+ * },
25183
+ * loadComponent: () => import('./users.page'),
25184
+ * },
25185
+ * ];
25186
+ * ```
25187
+ */
25188
+ const permissionGuardFromRoute = (route) => {
25189
+ const authService = inject(AuthService);
25190
+ const router = inject(Router);
25191
+ const config = inject(VALTECH_AUTH_CONFIG);
25192
+ const permissions = route.data['permissions'];
25193
+ const requireAll = route.data['requireAll'];
25194
+ if (!permissions || permissions.length === 0) {
25195
+ return true;
25196
+ }
25197
+ const hasAccess = requireAll
25198
+ ? authService.hasAllPermissions(permissions)
25199
+ : authService.hasAnyPermission(permissions);
25200
+ if (hasAccess) {
25201
+ return true;
25202
+ }
25203
+ console.warn(`[ValtechAuth] Permiso denegado. Requerido: ${permissions.join(requireAll ? ' y ' : ' o ')}`);
25204
+ return router.createUrlTree([config.unauthorizedRoute]);
25205
+ };
25206
+ /**
25207
+ * Guard que verifica si el usuario es super admin.
25208
+ *
25209
+ * @example
25210
+ * ```typescript
25211
+ * import { authGuard, superAdminGuard } from 'valtech-components';
25212
+ *
25213
+ * const routes: Routes = [
25214
+ * {
25215
+ * path: 'super-admin',
25216
+ * canActivate: [authGuard, superAdminGuard],
25217
+ * loadComponent: () => import('./super-admin.page'),
25218
+ * },
25219
+ * ];
25220
+ * ```
25221
+ */
25222
+ const superAdminGuard = () => {
25223
+ const authService = inject(AuthService);
25224
+ const router = inject(Router);
25225
+ const config = inject(VALTECH_AUTH_CONFIG);
25226
+ if (authService.isSuperAdmin()) {
25227
+ return true;
25228
+ }
25229
+ console.warn('[ValtechAuth] Acceso de super admin requerido');
25230
+ return router.createUrlTree([config.unauthorizedRoute]);
25231
+ };
25232
+ /**
25233
+ * Guard que verifica si el usuario tiene un rol específico.
25234
+ *
25235
+ * @param roles - Rol o lista de roles requeridos (OR)
25236
+ * @returns Guard function
25237
+ *
25238
+ * @example
25239
+ * ```typescript
25240
+ * import { authGuard, roleGuard } from 'valtech-components';
25241
+ *
25242
+ * const routes: Routes = [
25243
+ * {
25244
+ * path: 'editor',
25245
+ * canActivate: [authGuard, roleGuard(['editor', 'admin'])],
25246
+ * loadComponent: () => import('./editor.page'),
25247
+ * },
25248
+ * ];
25249
+ * ```
25250
+ */
25251
+ function roleGuard(roles) {
25252
+ return () => {
25253
+ const authService = inject(AuthService);
25254
+ const router = inject(Router);
25255
+ const config = inject(VALTECH_AUTH_CONFIG);
25256
+ const roleArray = Array.isArray(roles) ? roles : [roles];
25257
+ const hasRole = roleArray.some((role) => authService.hasRole(role));
25258
+ if (hasRole) {
25259
+ return true;
25260
+ }
25261
+ console.warn(`[ValtechAuth] Rol requerido: ${roleArray.join(' o ')}`);
25262
+ return router.createUrlTree([config.unauthorizedRoute]);
25263
+ };
25264
+ }
25265
+
25266
+ /**
25267
+ * Valtech Auth Service
25268
+ *
25269
+ * Servicio de autenticación reutilizable para aplicaciones Angular.
25270
+ * Proporciona autenticación con AuthV2, MFA, sincronización entre pestañas,
25271
+ * y refresh proactivo de tokens.
25272
+ *
25273
+ * @example
25274
+ * ```typescript
25275
+ * // En main.ts
25276
+ * import { bootstrapApplication } from '@angular/platform-browser';
25277
+ * import { provideValtechAuth } from 'valtech-components';
25278
+ * import { environment } from './environments/environment';
25279
+ *
25280
+ * bootstrapApplication(AppComponent, {
25281
+ * providers: [
25282
+ * provideValtechAuth({
25283
+ * apiUrl: environment.apiUrl,
25284
+ * enableFirebaseIntegration: true,
25285
+ * }),
25286
+ * ],
25287
+ * });
25288
+ *
25289
+ * // En app.routes.ts
25290
+ * import { authGuard, guestGuard, permissionGuard } from 'valtech-components';
25291
+ *
25292
+ * const routes: Routes = [
25293
+ * { path: 'login', canActivate: [guestGuard], loadComponent: () => import('./login.page') },
25294
+ * { path: 'dashboard', canActivate: [authGuard], loadComponent: () => import('./dashboard.page') },
25295
+ * { path: 'admin', canActivate: [authGuard, permissionGuard('admin:*')], loadComponent: () => import('./admin.page') },
25296
+ * ];
25297
+ *
25298
+ * // En componentes
25299
+ * import { AuthService } from 'valtech-components';
25300
+ *
25301
+ * @Component({...})
25302
+ * export class LoginComponent {
25303
+ * private auth = inject(AuthService);
25304
+ *
25305
+ * async login() {
25306
+ * await firstValueFrom(this.auth.signin({ email, password }));
25307
+ * if (this.auth.mfaPending().required) {
25308
+ * // Mostrar UI de MFA
25309
+ * } else {
25310
+ * this.router.navigate(['/dashboard']);
25311
+ * }
25312
+ * }
25313
+ *
25314
+ * // En template: usar signals directamente
25315
+ * // {{ auth.user()?.email }}
25316
+ * // @if (auth.hasPermission('templates:edit')) { ... }
25317
+ * }
25318
+ * ```
25319
+ */
25320
+ // Tipos
25321
+
21131
25322
  /*
21132
25323
  * Public API Surface of valtech-components
21133
25324
  */
@@ -21136,5 +25327,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
21136
25327
  * Generated bundle index. Do not edit.
21137
25328
  */
21138
25329
 
21139
- export { ARTICLE_SPACING, AccordionComponent, ActionHeaderComponent, ActionType, AlertBoxComponent, ArticleBuilder, ArticleComponent, AvatarComponent, BannerComponent, BaseDefault, BoxComponent, BreadcrumbComponent, ButtonComponent, ButtonGroupComponent, COMMON_COUNTRY_CODES, COMMON_CURRENCIES, CURRENCY_INFO, CardComponent, CardSection, CardType, CardsCarouselComponent, CheckInputComponent, ChipGroupComponent, ClearDefault, ClearDefaultBlock, ClearDefaultFull, ClearDefaultRound, ClearDefaultRoundBlock, ClearDefaultRoundFull, CodeDisplayComponent, CommandDisplayComponent, CommentComponent, CommentInputComponent, CommentSectionComponent, CompanyFooterComponent, ComponentStates, ConfirmationDialogService, ContentLoaderComponent, CountdownComponent, CurrencyInputComponent, DEFAULT_CANCEL_BUTTON, DEFAULT_CONFIRM_BUTTON, DEFAULT_COUNTDOWN_LABELS, DEFAULT_COUNTDOWN_LABELS_EN, DEFAULT_EMPTY_STATE, DEFAULT_LEGEND_LABELS, DEFAULT_MODAL_CANCEL_BUTTON, DEFAULT_MODAL_CONFIRM_BUTTON, DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_PAYMENT_STATUS_COLORS, DEFAULT_PAYMENT_STATUS_LABELS, DEFAULT_PLATFORMS, DEFAULT_STATUS_COLORS, DEFAULT_STATUS_LABELS, DEFAULT_WINNER_LABELS, DataTableComponent, DateInputComponent, DateRangeInputComponent, DisplayComponent, DividerComponent, DownloadService, EmailInputComponent, ExpandableTextComponent, FabComponent, FileInputComponent, FooterComponent, FooterLinksComponent, FormComponent, FormFooterComponent, FunHeaderComponent, GlowCardComponent, HeaderComponent, HintComponent, HorizontalScrollComponent, HourInputComponent, HrefComponent, Icon, IconComponent, IconService, ImageComponent, InAppBrowserService, InfoComponent, InputType, ItemListComponent, LanguageSelectorComponent, LayeredCardComponent, LayoutComponent, LinkComponent, LinkProcessorService, LinksAccordionComponent, LinksCakeComponent, LocalStorageService, LocaleService, MODAL_SIZES, MOTION, MenuComponent, ModalService, MultiSelectSearchComponent, NavigationService, NoContentComponent, NotesBoxComponent, NumberFromToComponent, NumberInputComponent, NumberStepperComponent, OutlineDefault, OutlineDefaultBlock, OutlineDefaultFull, OutlineDefaultRound, OutlineDefaultRoundBlock, OutlineDefaultRoundFull, PLATFORM_CONFIGS, PageContentComponent, PageTemplateComponent, PageWrapperComponent, PaginationComponent, ParticipantCardComponent, PasswordInputComponent, PhoneInputComponent, PillComponent, PinInputComponent, PlainCodeBoxComponent, PopoverSelectorComponent, PriceTagComponent, PrimarySolidBlockButton, PrimarySolidBlockHrefButton, PrimarySolidBlockIconButton, PrimarySolidBlockIconHrefButton, PrimarySolidDefaultRoundButton, PrimarySolidDefaultRoundHrefButton, PrimarySolidDefaultRoundIconButton, PrimarySolidDefaultRoundIconHrefButton, PrimarySolidFullButton, PrimarySolidFullHrefButton, PrimarySolidFullIconButton, PrimarySolidFullIconHrefButton, PrimarySolidLargeRoundButton, PrimarySolidLargeRoundHrefButton, PrimarySolidLargeRoundIconButton, PrimarySolidLargeRoundIconHrefButton, PrimarySolidSmallRoundButton, PrimarySolidSmallRoundHrefButton, PrimarySolidSmallRoundIconButton, PrimarySolidSmallRoundIconHrefButton, ProcessLinksPipe, ProgressBarComponent, ProgressRingComponent, ProgressStatusComponent, PrompterComponent, QR_PRESETS, QrCodeComponent, QrGeneratorService, QuoteBoxComponent, RadioInputComponent, RaffleStatusCardComponent, RangeInputComponent, RatingComponent, RecapCardComponent, RightsFooterComponent, SKELETON_PRESETS, SearchSelectorComponent, SearchbarComponent, SecondarySolidBlockButton, SecondarySolidBlockHrefButton, SecondarySolidBlockIconButton, SecondarySolidBlockIconHrefButton, SecondarySolidDefaultRoundButton, SecondarySolidDefaultRoundHrefButton, SecondarySolidDefaultRoundIconButton, SecondarySolidDefaultRoundIconHrefButton, SecondarySolidFullButton, SecondarySolidFullHrefButton, SecondarySolidFullIconButton, SecondarySolidFullIconHrefButton, SecondarySolidLargeRoundButton, SecondarySolidLargeRoundHrefButton, SecondarySolidLargeRoundIconButton, SecondarySolidLargeRoundIconHrefButton, SecondarySolidSmallRoundButton, SecondarySolidSmallRoundHrefButton, SecondarySolidSmallRoundIconButton, SecondarySolidSmallRoundIconHrefButton, SegmentControlComponent, SelectSearchComponent, ShareButtonsComponent, SimpleComponent, SkeletonComponent, SolidBlockButton, SolidDefault, SolidDefaultBlock, SolidDefaultButton, SolidDefaultFull, SolidDefaultRound, SolidDefaultRoundBlock, SolidDefaultRoundButton, SolidDefaultRoundFull, SolidFullButton, SolidLargeButton, SolidLargeRoundButton, SolidSmallButton, SolidSmallRoundButton, StatsCardComponent, StepperComponent, SwipeCarouselComponent, TabbedContentComponent, TabsComponent, TestimonialCardComponent, TestimonialCarouselComponent, TextComponent, TextInputComponent, TextareaInputComponent, ThemeOption, ThemeService, TicketGridComponent, TimelineComponent, TitleBlockComponent, TitleComponent, ToastService, ToggleInputComponent, ToolbarActionType, ToolbarComponent, WinnerDisplayComponent, WizardComponent, WizardFooterComponent, applyDefaultValueToControl, createGlowCardProps, createNumberFromToField, createTitleProps, goToTop, isAtEnd, maxLength, replaceSpecialChars, resolveColor, resolveInputDefaultValue };
25330
+ export { APP_IDS, ARTICLE_SPACING, AccordionComponent, ActionHeaderComponent, ActionType, AlertBoxComponent, ArticleBuilder, ArticleComponent, AuthService, AuthStateService, AuthStorageService, AuthSyncService, AvatarComponent, BannerComponent, BaseDefault, BoxComponent, BreadcrumbComponent, ButtonComponent, ButtonGroupComponent, COMMON_COUNTRY_CODES, COMMON_CURRENCIES, CURRENCY_INFO, CardComponent, CardSection, CardType, CardsCarouselComponent, CheckInputComponent, ChipGroupComponent, ClearDefault, ClearDefaultBlock, ClearDefaultFull, ClearDefaultRound, ClearDefaultRoundBlock, ClearDefaultRoundFull, CodeDisplayComponent, CommandDisplayComponent, CommentComponent, CommentInputComponent, CommentSectionComponent, CompanyFooterComponent, ComponentStates, ConfirmationDialogService, ContentLoaderComponent, CountdownComponent, CurrencyInputComponent, DEFAULT_AUTH_CONFIG, DEFAULT_CANCEL_BUTTON, DEFAULT_CONFIRM_BUTTON, DEFAULT_COUNTDOWN_LABELS, DEFAULT_COUNTDOWN_LABELS_EN, DEFAULT_EMPTY_STATE, DEFAULT_LEGEND_LABELS, DEFAULT_MODAL_CANCEL_BUTTON, DEFAULT_MODAL_CONFIRM_BUTTON, DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_PAYMENT_STATUS_COLORS, DEFAULT_PAYMENT_STATUS_LABELS, DEFAULT_PLATFORMS, DEFAULT_STATUS_COLORS, DEFAULT_STATUS_LABELS, DEFAULT_WINNER_LABELS, DataTableComponent, DateInputComponent, DateRangeInputComponent, DisplayComponent, DividerComponent, DownloadService, EmailInputComponent, ExpandableTextComponent, FIREBASE_PROJECTS, FabComponent, FileInputComponent, FirebaseService, FirestoreCollectionFactory, FirestoreService, FooterComponent, FooterLinksComponent, FormComponent, FormFooterComponent, FunHeaderComponent, GlowCardComponent, HeaderComponent, HintComponent, HorizontalScrollComponent, HourInputComponent, HrefComponent, INITIAL_AUTH_STATE, INITIAL_MFA_STATE, Icon, IconComponent, IconService, ImageComponent, InAppBrowserService, InfoComponent, InputType, ItemListComponent, LanguageSelectorComponent, LayeredCardComponent, LayoutComponent, LinkComponent, LinkProcessorService, LinksAccordionComponent, LinksCakeComponent, LocalStorageService, LocaleService, MODAL_SIZES, MOTION, MenuComponent, MessagingService, ModalService, MultiSelectSearchComponent, NavigationService, NoContentComponent, NotesBoxComponent, NumberFromToComponent, NumberInputComponent, NumberStepperComponent, OutlineDefault, OutlineDefaultBlock, OutlineDefaultFull, OutlineDefaultRound, OutlineDefaultRoundBlock, OutlineDefaultRoundFull, PLATFORM_CONFIGS, PageContentComponent, PageTemplateComponent, PageWrapperComponent, PaginationComponent, ParticipantCardComponent, PasswordInputComponent, PhoneInputComponent, PillComponent, PinInputComponent, PlainCodeBoxComponent, PopoverSelectorComponent, PriceTagComponent, PrimarySolidBlockButton, PrimarySolidBlockHrefButton, PrimarySolidBlockIconButton, PrimarySolidBlockIconHrefButton, PrimarySolidDefaultRoundButton, PrimarySolidDefaultRoundHrefButton, PrimarySolidDefaultRoundIconButton, PrimarySolidDefaultRoundIconHrefButton, PrimarySolidFullButton, PrimarySolidFullHrefButton, PrimarySolidFullIconButton, PrimarySolidFullIconHrefButton, PrimarySolidLargeRoundButton, PrimarySolidLargeRoundHrefButton, PrimarySolidLargeRoundIconButton, PrimarySolidLargeRoundIconHrefButton, PrimarySolidSmallRoundButton, PrimarySolidSmallRoundHrefButton, PrimarySolidSmallRoundIconButton, PrimarySolidSmallRoundIconHrefButton, ProcessLinksPipe, ProgressBarComponent, ProgressRingComponent, ProgressStatusComponent, PrompterComponent, QR_PRESETS, QrCodeComponent, QrGeneratorService, QueryBuilder, QuoteBoxComponent, RadioInputComponent, RaffleStatusCardComponent, RangeInputComponent, RatingComponent, RecapCardComponent, RightsFooterComponent, SHARED_EMULATOR_CONFIG, SKELETON_PRESETS, SearchSelectorComponent, SearchbarComponent, SecondarySolidBlockButton, SecondarySolidBlockHrefButton, SecondarySolidBlockIconButton, SecondarySolidBlockIconHrefButton, SecondarySolidDefaultRoundButton, SecondarySolidDefaultRoundHrefButton, SecondarySolidDefaultRoundIconButton, SecondarySolidDefaultRoundIconHrefButton, SecondarySolidFullButton, SecondarySolidFullHrefButton, SecondarySolidFullIconButton, SecondarySolidFullIconHrefButton, SecondarySolidLargeRoundButton, SecondarySolidLargeRoundHrefButton, SecondarySolidLargeRoundIconButton, SecondarySolidLargeRoundIconHrefButton, SecondarySolidSmallRoundButton, SecondarySolidSmallRoundHrefButton, SecondarySolidSmallRoundIconButton, SecondarySolidSmallRoundIconHrefButton, SegmentControlComponent, SelectSearchComponent, ShareButtonsComponent, SimpleComponent, SkeletonComponent, SolidBlockButton, SolidDefault, SolidDefaultBlock, SolidDefaultButton, SolidDefaultFull, SolidDefaultRound, SolidDefaultRoundBlock, SolidDefaultRoundButton, SolidDefaultRoundFull, SolidFullButton, SolidLargeButton, SolidLargeRoundButton, SolidSmallButton, SolidSmallRoundButton, StatsCardComponent, StepperComponent, StorageService, SwipeCarouselComponent, TabbedContentComponent, TabsComponent, TestimonialCardComponent, TestimonialCarouselComponent, TextComponent, TextInputComponent, TextareaInputComponent, ThemeOption, ThemeService, TicketGridComponent, TimelineComponent, TitleBlockComponent, TitleComponent, ToastService, ToggleInputComponent, TokenService, ToolbarActionType, ToolbarComponent, TypedCollection, VALTECH_AUTH_CONFIG, VALTECH_FIREBASE_CONFIG, WinnerDisplayComponent, WizardComponent, WizardFooterComponent, applyDefaultValueToControl, authGuard, authInterceptor, buildPath, collections, createFirebaseConfig, createGlowCardProps, createNumberFromToField, createTitleProps, extractPathParams, getCollectionPath, getDocumentId, goToTop, guestGuard, hasEmulators, isAtEnd, isCollectionPath, isDocumentPath, isEmulatorMode, isValidPath, joinPath, maxLength, permissionGuard, permissionGuardFromRoute, provideValtechAuth, provideValtechAuthInterceptor, provideValtechFirebase, query, replaceSpecialChars, resolveColor, resolveInputDefaultValue, roleGuard, storagePaths, superAdminGuard };
21140
25331
  //# sourceMappingURL=valtech-components.mjs.map