From 8bc244855c4f3b31c64c1d1a6436dda8b6684a7b Mon Sep 17 00:00:00 2001 From: Milton Gonzalez Date: Thu, 21 May 2026 22:07:22 -0500 Subject: [PATCH] Add FirmaEC signature style and bump to 1.2.17 --- CHANGELOG.md | 6 +- DEPLOY_ANDROID.md | 2 +- DEPLOY_WEB.md | 2 +- PRODUCCION.md | 2 +- VERSION | 2 +- android/app/build.gradle | 4 +- backend/package.json | 2 +- .../src/modules/signing/signing.service.js | 83 ++++++++++++++++++- backend/src/modules/users/users.service.js | 2 +- frontend/package.json | 2 +- .../components/ui/SignatureStylePicker.jsx | 59 ++++++++++++- frontend/src/index.css | 49 +++++++++++ 12 files changed, 199 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2f69c6..ef96eb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,14 @@ ## Versión actual -- Versión base: `1.2.16` +- Versión base: `1.2.17` ## Historial +### 1.2.17 - 2026-05-21 + +- Web y backend agregan el estilo de firma `FirmaEC`, con vista previa configurable y sello visible con QR de validación. + ### 1.2.16 - 2026-04-27 - Web aplica separación explícita entre `Certificado digital`, `Mostrar detalles` y `Elegir posición de firma` en `Firmar ahora`. diff --git a/DEPLOY_ANDROID.md b/DEPLOY_ANDROID.md index 3cc837c..22d0797 100644 --- a/DEPLOY_ANDROID.md +++ b/DEPLOY_ANDROID.md @@ -2,7 +2,7 @@ ## Versión actual -- Base actual: `1.2.16` +- Base actual: `1.2.17` - Fuente de verdad: [VERSION](/home/mgonzalez/desarrollo/react/linkedsign/VERSION) - Historial de cambios: [CHANGELOG.md](/home/mgonzalez/desarrollo/react/linkedsign/CHANGELOG.md) diff --git a/DEPLOY_WEB.md b/DEPLOY_WEB.md index a5db0d4..cde0e3d 100644 --- a/DEPLOY_WEB.md +++ b/DEPLOY_WEB.md @@ -2,7 +2,7 @@ ## Versión actual -- Base actual: `1.2.16` +- Base actual: `1.2.17` - Fuente de verdad: [VERSION](/home/mgonzalez/desarrollo/react/linkedsign/VERSION) - Historial de cambios: [CHANGELOG.md](/home/mgonzalez/desarrollo/react/linkedsign/CHANGELOG.md) diff --git a/PRODUCCION.md b/PRODUCCION.md index b7e95be..e358e9b 100644 --- a/PRODUCCION.md +++ b/PRODUCCION.md @@ -2,7 +2,7 @@ ## Versión actual -- Base actual: `1.2.16` +- Base actual: `1.2.17` - Fuente de verdad: [VERSION](/home/mgonzalez/desarrollo/react/linkedsign/VERSION) - Historial de cambios: [CHANGELOG.md](/home/mgonzalez/desarrollo/react/linkedsign/CHANGELOG.md) diff --git a/VERSION b/VERSION index f69752a..b66183a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.2.16 +1.2.17 diff --git a/android/app/build.gradle b/android/app/build.gradle index eb72904..a43e7e3 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -18,8 +18,8 @@ android { applicationId "net.linkedcorp.linkedsign" minSdk 26 targetSdk 35 - versionCode 10216 - versionName "1.2.16" + versionCode 10217 + versionName "1.2.17" buildConfigField "String", "API_BASE_URL", "\"${apiBaseUrl}\"" } diff --git a/backend/package.json b/backend/package.json index f9472ae..d05ec97 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "linkedsign-backend", - "version": "1.2.16", + "version": "1.2.17", "private": true, "description": "Initial backend for LinkedSign", "main": "src/server.js", diff --git a/backend/src/modules/signing/signing.service.js b/backend/src/modules/signing/signing.service.js index 0fcd0f6..3a6b7e0 100644 --- a/backend/src/modules/signing/signing.service.js +++ b/backend/src/modules/signing/signing.service.js @@ -33,10 +33,30 @@ function truncate(text, font, size, maxWidth) { } function getSignatureWidth(style, signerName = '') { - if (style !== 'qr') return 200; - const normalizedName = String(signerName).trim(); - const calculatedWidth = 78 + (normalizedName.length * 4.9); - return Math.max(170, Math.min(270, Math.round(calculatedWidth))); + if (style === 'qr' || style === 'firmaec') { + const normalizedName = String(signerName).trim(); + const calculatedWidth = 78 + (normalizedName.length * 4.9); + return Math.max(170, Math.min(270, Math.round(calculatedWidth))); + } + return 200; +} + +function wrapToLines(text, font, size, maxWidth, maxLines) { + const words = text.split(' '); + const lines = []; + let current = ''; + for (const word of words) { + if (lines.length >= maxLines) break; + const test = current ? `${current} ${word}` : word; + if (font.widthOfTextAtSize(test, size) <= maxWidth) { + current = test; + } else { + if (current) lines.push(current); + current = truncate(word, font, size, maxWidth); + } + } + if (current && lines.length < maxLines) lines.push(current); + return lines; } function buildLocalizedSignatureTimestamp({ locale, timeZone } = {}) { @@ -240,6 +260,60 @@ async function drawStyleImage(pdfDoc, page, sx, sy, sw, sh, { signerName, reason if (location) page.drawText(truncate(location, f2, 6, textW), { x: textX, y: sy + sh - 59, size: 6, font: f2, color: C.lightGray }); } +// ── Style: FIRMAEC (QR izquierda + texto monoespaciado estilo FirmaEC Ecuador) ─ +async function drawStyleFirmaEC(pdfDoc, page, sx, sy, sw, sh, { signerName, verificationUrl, qrPosition, qrBackground, qrBorder }) { + const fBold = await pdfDoc.embedFont(StandardFonts.CourierBold); + const fReg = await pdfDoc.embedFont(StandardFonts.Courier); + const useSoftBackground = qrBackground === 'soft'; + const useBorder = qrBorder === 'line'; + const qrOnRight = qrPosition === 'right'; + + const qrContent = verificationUrl || [ + 'FIRMA DIGITAL VERIFICADA', + `Firmante: ${signerName}`, + ].join('\n'); + + const qrPng = await QRCode.toBuffer(qrContent, { + type: 'png', + width: 120, + margin: 1, + color: { dark: '#000000', light: '#ffffff' }, + }); + const qrImage = await pdfDoc.embedPng(qrPng); + const qrSize = Math.min(sh - 10, 50); + + // Background + const bgColor = useSoftBackground ? C.bgBlue : C.white; + page.drawRectangle({ x: sx, y: sy, width: sw, height: sh, color: bgColor }); + const borderColor = useBorder ? rgb(0.15, 0.15, 0.15) : rgb(0.70, 0.70, 0.72); + const borderWidth = useBorder ? 0.85 : 0.5; + page.drawRectangle({ x: sx, y: sy, width: sw, height: sh, borderColor, borderWidth }); + + // QR image + const qrX = qrOnRight ? sx + sw - qrSize - 4 : sx + 4; + const qrY = sy + (sh - qrSize) / 2; + page.drawImage(qrImage, { x: qrX, y: qrY, width: qrSize, height: qrSize }); + + // Vertical divider + const divX = qrOnRight ? qrX - 4 : sx + qrSize + 8; + page.drawLine({ start: { x: divX, y: sy + 6 }, end: { x: divX, y: sy + sh - 6 }, thickness: 0.5, color: rgb(0.55, 0.55, 0.58) }); + + // Text column + const textX = qrOnRight ? sx + 6 : divX + 5; + const textW = sw - qrSize - 18; + + page.drawText('Validar unicamente en FirmaEC.', { x: textX, y: sy + sh - 12, size: 6, font: fReg, color: rgb(0.12, 0.12, 0.14) }); + page.drawText('Firmado electronicamente por:', { x: textX, y: sy + sh - 21, size: 6, font: fReg, color: rgb(0.12, 0.12, 0.14) }); + + // Name — bold, uppercase, word-wrapped up to 2 lines + const nameUpper = signerName.toUpperCase(); + const nameSize = 8.5; + const nameLines = wrapToLines(nameUpper, fBold, nameSize, textW, 2); + nameLines.forEach((line, i) => { + page.drawText(line, { x: textX, y: sy + sh - 34 - (i * 13), size: nameSize, font: fBold, color: rgb(0.05, 0.05, 0.07) }); + }); +} + // ── Main sign function ───────────────────────────────────────────────────────── const signPdf = async ({ pdfPath, p12Buffer, p12Password, @@ -300,6 +374,7 @@ const signPdf = async ({ if (style === 'qr') await drawStyleQR(pdfDoc, tp, sx, sy, sw, sh, info); else if (style === 'professional') await drawStyleProfessional(pdfDoc, tp, sx, sy, sw, sh, info); else if (style === 'image') await drawStyleImage(pdfDoc, tp, sx, sy, sw, sh, info); + else if (style === 'firmaec') await drawStyleFirmaEC(pdfDoc, tp, sx, sy, sw, sh, info); else await drawStyleClassic(pdfDoc, tp, sx, sy, sw, sh, info); // ── Save page with visual stamp ─────────────────────────────────── diff --git a/backend/src/modules/users/users.service.js b/backend/src/modules/users/users.service.js index cddeee7..45ecc99 100644 --- a/backend/src/modules/users/users.service.js +++ b/backend/src/modules/users/users.service.js @@ -1,6 +1,6 @@ const usersRepository = require('./users.repository'); -const ALLOWED_SIGNATURE_STYLES = ['classic', 'qr', 'professional']; +const ALLOWED_SIGNATURE_STYLES = ['classic', 'qr', 'professional', 'firmaec']; const ALLOWED_QR_POSITIONS = ['left', 'right']; const ALLOWED_QR_BACKGROUNDS = ['transparent', 'soft']; const ALLOWED_QR_BORDERS = ['none', 'line']; diff --git a/frontend/package.json b/frontend/package.json index 9e7fdd7..bf17555 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "1.2.16", + "version": "1.2.17", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/components/ui/SignatureStylePicker.jsx b/frontend/src/components/ui/SignatureStylePicker.jsx index 8bc0a23..9532c4c 100644 --- a/frontend/src/components/ui/SignatureStylePicker.jsx +++ b/frontend/src/components/ui/SignatureStylePicker.jsx @@ -1,4 +1,4 @@ -import { QrCode, Briefcase, Stamp, PanelsLeftBottom, PanelsRightBottom, CircleOff, Square } from 'lucide-react'; +import { QrCode, Briefcase, Stamp, PanelsLeftBottom, PanelsRightBottom, CircleOff, Square, ShieldCheck } from 'lucide-react'; const getQrPreviewWidth = (name = '') => { const extraWidth = Math.max(0, name.trim().length - 18) * 4; @@ -84,6 +84,61 @@ const STYLES = [ ), }, + { + id: 'firmaec', + label: 'FirmaEC', + icon: ShieldCheck, + desc: 'Formato oficial FirmaEC: QR de validación + nombre en negrita', + preview: ({ name, qrPosition = 'left', qrBackground = 'transparent', qrBorder = 'none' }) => ( +
+ {qrPosition === 'left' && ( + <> +
+ + {[0,1,2,3,4,5,6,7,8,9].map(r => + [0,1,2,3,4,5,6,7,8,9].map(c => { + const v = ((r * 3 + c * 7 + r ^ c) % 3 === 0); + return v ? : null; + }) + )} + + + + +
+
+ + )} +
+ Validar únicamente en FirmaEC. + Firmado electrónicamente por: + {name.toUpperCase()} +
+ {qrPosition === 'right' && ( + <> +
+
+ + {[0,1,2,3,4,5,6,7,8,9].map(r => + [0,1,2,3,4,5,6,7,8,9].map(c => { + const v = ((r * 3 + c * 7 + r ^ c) % 3 === 0); + return v ? : null; + }) + )} + + + + +
+ + )} +
+ ), + }, { id: 'professional', label: 'Profesional', @@ -145,7 +200,7 @@ export default function SignatureStylePicker({ })}

{s.desc}

- {s.id === 'qr' && isActive && ( + {(s.id === 'qr' || s.id === 'firmaec') && isActive && (