valtech-components 2.0.837 → 2.0.839

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.
@@ -2,5 +2,5 @@
2
2
  * Current version of valtech-components.
3
3
  * This is automatically updated during the publish process.
4
4
  */
5
- export const VERSION = '2.0.837';
6
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidmVyc2lvbi5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uL3NyYy9saWIvdmVyc2lvbi50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7O0dBR0c7QUFDSCxNQUFNLENBQUMsTUFBTSxPQUFPLEdBQUcsU0FBUyxDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBDdXJyZW50IHZlcnNpb24gb2YgdmFsdGVjaC1jb21wb25lbnRzLlxuICogVGhpcyBpcyBhdXRvbWF0aWNhbGx5IHVwZGF0ZWQgZHVyaW5nIHRoZSBwdWJsaXNoIHByb2Nlc3MuXG4gKi9cbmV4cG9ydCBjb25zdCBWRVJTSU9OID0gJzIuMC44MzcnO1xuIl19
5
+ export const VERSION = '2.0.839';
6
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidmVyc2lvbi5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uL3NyYy9saWIvdmVyc2lvbi50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7O0dBR0c7QUFDSCxNQUFNLENBQUMsTUFBTSxPQUFPLEdBQUcsU0FBUyxDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBDdXJyZW50IHZlcnNpb24gb2YgdmFsdGVjaC1jb21wb25lbnRzLlxuICogVGhpcyBpcyBhdXRvbWF0aWNhbGx5IHVwZGF0ZWQgZHVyaW5nIHRoZSBwdWJsaXNoIHByb2Nlc3MuXG4gKi9cbmV4cG9ydCBjb25zdCBWRVJTSU9OID0gJzIuMC44MzknO1xuIl19
@@ -53,7 +53,7 @@ import 'prismjs/components/prism-json';
53
53
  * Current version of valtech-components.
54
54
  * This is automatically updated during the publish process.
55
55
  */
56
- const VERSION = '2.0.837';
56
+ const VERSION = '2.0.839';
57
57
 
58
58
  /**
59
59
  * Servicio para gestionar presets de componentes.
@@ -20056,7 +20056,7 @@ const UPDATE_BANNER_I18N_NAMESPACE = 'UpdateBanner';
20056
20056
  */
