valtech-components 2.0.452 → 2.0.454

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