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.
- package/esm2022/lib/components/molecules/ticket-card/ticket-card.component.mjs +4 -1
- package/esm2022/lib/components/molecules/ticket-card/types.mjs +1 -1
- package/esm2022/lib/services/app-version/app-version.service.mjs +57 -6
- package/esm2022/lib/services/ticket-card-image/ticket-card-image.service.mjs +117 -41
- package/esm2022/lib/services/ticket-card-image/types.mjs +1 -1
- package/esm2022/lib/version.mjs +2 -2
- package/fesm2022/valtech-components.mjs +175 -45
- package/fesm2022/valtech-components.mjs.map +1 -1
- package/lib/components/molecules/ticket-card/types.d.ts +7 -1
- package/lib/services/app-version/app-version.service.d.ts +8 -0
- package/lib/services/ticket-card-image/ticket-card-image.service.d.ts +3 -0
- package/lib/services/ticket-card-image/types.d.ts +6 -0
- package/lib/version.d.ts +1 -1
- package/package.json +1 -1
|
@@ -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.
|
|
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, {
|
|
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, {
|
|
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
|
-
|
|
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
|
|
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 =
|
|
61469
|
-
const QR_ZONE_H =
|
|
61470
|
-
const
|
|
61471
|
-
const
|
|
61472
|
-
const
|
|
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 =
|
|
61547
|
-
const
|
|
61548
|
-
let
|
|
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 =
|
|
61555
|
-
|
|
61556
|
-
|
|
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 —
|
|
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.
|
|
61617
|
+
ctx.font = `bold 1.625rem ${FONT_STACK}`;
|
|
61568
61618
|
ctx.textAlign = 'center';
|
|
61569
|
-
ctx.textBaseline = '
|
|
61570
|
-
ctx.fillText(config.brandName, width / 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 -
|
|
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
|
-
|
|
61588
|
-
ctx.
|
|
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
|
-
|
|
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
|
-
|
|
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 ?? '
|
|
61603
|
-
|
|
61604
|
-
ctx.fillStyle =
|
|
61605
|
-
ctx.font = `
|
|
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(
|
|
61609
|
-
contentY +=
|
|
61703
|
+
ctx.fillText(`#${config.folio}`, width / 2, contentY, width - 40);
|
|
61704
|
+
contentY += 68;
|
|
61610
61705
|
}
|
|
61611
61706
|
if (config.buyerName) {
|
|
61612
|
-
ctx.fillStyle = '#
|
|
61613
|
-
ctx.font = `
|
|
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
|
-
|
|
61619
|
-
ctx.
|
|
61620
|
-
ctx.
|
|
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
|
|
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) {
|