20057
20057
  const UPDATE_BANNER_DEFAULT_CONTENT = {
20058
20058
  es: {
20059
- availableTitle: 'Hay una versión nueva disponible',
20059
+ availableTitle: 'Hay una versión nueva disponible',
20060
20060
  availableMessage: 'Actualiza para obtener las últimas mejoras.',
20061
20061
  requiredTitle: 'Debes actualizar para continuar',
20062
20062
  requiredMessage: 'Esta versión ya no es compatible. Actualiza para seguir.',
@@ -20064,7 +20064,7 @@ const UPDATE_BANNER_DEFAULT_CONTENT = {
20064
20064
  dismissAction: 'Cerrar',
20065
20065
  },
20066
20066
  en: {
20067
- availableTitle: 'A new version is available',
20067
+ availableTitle: 'A new version is available',
20068
20068
  availableMessage: 'Update to get the latest improvements.',
20069
20069
  requiredTitle: 'You must update to continue',
20070
20070
  requiredMessage: 'This version is no longer supported. Please update.',
@@ -20072,7 +20072,7 @@ const UPDATE_BANNER_DEFAULT_CONTENT = {
20072
20072
  dismissAction: 'Close',
20073
20073
  },
20074
20074
  pt: {
20075
- availableTitle: 'Há uma nova versão disponível',
20075
+ availableTitle: 'Há uma nova versão disponível',
20076
20076
  availableMessage: 'Atualize para obter as últimas melhorias.',
20077
20077
  requiredTitle: 'Você precisa atualizar para continuar',
20078
20078
  requiredMessage: 'Esta versão não é mais compatível. Atualize para seguir.',
@@ -20224,7 +20224,7 @@ class UpdateBannerComponent {
20224
20224
  <val-button
20225
20225
  [props]="{
20226
20226
  text: t().dismissAction,
20227
- color: 'medium',
20227
+ color: 'dark',
20228
20228
  fill: 'clear',
20229
20229
  size: 'small',
20230
20230
  state: 'ENABLED',
@@ -20237,7 +20237,7 @@ class UpdateBannerComponent {
20237
20237
  </div>
20238
20238
  </div>
20239
20239
  }
20240
- `, isInline: true, styles: [":host{display:contents}.val-update-banner{position:fixed;top:0;left:0;right:0;z-index:1100;display:flex;justify-content:center;padding:calc(12px + env(safe-area-inset-top,0px)) 16px 12px;pointer-events:none}.val-update-banner__backdrop{position:fixed;inset:0;z-index:-1;background:#0000008c;backdrop-filter:blur(2px);-webkit-backdrop-filter:blur(2px);pointer-events:auto}.val-update-banner__panel{pointer-events:auto;display:flex;flex-direction:row;align-items:center;gap:12px;width:100%;max-width:640px;padding:12px 16px;background:var(--ion-background-color, #fff);border:1px solid var(--val-border-color, rgba(0, 0, 0, .08));border-radius:var(--val-border-radius, 12px);box-shadow:0 4px 24px #0000001f;animation:val-update-banner-in .25s ease-out}.val-update-banner--required{align-items:center;height:100%}.val-update-banner--required .val-update-banner__panel{flex-direction:column;text-align:center;max-width:420px}.val-update-banner__icon{flex:0 0 auto;display:inline-flex;align-items:center}.val-update-banner__body{flex:1 1 auto;display:flex;flex-direction:column;gap:2px;min-width:0}.val-update-banner__actions{flex:0 0 auto;display:flex;flex-direction:row;align-items:center;gap:4px}.val-update-banner--required .val-update-banner__actions{margin-top:8px}@keyframes val-update-banner-in{0%{opacity:0;transform:translateY(-12px)}to{opacity:1;transform:translateY(0)}}@media (max-width: 540px){.val-update-banner__panel{flex-direction:column;text-align:center}.val-update-banner__actions{width:100%;justify-content:center}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: TextComponent, selector: "val-text", inputs: ["props"] }, { kind: "component", type: ButtonComponent, selector: "val-button", inputs: ["preset", "props"], outputs: ["onClick"] }, { kind: "component", type: IconComponent, selector: "val-icon", inputs: ["props"] }] }); }
20240
+ `, isInline: true, styles: ["@charset \"UTF-8\";:host{display:contents}.val-update-banner{position:fixed;top:0;left:0;right:0;z-index:1100;display:flex;justify-content:center;padding:calc(12px + env(safe-area-inset-top,0px)) 16px 12px;pointer-events:none}.val-update-banner__backdrop{position:fixed;inset:0;z-index:-1;background:#0000008c;backdrop-filter:blur(2px);-webkit-backdrop-filter:blur(2px);pointer-events:auto}.val-update-banner__panel{pointer-events:auto;display:flex;flex-direction:row;align-items:center;gap:12px;width:100%;max-width:640px;padding:12px 16px;background:var(--ion-background-color, #fff);border:1px solid var(--val-border-color, rgba(0, 0, 0, .08));border-radius:var(--val-border-radius, 12px);box-shadow:0 4px 24px #0000001f;animation:val-update-banner-in .25s ease-out}.val-update-banner--required{align-items:center;height:100%}.val-update-banner--required .val-update-banner__panel{flex-direction:column;text-align:center;max-width:420px}.val-update-banner__icon{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:44px;height:44px;border-radius:12px;background:color-mix(in srgb,var(--ion-color-primary) 14%,transparent)}.val-update-banner--required .val-update-banner__icon{background:color-mix(in srgb,var(--ion-color-warning) 16%,transparent)}.val-update-banner__body{flex:1 1 auto;display:flex;flex-direction:column;gap:2px;min-width:0}.val-update-banner__actions{flex:0 0 auto;display:flex;flex-direction:row;align-items:center;gap:4px}.val-update-banner--required .val-update-banner__actions{margin-top:8px}@keyframes val-update-banner-in{0%{opacity:0;transform:translateY(-12px)}to{opacity:1;transform:translateY(0)}}@media (max-width: 540px){.val-update-banner__panel{flex-direction:column;text-align:center}.val-update-banner__actions{width:100%;justify-content:center}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: TextComponent, selector: "val-text", inputs: ["props"] }, { kind: "component", type: ButtonComponent, selector: "val-button", inputs: ["preset", "props"], outputs: ["onClick"] }, { kind: "component", type: IconComponent, selector: "val-icon", inputs: ["props"] }] }); }
20241
20241
  }
20242
20242
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: UpdateBannerComponent, decorators: [{
20243
20243
  type: Component,
@@ -20299,7 +20299,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
20299
20299
  <val-button
20300
20300
  [props]="{
20301
20301
  text: t().dismissAction,
20302
- color: 'medium',
20302
+ color: 'dark',
20303
20303
  fill: 'clear',
20304
20304
  size: 'small',
20305
20305
  state: 'ENABLED',
@@ -20312,7 +20312,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
20312
20312
  </div>
20313
20313
  </div>
20314
20314
  }
20315
- `, styles: [":host{display:contents}.val-update-banner{position:fixed;top:0;left:0;right:0;z-index:1100;display:flex;justify-content:center;padding:calc(12px + env(safe-area-inset-top,0px)) 16px 12px;pointer-events:none}.val-update-banner__backdrop{position:fixed;inset:0;z-index:-1;background:#0000008c;backdrop-filter:blur(2px);-webkit-backdrop-filter:blur(2px);pointer-events:auto}.val-update-banner__panel{pointer-events:auto;display:flex;flex-direction:row;align-items:center;gap:12px;width:100%;max-width:640px;padding:12px 16px;background:var(--ion-background-color, #fff);border:1px solid var(--val-border-color, rgba(0, 0, 0, .08));border-radius:var(--val-border-radius, 12px);box-shadow:0 4px 24px #0000001f;animation:val-update-banner-in .25s ease-out}.val-update-banner--required{align-items:center;height:100%}.val-update-banner--required .val-update-banner__panel{flex-direction:column;text-align:center;max-width:420px}.val-update-banner__icon{flex:0 0 auto;display:inline-flex;align-items:center}.val-update-banner__body{flex:1 1 auto;display:flex;flex-direction:column;gap:2px;min-width:0}.val-update-banner__actions{flex:0 0 auto;display:flex;flex-direction:row;align-items:center;gap:4px}.val-update-banner--required .val-update-banner__actions{margin-top:8px}@keyframes val-update-banner-in{0%{opacity:0;transform:translateY(-12px)}to{opacity:1;transform:translateY(0)}}@media (max-width: 540px){.val-update-banner__panel{flex-direction:column;text-align:center}.val-update-banner__actions{width:100%;justify-content:center}}\n"] }]
20315
+ `, styles: ["@charset \"UTF-8\";:host{display:contents}.val-update-banner{position:fixed;top:0;left:0;right:0;z-index:1100;display:flex;justify-content:center;padding:calc(12px + env(safe-area-inset-top,0px)) 16px 12px;pointer-events:none}.val-update-banner__backdrop{position:fixed;inset:0;z-index:-1;background:#0000008c;backdrop-filter:blur(2px);-webkit-backdrop-filter:blur(2px);pointer-events:auto}.val-update-banner__panel{pointer-events:auto;display:flex;flex-direction:row;align-items:center;gap:12px;width:100%;max-width:640px;padding:12px 16px;background:var(--ion-background-color, #fff);border:1px solid var(--val-border-color, rgba(0, 0, 0, .08));border-radius:var(--val-border-radius, 12px);box-shadow:0 4px 24px #0000001f;animation:val-update-banner-in .25s ease-out}.val-update-banner--required{align-items:center;height:100%}.val-update-banner--required .val-update-banner__panel{flex-direction:column;text-align:center;max-width:420px}.val-update-banner__icon{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:44px;height:44px;border-radius:12px;background:color-mix(in srgb,var(--ion-color-primary) 14%,transparent)}.val-update-banner--required .val-update-banner__icon{background:color-mix(in srgb,var(--ion-color-warning) 16%,transparent)}.val-update-banner__body{flex:1 1 auto;display:flex;flex-direction:column;gap:2px;min-width:0}.val-update-banner__actions{flex:0 0 auto;display:flex;flex-direction:row;align-items:center;gap:4px}.val-update-banner--required .val-update-banner__actions{margin-top:8px}@keyframes val-update-banner-in{0%{opacity:0;transform:translateY(-12px)}to{opacity:1;transform:translateY(0)}}@media (max-width: 540px){.val-update-banner__panel{flex-direction:column;text-align:center}.val-update-banner__actions{width:100%;justify-content:center}}\n"] }]
20316
20316
  }] });
