valtech-components 4.0.166 → 4.0.168

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.
@@ -56,7 +56,7 @@ import { BrowserMultiFormatReader } from '@zxing/browser';
56
56
  * Current version of valtech-components.
57
57
  * This is automatically updated during the publish process.
58
58
  */
59
- const VERSION = '4.0.166';
59
+ const VERSION = '4.0.168';
60
60
 
61
61
  // Control de estado de refresco (singleton a nivel de módulo)
62
62
  let isRefreshing = false;
@@ -27973,6 +27973,13 @@ function provideValtechAppVersionHttp(config) {
27973
27973
  *
27974
27974
  * Las tres dependencias se inyectan como opcionales.
27975
27975
  */
27976
+ /**
27977
+ * Máximo que `applyUpdate` espera por el `VERSION_READY` del SW antes de recargar
27978
+ * igual. Cubre el caso en que el update lo detectó el path HTTP antes de que el
27979
+ * SW descargara el bundle nuevo (evita el loop de banner). Si el SW nunca queda
27980
+ * listo (Android Chrome), recarga como último recurso al vencer.
27981
+ */
27982
+ const SW_READY_TIMEOUT_MS = 8000;
27976
27983
  /**
27977
27984
  * Compara dos versiones semver. Retorna true si `a` es estrictamente menor
27978
27985
  * que `b`. Tolerante a longitudes distintas y a partes no numéricas.
@@ -27999,9 +28006,13 @@ class AppVersionService {
27999
28006
  this.appConfig = inject(AppConfigService, { optional: true });
28000
28007
  this.serviceConfig = inject(VALTECH_APP_VERSION, { optional: true });
28001
28008
  /** Plugin nativo opcional: provisto por apps Capacitor para Play Store / App Store. */
28002
- this.platformPlugin = inject(APP_VERSION_PLATFORM_PLUGIN, { optional: true });
28009
+ this.platformPlugin = inject(APP_VERSION_PLATFORM_PLUGIN, {
28010
+ optional: true,
28011
+ });
28003
28012
  /** Plugin HTTP opcional: alternativa a AppConfigService para apps sin Firebase. */
28004
- this.remotePlugin = inject(APP_VERSION_REMOTE_PLUGIN, { optional: true });
28013
+ this.remotePlugin = inject(APP_VERSION_REMOTE_PLUGIN, {
28014
+ optional: true,
28015
+ });
28005
28016
  this.document = inject(DOCUMENT);
28006
28017
  this.destroyRef = inject(DestroyRef);
28007
28018
  /** True cuando el SW reportó un bundle nuevo listo (`VERSION_READY`). */
@@ -28070,7 +28081,21 @@ class AppVersionService {
28070
28081
  }
28071
28082
  try {
28072
28083
  if (this.swUpdate?.isEnabled) {
28073
- await this.swUpdate.activateUpdate();
28084
+ // Si la actualización se detectó por el path HTTP (/version.json) ANTES
28085
+ // de que el SW descargue el bundle nuevo, `swUpdateReady` aún es false:
28086
+ // recargar ahora volvería a cargar el MISMO bundle viejo y el banner
28087
+ // reaparecería (loop de "actualizar N veces"). Pedimos al SW que chequee
28088
+ // y esperamos a que el bundle quede listo (con timeout) para que el
28089
+ // reload realmente cambie de versión.
28090
+ if (!this.swUpdateReady()) {
28091
+ await this.swUpdate.checkForUpdate().catch(() => undefined);
28092
+ if (!this.swUpdateReady()) {
28093
+ await this.waitForSwReady(SW_READY_TIMEOUT_MS);
28094
+ }
28095
+ }
28096
+ if (this.swUpdateReady()) {
28097
+ await this.swUpdate.activateUpdate();
28098
+ }
28074
28099
  }
28075
28100
  }
28076
28101
  catch (err) {
@@ -28080,6 +28105,30 @@ class AppVersionService {
28080
28105
  this.document.defaultView?.location.reload();
28081
28106
  }
28082
28107
  }
28108
+ /**
28109
+ * Espera a que el SW reporte un bundle nuevo listo (`VERSION_READY`), o hasta
28110
+ * `timeoutMs`. Evita el reload "en seco" cuando el update lo detectó el path
28111
+ * HTTP antes de que el SW descargara el bundle. Si el timeout vence (ej.
28112
+ * Android Chrome donde VERSION_READY es poco confiable), resuelve igual y el
28113
+ * caller recarga como último recurso.
28114
+ */
28115
+ waitForSwReady(timeoutMs) {
28116
+ if (this.swUpdateReady() || !this.swUpdate?.isEnabled)
28117
+ return Promise.resolve();
28118
+ return new Promise(resolve => {
28119
+ let settled = false;
28120
+ const finish = () => {
28121
+ if (settled)
28122
+ return;
28123
+ settled = true;
28124
+ sub.unsubscribe();
28125
+ clearTimeout(timer);
28126
+ resolve();
28127
+ };
28128
+ const sub = this.swUpdate.versionUpdates.pipe(filter$1(evt => evt.type === 'VERSION_READY')).subscribe(() => finish());
28129
+ const timer = setTimeout(finish, timeoutMs);
28130
+ });
28131
+ }
28083
28132
  /**
28084
28133
  * Descarta el banner de actualización opcional.
28085
28134
  * No tiene efecto sobre el estado `update-required` (hard gate).
@@ -28093,7 +28142,9 @@ class AppVersionService {
28093
28142
  async checkNow() {
28094
28143
  await Promise.all([
28095
28144
  this.swUpdate?.isEnabled
28096
- ? this.swUpdate.checkForUpdate().catch(err => console.warn('[AppVersionService] checkForUpdate failed:', err))
28145
+ ? this.swUpdate
28146
+ .checkForUpdate()
28147
+ .catch(err => console.warn('[AppVersionService] checkForUpdate failed:', err))
28097
28148
  : undefined,
28098
28149
  this.remotePlugin ? this.loadRemoteConfig() : undefined,
28099
28150
  ]);
@@ -61465,14 +61516,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
61465
61516
  // Use LocaleService for language management instead
61466
61517
 
61467
61518
  const CANVAS_WIDTH = 560;
61468
- const HEADER_H = 160;
61469
- const QR_ZONE_H = 380;
61470
- const FOOTER_H = 240;
61471
- const CANVAS_HEIGHT = HEADER_H + QR_ZONE_H + FOOTER_H;
61472
- const QR_SIZE = 280;
61519
+ const HEADER_H = 210;
61520
+ const QR_ZONE_H = 340;
61521
+ const PERF_H = 28;
61522
+ const FOOTER_H = 220;
61523
+ const CANVAS_HEIGHT = HEADER_H + QR_ZONE_H + PERF_H + FOOTER_H;
61524
+ const QR_SIZE = 260;
61473
61525
  const DEFAULT_BRAND_COLOR = '#9b1fde';
61474
61526
  const DEFAULT_BRAND_COLOR_TO = '#c93075';
61475
- const FONT_STACK = 'Khula, Arial, sans-serif';
61527
+ const FONT_STACK = '"Plus Jakarta Sans", Khula, Arial, sans-serif';
61476
61528
  class TicketCardImageService {
61477
61529
  async generate(config) {
61478
61530
  const width = config.width ?? CANVAS_WIDTH;
@@ -61490,6 +61542,7 @@ class TicketCardImageService {
61490
61542
  }
61491
61543
  await this.drawHeader(ctx, config, width);
61492
61544
  await this.drawQrZone(ctx, config, width);
61545
+ this.drawPerforated(ctx, config, width);
61493
61546
  this.drawFooter(ctx, config, width);
61494
61547
  if (canvas instanceof OffscreenCanvas) {
61495
61548
  return canvas.convertToBlob({ type: 'image/png' });
@@ -61543,31 +61596,41 @@ class TicketCardImageService {
61543
61596
  grad.addColorStop(1, colorTo);
61544
61597
  ctx.fillStyle = grad;
61545
61598
  ctx.fillRect(0, 0, width, HEADER_H);
61546
- const logoH = 44;
61547
- const logoPad = 28;
61548
- let textY = HEADER_H / 2;
61599
+ const logoH = 48;
61600
+ const logoPadTop = 28;
61601
+ let cursorY = logoPadTop;
61549
61602
  if (config.logoUrl) {
61550
61603
  try {
61551
61604
  const logoImg = await this.loadImage(config.logoUrl);
61552
61605
  const ratio = logoImg.width / logoImg.height;
61553
61606
  const logoW = logoH * ratio;
61554
- const logoX = logoPad;
61555
- const logoY = (HEADER_H - logoH) / 2;
61556
- ctx.drawImage(logoImg, logoX, logoY, logoW, logoH);
61557
- if (config.brandName) {
61558
- textY = logoY + logoH + 14;
61559
- }
61607
+ const logoX = (width - logoW) / 2;
61608
+ ctx.drawImage(logoImg, logoX, cursorY, logoW, logoH);
61609
+ cursorY += logoH + 14;
61560
61610
  }
61561
61611
  catch {
61562
- // logo failed — render brand name centered as fallback
61612
+ // logo failed — continue without it
61563
61613
  }
61564
61614
  }
61565
61615
  if (config.brandName) {
61566
61616
  ctx.fillStyle = '#ffffff';
61567
- ctx.font = `bold 1.75rem ${FONT_STACK}`;
61617
+ ctx.font = `bold 1.625rem ${FONT_STACK}`;
61568
61618
  ctx.textAlign = 'center';
61569
- ctx.textBaseline = 'middle';
61570
- ctx.fillText(config.brandName, width / 2, textY, width - logoPad * 2);
61619
+ ctx.textBaseline = 'top';
61620
+ ctx.fillText(config.brandName, width / 2, cursorY, width - 48);
61621
+ cursorY += 32;
61622
+ }
61623
+ const metaParts = [];
61624
+ if (config.eventDate)
61625
+ metaParts.push(config.eventDate);
61626
+ if (config.eventLocation)
61627
+ metaParts.push(config.eventLocation);
61628
+ if (metaParts.length > 0) {
61629
+ ctx.fillStyle = 'rgba(255,255,255,0.78)';
61630
+ ctx.font = `0.875rem ${FONT_STACK}`;
61631
+ ctx.textAlign = 'center';
61632
+ ctx.textBaseline = 'top';
61633
+ ctx.fillText(metaParts.join(' · '), width / 2, cursorY, width - 48);
61571
61634
  }
61572
61635
  }
61573
61636
  async drawQrZone(ctx, config, width) {
@@ -61575,7 +61638,7 @@ class TicketCardImageService {
61575
61638
  ctx.fillStyle = '#ffffff';
61576
61639
  ctx.fillRect(0, zoneTop, width, QR_ZONE_H);
61577
61640
  const qrX = (width - QR_SIZE) / 2;
61578
- const qrY = zoneTop + (QR_ZONE_H - QR_SIZE - 28) / 2;
61641
+ const qrY = zoneTop + (QR_ZONE_H - QR_SIZE - 40) / 2;
61579
61642
  try {
61580
61643
  const qrImg = await this.loadImage(config.qrDataUrl);
61581
61644
  ctx.drawImage(qrImg, qrX, qrY, QR_SIZE, QR_SIZE);
@@ -61584,42 +61647,106 @@ class TicketCardImageService {
61584
61647
  ctx.strokeStyle = '#e0e0e0';
61585
61648
  ctx.strokeRect(qrX, qrY, QR_SIZE, QR_SIZE);
61586
61649
  }
61587
- ctx.fillStyle = '#2f2a36';
61588
- ctx.font = `0.9375rem ${FONT_STACK}`;
61650
+ const hint = config.scanHint ?? 'Muestra este QR en la puerta para canjear tu entrada.';
61651
+ ctx.fillStyle = '#6b6675';
61652
+ ctx.font = `0.875rem ${FONT_STACK}`;
61589
61653
  ctx.textAlign = 'center';
61590
61654
  ctx.textBaseline = 'top';
61591
- ctx.fillText('Escanea para verificar', width / 2, qrY + QR_SIZE + 12);
61655
+ const hintY = qrY + QR_SIZE + 14;
61656
+ this.fillWrappedText(ctx, hint, width / 2, hintY, width - 80, 22);
61657
+ }
61658
+ drawPerforated(ctx, config, width) {
61659
+ const y = HEADER_H + QR_ZONE_H;
61660
+ const notchR = PERF_H / 2;
61661
+ const colorFrom = config.brandColor ?? DEFAULT_BRAND_COLOR;
61662
+ ctx.fillStyle = '#ffffff';
61663
+ ctx.fillRect(0, y, width, PERF_H);
61664
+ // Left notch (semicircle cut from edge)
61665
+ ctx.fillStyle = '#f8f8f8';
61666
+ ctx.beginPath();
61667
+ ctx.arc(-1, y + notchR, notchR + 1, -Math.PI / 2, Math.PI / 2);
61668
+ ctx.fill();
61669
+ // Right notch
61670
+ ctx.beginPath();
61671
+ ctx.arc(width + 1, y + notchR, notchR + 1, Math.PI / 2, -Math.PI / 2);
61672
+ ctx.fill();
61673
+ // Dashed line in middle
61674
+ ctx.setLineDash([8, 6]);
61675
+ ctx.strokeStyle = colorFrom + '55';
61676
+ ctx.lineWidth = 1.5;
61677
+ ctx.beginPath();
61678
+ ctx.moveTo(notchR * 2 + 8, y + notchR);
61679
+ ctx.lineTo(width - notchR * 2 - 8, y + notchR);
61680
+ ctx.stroke();
61681
+ ctx.setLineDash([]);
61592
61682
  }
61593
61683
  drawFooter(ctx, config, width) {
61594
- const footerTop = HEADER_H + QR_ZONE_H;
61684
+ const footerTop = HEADER_H + QR_ZONE_H + PERF_H;
61685
+ const colorFrom = config.brandColor ?? DEFAULT_BRAND_COLOR;
61595
61686
  ctx.fillStyle = '#ffffff';
61596
61687
  ctx.fillRect(0, footerTop, width, FOOTER_H);
61597
- const colorFrom = config.brandColor ?? DEFAULT_BRAND_COLOR;
61598
- ctx.fillStyle = colorFrom;
61599
- ctx.fillRect(0, footerTop, width, 3);
61600
- let contentY = footerTop + 40;
61688
+ let contentY = footerTop + 32;
61601
61689
  if (config.folio !== undefined && config.folio !== null && config.folio !== '') {
61602
- const label = config.folioLabel ?? 'Entrada';
61603
- const folioText = `${label} #${config.folio}`;
61604
- ctx.fillStyle = '#1a0b2e';
61605
- ctx.font = `bold 3rem ${FONT_STACK}`;
61690
+ const label = (config.folioLabel ?? 'Cartón').toUpperCase();
61691
+ // Label small brand color, letter-spaced
61692
+ ctx.fillStyle = colorFrom;
61693
+ ctx.font = `600 0.8125rem ${FONT_STACK}`;
61694
+ ctx.textAlign = 'center';
61695
+ ctx.textBaseline = 'top';
61696
+ this.fillLetterSpaced(ctx, label, width / 2, contentY, 3);
61697
+ contentY += 24;
61698
+ // Folio number — large, bold, brand color
61699
+ ctx.fillStyle = colorFrom;
61700
+ ctx.font = `800 3.5rem ${FONT_STACK}`;
61606
61701
  ctx.textAlign = 'center';
61607
61702
  ctx.textBaseline = 'top';
61608
- ctx.fillText(folioText, width / 2, contentY, width - 40);
61609
- contentY += 56;
61703
+ ctx.fillText(`#${config.folio}`, width / 2, contentY, width - 40);
61704
+ contentY += 68;
61610
61705
  }
61611
61706
  if (config.buyerName) {
61612
- ctx.fillStyle = '#2f2a36';
61613
- ctx.font = `1.25rem ${FONT_STACK}`;
61707
+ ctx.fillStyle = '#6b6675';
61708
+ ctx.font = `0.9375rem ${FONT_STACK}`;
61614
61709
  ctx.textAlign = 'center';
61615
61710
  ctx.textBaseline = 'top';
61616
61711
  ctx.fillText(config.buyerName, width / 2, contentY, width - 40);
61617
61712
  }
61618
- ctx.fillStyle = '#6b6675';
61619
- ctx.font = `0.75rem ${FONT_STACK}`;
61620
- ctx.textAlign = 'right';
61713
+ // Copyright
61714
+ ctx.fillStyle = '#b0a8bc';
61715
+ ctx.font = `0.6875rem ${FONT_STACK}`;
61716
+ ctx.textAlign = 'center';
61621
61717
  ctx.textBaseline = 'bottom';
61622
- ctx.fillText('Valtech', width - 16, footerTop + FOOTER_H - 12);
61718
+ ctx.fillText('© 2026 Valtech. Todos los derechos reservados.', width / 2, footerTop + FOOTER_H - 14);
61719
+ }
61720
+ fillWrappedText(ctx, text, x, y, maxWidth, lineHeight) {
61721
+ const words = text.split(' ');
61722
+ let line = '';
61723
+ let curY = y;
61724
+ for (const word of words) {
61725
+ const test = line ? `${line} ${word}` : word;
61726
+ if (ctx.measureText(test).width > maxWidth && line) {
61727
+ ctx.fillText(line, x, curY);
61728
+ line = word;
61729
+ curY += lineHeight;
61730
+ }
61731
+ else {
61732
+ line = test;
61733
+ }
61734
+ }
61735
+ if (line)
61736
+ ctx.fillText(line, x, curY);
61737
+ }
61738
+ fillLetterSpaced(ctx, text, cx, y, spacing) {
61739
+ // Measure total width with spacing and center
61740
+ const chars = text.split('');
61741
+ const charWidths = chars.map(c => ctx.measureText(c).width);
61742
+ const totalW = charWidths.reduce((s, w) => s + w, 0) + spacing * (chars.length - 1);
61743
+ let x = cx - totalW / 2;
61744
+ ctx.textAlign = 'left';
61745
+ for (let i = 0; i < chars.length; i++) {
61746
+ ctx.fillText(chars[i], x, y);
61747
+ x += charWidths[i] + spacing;
61748
+ }
61749
+ ctx.textAlign = 'center';
61623
61750
  }
61624
61751
  loadImage(src) {
61625
61752
  return new Promise((resolve, reject) => {
@@ -70381,6 +70508,9 @@ class TicketCardComponent {
70381
70508
  folio: p.folio,
70382
70509
  folioLabel: p.shareFolioLabel,
70383
70510
  buyerName: p.buyerName,
70511
+ eventDate: p.shareEventDate,
70512
+ eventLocation: p.shareEventLocation,
70513
+ scanHint: p.shareScanHint,
70384
70514
  };
70385
70515
  }
70386
70516
  async copyToClipboard(config, p) {