20317
20317
 
20318
20318
  /**
@@ -23242,6 +23242,31 @@ class MessagingService {
23242
23242
  * Es un *optimistic hint* — la verdad la confirma el siguiente `getToken()`.
23243
23243
  */
23244
23244
  this.TOKEN_STORAGE_KEY = 'valtech_fcm_token';
23245
+ /**
23246
+ * Timeout (ms) para `navigator.serviceWorker.ready` dentro de `getToken()`.
23247
+ *
23248
+ * En un cold load el SW puede no estar activado aún y `serviceWorker.ready`
23249
+ * no resolver nunca. Pasado este tiempo, `getToken()` rechaza limpio en lugar
23250
+ * de colgarse indefinidamente.
23251
+ */
23252
+ this.SW_READY_TIMEOUT_MS = 10_000;
23253
+ /**
23254
+ * Timeout (ms) del watchdog de `enable()` antes de auto-recargar la página.
23255
+ *
23256
+ * El flujo de activación a veces se cuelga ANTES de `getToken()` — caso
23257
+ * típico: `Notification.requestPermission()` no muestra el popup del SO en un
23258
+ * cold load. El timeout de `getToken()` (SW_READY_TIMEOUT_MS) no cubre eso;
23259
+ * por eso `enable()` envuelve el flujo completo en este watchdog.
23260
+ *
23261
+ * Un flujo exitoso real llega a token+device en ~4s; 15s es holgado y no
23262
+ * atrapa un éxito lento.
23263
+ */
23264
+ this.ENABLE_WATCHDOG_MS = 15_000;
23265
+ /**
23266
+ * Key de sessionStorage — garantiza un solo auto-reload por sesión. Si el
23267
+ * flujo se cuelga una 2ª vez tras el reload, NO se recarga de nuevo (anti-loop).
23268
+ */
23269
+ this.AUTORELOAD_FLAG = 'notif-enable-autoreload';
23245
23270
  this.debugPersistence = this.config?.debugMessagePersistence ?? false;
23246
23271
  this.initializeMessaging();
23247
23272
  }
@@ -23395,6 +23420,158 @@ class MessagingService {
23395
23420
  return null;
23396
23421
  }
23397
23422
  }
23423
+ /**
23424
+ * Flujo completo de activación de push, robusto, cross-app.
23425
+ *
23426
+ * Orquesta: pedir permiso → SW ready → obtener token FCM → (opcional)
23427
+ * registrar el device en backend. Es el punto de orquestación único que cada
23428
+ * app del factory consume — la lógica de robustez vive aquí, no en cada página.
23429
+ *
23430
+ * **Watchdog de auto-reload.** El flujo a veces se cuelga ANTES de `getToken()`
23431
+ * (ej. `Notification.requestPermission()` que no muestra el popup en un cold
23432
+ * load) — el timeout interno de `getToken()` no cubre ese caso. Por eso
23433
+ * `enable()` envuelve el flujo entero en un watchdog: si no alcanza un estado
23434
+ * terminal en `ENABLE_WATCHDOG_MS` (15s):
23435
+ * - 1ª vez en la sesión → marca un flag en `sessionStorage` y hace
23436
+ * `window.location.reload()` (un fresh load suele tener el SW activo).
23437
+ * Resuelve con `{ status: 'timeout', reloaded: true }`.
23438
+ * - ya se auto-recargó antes → NO recarga (anti-loop): limpia el flag y
23439
+ * resuelve con `{ status: 'timeout', reloaded: false }` para que la app
23440
+ * muestre un error.
23441
+ *
23442
+ * NO hace throw — siempre resuelve con un `EnablePushResult` descriptivo que
23443
+ * la página consumidora inspecciona para decidir qué toast mostrar.
23444
+ *
23445
+ * @param options.registerDevice Callback opcional que registra el device en
23446
+ * el backend (vive en `AuthService` — se pasa como callback para evitar el
23447
+ * ciclo de DI AuthService ↔ MessagingService).
23448
+ *
23449
+ * @example
23450
+ * ```typescript
23451
+ * const result = await messaging.enable({
23452
+ * registerDevice: (token) => auth.registerDevice(token).then(r => r.registered),
23453
+ * });
23454
+ * if (result.status === 'enabled') {
23455
+ * // result.token disponible — push activo
23456
+ * } else if (result.status === 'timeout' && !result.reloaded) {
23457
+ * // mostrar error: el flujo se colgó y ya se consumió el auto-reload
23458
+ * }
23459
+ * ```
23460
+ */
23461
+ async enable(options = {}) {
23462
+ console.log('[Messaging] enable() start');
23463
+ let watchdog;
23464
+ // El watchdog corre en paralelo al flujo real. Lo que gane el race define
23465
+ // el resultado. El flujo real, si gana, limpia el watchdog en el `finally`.
23466
+ const watchdogPromise = new Promise(resolve => {
23467
+ if (typeof window === 'undefined')
23468
+ return; // SSR — sin watchdog.
23469
+ watchdog = setTimeout(() => {
23470
+ watchdog = undefined;
23471
+ console.warn(`[Messaging] enable() colgado >${this.ENABLE_WATCHDOG_MS}ms — watchdog`);
23472
+ if (this.hasAutoReloaded()) {
23473
+ // Anti-loop: ya recargamos una vez esta sesión y volvió a colgar.
23474
+ console.warn('[Messaging] auto-reload ya consumido — no se recarga de nuevo');
23475
+ this.clearAutoReloadFlag();
23476
+ resolve({
23477
+ status: 'timeout',
23478
+ reloaded: false,
23479
+ reason: `Flujo de activación colgado >${this.ENABLE_WATCHDOG_MS / 1000}s`,
23480
+ });
23481
+ return;
23482
+ }
23483
+ // 1ª vez: marcar y recargar. Un fresh load suele tener el SW activo.
23484
+ console.warn('[Messaging] auto-recargando la página (1/1 por sesión)');
23485
+ this.markAutoReloaded();
23486
+ resolve({
23487
+ status: 'timeout',
23488
+ reloaded: true,
23489
+ reason: 'Auto-recargando para reintentar la activación',
23490
+ });
23491
+ window.location.reload();
23492
+ }, this.ENABLE_WATCHDOG_MS);
23493
+ });
23494
+ const flow = this.runEnableFlow(options).finally(() => {
23495
+ if (watchdog !== undefined) {
23496
+ clearTimeout(watchdog);
23497
+ watchdog = undefined;
23498
+ }
23499
+ });
23500
+ return Promise.race([flow, watchdogPromise]);
23501
+ }
23502
+ /**
23503
+ * Flujo real de activación, sin el watchdog (lo envuelve `enable()`).
23504
+ * Resuelve siempre con un `EnablePushResult` — no hace throw.
23505
+ */
23506
+ async runEnableFlow(options) {
23507
+ if (!(await this.isSupported())) {
23508
+ console.warn('[Messaging] enable: FCM no soportado');
23509
+ return { status: 'unsupported', reason: 'FCM no soportado en este navegador' };
23510
+ }
23511
+ try {
23512
+ // permission → SW ready → getToken (requestPermission encadena las 3).
23513
+ const token = await this.requestPermission();
23514
+ if (!token) {
23515
+ // requestPermission devuelve null tanto por permiso denegado como por
23516
+ // fallo al obtener el token. Distinguimos por el estado del permiso.
23517
+ const permission = this.getPermissionState();
23518
+ if (permission === 'denied' || permission === 'default') {
23519
+ return { status: 'denied', reason: 'Permiso de notificaciones denegado' };
23520
+ }
23521
+ return { status: 'error', reason: 'No se pudo obtener el token FCM' };
23522
+ }
23523
+ // Registro de device en backend (opcional — el caller pasa el callback).
23524
+ if (options.registerDevice) {
23525
+ const registered = await options.registerDevice(token);
23526
+ if (!registered) {
23527
+ return { status: 'error', token, reason: 'No se pudo registrar el dispositivo' };
23528
+ }
23529
+ }
23530
+ // Éxito: estado terminal. Limpiar el flag anti-loop para futuros intentos.
23531
+ this.clearAutoReloadFlag();
23532
+ console.log('[Messaging] enable() success');
23533
+ return { status: 'enabled', token };
23534
+ }
23535
+ catch (error) {
23536
+ const reason = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
23537
+ console.error('[Messaging] enable() error:', reason, error);
23538
+ return { status: 'error', reason };
23539
+ }
23540
+ }
23541
+ /** True si ya se auto-recargó una vez en esta sesión. */
23542
+ hasAutoReloaded() {
23543
+ if (!isPlatformBrowser(this.platformId))
23544
+ return false;
23545
+ try {
23546
+ return sessionStorage.getItem(this.AUTORELOAD_FLAG) === '1';
23547
+ }
23548
+ catch {
23549
+ // sessionStorage puede no estar disponible (modo privado estricto, SSR).
23550
+ return false;
23551
+ }
23552
+ }
23553
+ /** Marca que se consumió el auto-reload de esta sesión. */
23554
+ markAutoReloaded() {
23555
+ if (!isPlatformBrowser(this.platformId))
23556
+ return;
23557
+ try {
23558
+ sessionStorage.setItem(this.AUTORELOAD_FLAG, '1');
23559
+ }
23560
+ catch {
23561
+ /* sessionStorage no disponible — best-effort. */
23562
+ }
23563
+ }
23564
+ /** Limpia el flag anti-loop — tras un éxito o un fallo definitivo. */
23565
+ clearAutoReloadFlag() {
23566
+ if (!isPlatformBrowser(this.platformId))
23567
+ return;
23568
+ try {
23569
+ sessionStorage.removeItem(this.AUTORELOAD_FLAG);
23570
+ }
23571
+ catch {
23572
+ /* sessionStorage no disponible — nada que limpiar. */
23573
+ }
23574
+ }
23398
23575
  /**
23399
23576
  * Obtiene el token FCM actual (sin solicitar permiso).
23400
23577
  *
@@ -23423,8 +23600,11 @@ class MessagingService {
23423
23600
  // revalidaciones del SW en iOS PWA. Solo registramos si no existe aún.
23424
23601
  const registration = await this.resolveServiceWorkerRegistration();
23425
23602
  console.log('[Messaging] SW resolved, waiting ready...');
23426
- // Esperar a que el SW esté activo
23427
- await navigator.serviceWorker.ready;
23603
+ // Esperar a que el SW esté activo, con timeout: `navigator.serviceWorker.ready`
23604
+ // puede no resolver NUNCA en un cold load (SW aún no activado) → sin el
23605
+ // timeout `getToken()` se cuelga indefinidamente. Con el race, si el SW no
23606
+ // queda listo en SW_READY_TIMEOUT_MS, esta promesa rechaza limpio.
23607
+ await this.waitForServiceWorkerReady();
23428
23608
  console.log('[Messaging] SW ready, calling Firebase getToken()...');
23429
23609
  const token = await getToken(messaging, {
23430
23610
  vapidKey,
@@ -23463,6 +23643,35 @@ class MessagingService {
23463
23643
  console.warn('[Messaging] SW not registered yet, registering as fallback');
23464
23644
  return navigator.serviceWorker.register('/firebase-messaging-sw.js');
23465
23645
  }
23646
+ /**
23647
+ * Espera a que el Service Worker quede activo (`navigator.serviceWorker.ready`)
23648
+ * pero con un timeout duro.
23649
+ *
23650
+ * `navigator.serviceWorker.ready` resuelve cuando hay un SW *activo*. En un
23651
+ * cold load (primera visita, SW recién registrado, iOS PWA recién abierta) el
23652
+ * SW puede quedar en estado `installing`/`waiting` y `ready` no resolver nunca
23653
+ * → `getToken()` se cuelga. El `Promise.race` contra un `setTimeout` garantiza
23654
+ * que esta espera termina: o gana `ready` (caso normal) o gana el timeout y
23655
+ * lanzamos un error claro para que `getToken()` rechace en vez de colgarse.
23656
+ *
23657
+ * El timer se limpia en ambas ramas para no dejar timers colgando.
23658
+ */
23659
+ waitForServiceWorkerReady() {
23660
+ let timer;
23661
+ const ready = navigator.serviceWorker.ready.then(() => {
23662
+ if (timer !== undefined)
23663
+ clearTimeout(timer);
23664
+ });
23665
+ const timeout = new Promise((_, reject) => {
23666
+ timer = setTimeout(() => {
23667
+ reject(new Error(`[Messaging] service worker no quedó listo en ${this.SW_READY_TIMEOUT_MS / 1000}s`));
23668
+ }, this.SW_READY_TIMEOUT_MS);
23669
+ });
23670
+ return Promise.race([ready, timeout]).finally(() => {
23671
+ if (timer !== undefined)
23672
+ clearTimeout(timer);
23673
+ });
23674
+ }
23466
23675
  /**
23467
23676
  * Persiste el token FCM en localStorage (o lo limpia si es null/empty).
23468
23677
  */
@@ -29410,7 +29619,7 @@ class CookieBannerComponent {
29410
29619
  </div>
29411
29620
 
29412
29621
  <div class="val-cookie-banner__actions">
29413
- <ion-button fill="clear" size="small" [color]="props.rejectColor || 'medium'" (click)="reject.emit()">
29622
+ <ion-button fill="clear" size="small" [color]="props.rejectColor || 'dark'" (click)="reject.emit()">
29414
29623
  {{ props.rejectText }}
29415
29624
  </ion-button>
29416
29625
 
@@ -29444,7 +29653,7 @@ class CookieBannerComponent {
29444
29653
  </div>
29445
29654
  </div>
29446
29655
  }
29447
- `, isInline: true, styles: [":host{display:contents}.val-cookie-banner{position:fixed;left:0;right:0;z-index:1000;padding:12px 16px calc(12px + env(safe-area-inset-bottom,0px));background:var(--ion-background-color, #fff);border-color:var(--val-border-color, rgba(0, 0, 0, .08));border-style:solid;border-width:0;box-shadow:0 -2px 16px #00000014;animation:val-cookie-banner-in .25s ease-out}.val-cookie-banner--bottom{bottom:0;border-top-width:1px}.val-cookie-banner--top{top:0;border-bottom-width:1px;padding:calc(12px + env(safe-area-inset-top,0px)) 16px 12px;box-shadow:0 2px 16px #00000014}.val-cookie-banner--translucent{background:#ffffffd9;backdrop-filter:saturate(180%) blur(16px);-webkit-backdrop-filter:saturate(180%) blur(16px)}@media (prefers-color-scheme: dark){.val-cookie-banner--translucent{background:#141414d9}}.val-cookie-banner__inner{margin:0 auto;display:flex;flex-direction:row;align-items:center;gap:16px;position:relative}.val-cookie-banner__dismiss{position:absolute;top:-8px;right:-4px;width:28px;height:28px;display:inline-flex;align-items:center;justify-content:center;background:transparent;border:0;border-radius:50%;cursor:pointer;color:var(--ion-color-medium);transition:color .15s ease,background .15s ease}.val-cookie-banner__dismiss:hover{color:var(--ion-color-dark);background:var(--ion-color-light-shade, rgba(0, 0, 0, .04))}.val-cookie-banner__dismiss ion-icon{font-size:18px}.val-cookie-banner__copy{flex:1 1 auto;min-width:0}.val-cookie-banner__title{margin:0 0 4px;font-size:14px;font-weight:600;color:var(--ion-color-dark)}.val-cookie-banner__message{margin:0;font-size:13px;line-height:1.45;color:var(--ion-color-dark)}.val-cookie-banner__policy{margin-left:4px;color:var(--ion-color-primary);text-decoration:underline;text-underline-offset:2px}.val-cookie-banner__actions{display:inline-flex;flex-direction:row;align-items:center;gap:8px;flex-shrink:0}@media (max-width: 768px){.val-cookie-banner__inner{flex-direction:column;align-items:stretch;gap:12px}.val-cookie-banner__actions{flex-wrap:wrap;justify-content:flex-end}}@media (max-width: 480px){.val-cookie-banner__actions{flex-direction:column;align-items:stretch}.val-cookie-banner__actions ion-button{width:100%}}@keyframes val-cookie-banner-in{0%{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}.val-cookie-banner--top{animation-name:val-cookie-banner-in-top}@keyframes val-cookie-banner-in-top{0%{opacity:0;transform:translateY(-12px)}to{opacity:1;transform:translateY(0)}}@media (prefers-reduced-motion: reduce){.val-cookie-banner{animation:none}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
29656
+ `, isInline: true, styles: [":host{display:contents}.val-cookie-banner{position:fixed;left:0;right:0;z-index:1000;padding:12px 16px calc(12px + env(safe-area-inset-bottom,0px));background:var(--ion-background-color, #fff);border-color:var(--val-border-color, rgba(0, 0, 0, .08));border-style:solid;border-width:0;box-shadow:0 -2px 16px #00000014;animation:val-cookie-banner-in .25s ease-out}.val-cookie-banner--bottom{bottom:0;border-top-width:1px}.val-cookie-banner--top{top:0;border-bottom-width:1px;padding:calc(12px + env(safe-area-inset-top,0px)) 16px 12px;box-shadow:0 2px 16px #00000014}.val-cookie-banner--translucent{background:color-mix(in srgb,var(--ion-background-color, #fff) 85%,transparent);backdrop-filter:saturate(180%) blur(16px);-webkit-backdrop-filter:saturate(180%) blur(16px)}.val-cookie-banner__inner{margin:0 auto;display:flex;flex-direction:row;align-items:center;gap:16px;position:relative}.val-cookie-banner__dismiss{position:absolute;top:-8px;right:-4px;width:28px;height:28px;display:inline-flex;align-items:center;justify-content:center;background:transparent;border:0;border-radius:50%;cursor:pointer;color:var(--ion-color-medium);transition:color .15s ease,background .15s ease}.val-cookie-banner__dismiss:hover{color:var(--ion-color-dark);background:var(--ion-color-light-shade, rgba(0, 0, 0, .04))}.val-cookie-banner__dismiss ion-icon{font-size:18px}.val-cookie-banner__copy{flex:1 1 auto;min-width:0}.val-cookie-banner__title{margin:0 0 4px;font-size:14px;font-weight:600;color:var(--ion-color-dark)}.val-cookie-banner__message{margin:0;font-size:13px;line-height:1.45;color:var(--ion-color-dark)}.val-cookie-banner__policy{margin-left:4px;color:var(--ion-color-primary);text-decoration:underline;text-underline-offset:2px}.val-cookie-banner__actions{display:inline-flex;flex-direction:row;align-items:center;gap:8px;flex-shrink:0}@media (max-width: 768px){.val-cookie-banner__inner{flex-direction:column;align-items:stretch;gap:12px}.val-cookie-banner__actions{flex-wrap:wrap;justify-content:flex-end}}@media (max-width: 480px){.val-cookie-banner__actions{flex-direction:column;align-items:stretch}.val-cookie-banner__actions ion-button{width:100%}}@keyframes val-cookie-banner-in{0%{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}.val-cookie-banner--top{animation-name:val-cookie-banner-in-top}@keyframes val-cookie-banner-in-top{0%{opacity:0;transform:translateY(-12px)}to{opacity:1;transform:translateY(0)}}@media (prefers-reduced-motion: reduce){.val-cookie-banner{animation:none}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
29448
29657
  }
29449
29658
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: CookieBannerComponent, decorators: [{
29450
29659
  type: Component,
@@ -29486,7 +29695,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
29486
29695
  </div>
29487
29696
 
29488
29697
  <div class="val-cookie-banner__actions">
29489
- <ion-button fill="clear" size="small" [color]="props.rejectColor || 'medium'" (click)="reject.emit()">
29698
+ <ion-button fill="clear" size="small" [color]="props.rejectColor || 'dark'" (click)="reject.emit()">
29490
29699
  {{ props.rejectText }}
29491
29700
  </ion-button>
29492
29701
 
@@ -29520,7 +29729,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
29520
29729
  </div>
29521
29730
  </div>
29522
29731
  }
29523
- `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:contents}.val-cookie-banner{position:fixed;left:0;right:0;z-index:1000;padding:12px 16px calc(12px + env(safe-area-inset-bottom,0px));background:var(--ion-background-color, #fff);border-color:var(--val-border-color, rgba(0, 0, 0, .08));border-style:solid;border-width:0;box-shadow:0 -2px 16px #00000014;animation:val-cookie-banner-in .25s ease-out}.val-cookie-banner--bottom{bottom:0;border-top-width:1px}.val-cookie-banner--top{top:0;border-bottom-width:1px;padding:calc(12px + env(safe-area-inset-top,0px)) 16px 12px;box-shadow:0 2px 16px #00000014}.val-cookie-banner--translucent{background:#ffffffd9;backdrop-filter:saturate(180%) blur(16px);-webkit-backdrop-filter:saturate(180%) blur(16px)}@media (prefers-color-scheme: dark){.val-cookie-banner--translucent{background:#141414d9}}.val-cookie-banner__inner{margin:0 auto;display:flex;flex-direction:row;align-items:center;gap:16px;position:relative}.val-cookie-banner__dismiss{position:absolute;top:-8px;right:-4px;width:28px;height:28px;display:inline-flex;align-items:center;justify-content:center;background:transparent;border:0;border-radius:50%;cursor:pointer;color:var(--ion-color-medium);transition:color .15s ease,background .15s ease}.val-cookie-banner__dismiss:hover{color:var(--ion-color-dark);background:var(--ion-color-light-shade, rgba(0, 0, 0, .04))}.val-cookie-banner__dismiss ion-icon{font-size:18px}.val-cookie-banner__copy{flex:1 1 auto;min-width:0}.val-cookie-banner__title{margin:0 0 4px;font-size:14px;font-weight:600;color:var(--ion-color-dark)}.val-cookie-banner__message{margin:0;font-size:13px;line-height:1.45;color:var(--ion-color-dark)}.val-cookie-banner__policy{margin-left:4px;color:var(--ion-color-primary);text-decoration:underline;text-underline-offset:2px}.val-cookie-banner__actions{display:inline-flex;flex-direction:row;align-items:center;gap:8px;flex-shrink:0}@media (max-width: 768px){.val-cookie-banner__inner{flex-direction:column;align-items:stretch;gap:12px}.val-cookie-banner__actions{flex-wrap:wrap;justify-content:flex-end}}@media (max-width: 480px){.val-cookie-banner__actions{flex-direction:column;align-items:stretch}.val-cookie-banner__actions ion-button{width:100%}}@keyframes val-cookie-banner-in{0%{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}.val-cookie-banner--top{animation-name:val-cookie-banner-in-top}@keyframes val-cookie-banner-in-top{0%{opacity:0;transform:translateY(-12px)}to{opacity:1;transform:translateY(0)}}@media (prefers-reduced-motion: reduce){.val-cookie-banner{animation:none}}\n"] }]
29732
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:contents}.val-cookie-banner{position:fixed;left:0;right:0;z-index:1000;padding:12px 16px calc(12px + env(safe-area-inset-bottom,0px));background:var(--ion-background-color, #fff);border-color:var(--val-border-color, rgba(0, 0, 0, .08));border-style:solid;border-width:0;box-shadow:0 -2px 16px #00000014;animation:val-cookie-banner-in .25s ease-out}.val-cookie-banner--bottom{bottom:0;border-top-width:1px}.val-cookie-banner--top{top:0;border-bottom-width:1px;padding:calc(12px + env(safe-area-inset-top,0px)) 16px 12px;box-shadow:0 2px 16px #00000014}.val-cookie-banner--translucent{background:color-mix(in srgb,var(--ion-background-color, #fff) 85%,transparent);backdrop-filter:saturate(180%) blur(16px);-webkit-backdrop-filter:saturate(180%) blur(16px)}.val-cookie-banner__inner{margin:0 auto;display:flex;flex-direction:row;align-items:center;gap:16px;position:relative}.val-cookie-banner__dismiss{position:absolute;top:-8px;right:-4px;width:28px;height:28px;display:inline-flex;align-items:center;justify-content:center;background:transparent;border:0;border-radius:50%;cursor:pointer;color:var(--ion-color-medium);transition:color .15s ease,background .15s ease}.val-cookie-banner__dismiss:hover{color:var(--ion-color-dark);background:var(--ion-color-light-shade, rgba(0, 0, 0, .04))}.val-cookie-banner__dismiss ion-icon{font-size:18px}.val-cookie-banner__copy{flex:1 1 auto;min-width:0}.val-cookie-banner__title{margin:0 0 4px;font-size:14px;font-weight:600;color:var(--ion-color-dark)}.val-cookie-banner__message{margin:0;font-size:13px;line-height:1.45;color:var(--ion-color-dark)}.val-cookie-banner__policy{margin-left:4px;color:var(--ion-color-primary);text-decoration:underline;text-underline-offset:2px}.val-cookie-banner__actions{display:inline-flex;flex-direction:row;align-items:center;gap:8px;flex-shrink:0}@media (max-width: 768px){.val-cookie-banner__inner{flex-direction:column;align-items:stretch;gap:12px}.val-cookie-banner__actions{flex-wrap:wrap;justify-content:flex-end}}@media (max-width: 480px){.val-cookie-banner__actions{flex-direction:column;align-items:stretch}.val-cookie-banner__actions ion-button{width:100%}}@keyframes val-cookie-banner-in{0%{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}.val-cookie-banner--top{animation-name:val-cookie-banner-in-top}@keyframes val-cookie-banner-in-top{0%{opacity:0;transform:translateY(-12px)}to{opacity:1;transform:translateY(0)}}@media (prefers-reduced-motion: reduce){.val-cookie-banner{animation:none}}\n"] }]
29524
29733
  }], ctorParameters: () => [], propDecorators: { props: [{
29525
29734
  type: Input
29526
29735
  }], accept: [{
@@ -47070,10 +47279,13 @@ const VALTECH_FOOTER_LOGO = {
47070
47279
  * Company links organized by section
47071
47280
  */
47072
47281
  const VALTECH_COMPANY_LINKS = {
47282
+ // Links legales con `external: true` → abren en tab nueva (target=_blank).
47073
47283
  legal: [
47074
47284
  { key: 'aboutUs', url: '/legal/about', kind: 'site', external: false },
47075
- { key: 'privacyPolicy', url: '/legal/privacy', kind: 'legal', external: false },
47076
- { key: 'termsConditions', url: '/legal/terms', kind: 'legal', external: false },
47285
+ { key: 'privacyPolicy', url: '/legal/privacy', kind: 'legal', external: true },
47286
+ { key: 'termsConditions', url: '/legal/terms', kind: 'legal', external: true },
47287
+ { key: 'cookiesPolicy', url: '/legal/cookies', kind: 'legal', external: true },
47288
+ { key: 'legalNotice', url: '/legal/legal-notice', kind: 'legal', external: true },
47077
47289
  ],
47078
47290
  support: [
47079
47291
  { key: 'contactSupport', url: '/contact', kind: 'support', external: false },
@@ -47096,6 +47308,8 @@ const VALTECH_FOOTER_I18N = {
47096
47308
  aboutUs: 'Nosotros',
47097
47309
  privacyPolicy: 'Política de privacidad',
47098
47310
  termsConditions: 'Términos y condiciones',
47311
+ cookiesPolicy: 'Política de cookies',
47312
+ legalNotice: 'Aviso legal',
47099
47313
  // Support links
47100
47314
  contactSupport: 'Contactar a soporte',
47101
47315
  faq: 'Preguntas frecuentes',
@@ -47111,6 +47325,8 @@ const VALTECH_FOOTER_I18N = {
47111
47325
  aboutUs: 'About Us',
47112
47326
  privacyPolicy: 'Privacy Policy',
47113
47327
  termsConditions: 'Terms & Conditions',
47328
+ cookiesPolicy: 'Cookie Policy',
47329
+ legalNotice: 'Legal Notice',
47114
47330
  // Support links
47115
47331
  contactSupport: 'Contact Support',
47116
47332
  faq: 'FAQ',