/** * Client-side QRPH / EMV QRCPS utilities. * * All operations run in the browser — no external API calls. * * Exports: * parseTlv(str) → { tag: value, … } * buildTlv(fields) → EMV TLV string (sorted, CRC appended) * injectAmount(qrphStr, amount) → new QRPH string with amount + fresh CRC * generateQrDataUrl(text, opts) → Promise via qrcode library */ // ── CRC-16 / CCITT-FALSE (polynomial 0x1021, seed 0xFFFF) ────────────────── function crc16(data) { let crc = 0xFFFF; for (let i = 0; i < data.length; i++) { crc ^= data.charCodeAt(i) << 8; for (let j = 0; j < 8; j++) { crc = (crc & 0x8000) ? ((crc << 1) ^ 0x1021) & 0xFFFF : (crc << 1) & 0xFFFF; } } return crc.toString(16).toUpperCase().padStart(4, '0'); } // ── TLV parser ────────────────────────────────────────────────────────────── export function parseTlv(data) { const fields = {}; let pos = 0; while (pos + 4 <= data.length) { const tag = data.substring(pos, pos + 2); const len = parseInt(data.substring(pos + 2, pos + 4), 10); const val = data.substring(pos + 4, pos + 4 + len); fields[tag] = val; pos += 4 + len; if (tag === '63') break; } return fields; } // ── TLV builder ───────────────────────────────────────────────────────────── // Reassembles fields in ascending tag order, appends a fresh CRC-16. function buildTlv(fields) { // Sort numerically, excluding CRC (will be appended fresh) const sorted = Object.keys(fields) .filter(t => t !== '63') .sort((a, b) => parseInt(a, 10) - parseInt(b, 10)); let body = ''; for (const tag of sorted) { const val = fields[tag]; const lpad = String(val.length).padStart(2, '0'); body += tag + lpad + val; } // Append CRC header then compute over the full string including header const withHeader = body + '6304'; return withHeader + crc16(withHeader); } // ── Amount injector ───────────────────────────────────────────────────────── /** * Returns a new QRPH string with field 54 (Transaction Amount) set to * the given amount and a freshly computed CRC. * * @param {string} qrphStr Base QRPH string (stored static QR) * @param {number} amount PHP amount, e.g. 500 * @returns {string} Modified QRPH string */ export function injectAmount(qrphStr, amount) { const fields = parseTlv(qrphStr); if (!amount || amount <= 0) { // Remove amount field and rebuild delete fields['54']; } else { // EMV spec: amount as decimal string, e.g. "500.00" fields['54'] = parseFloat(amount).toFixed(2); } return buildTlv(fields); } // ── QR image renderer ─────────────────────────────────────────────────────── /** * Renders a string as a QR code and returns a data URL (PNG). * * @param {string} text * @param {object} opts qrcode options (width, margin, color…) * @returns {Promise} data URL */ export async function generateQrDataUrl(text, opts = {}) { const QRCode = (await import('qrcode')).default; return QRCode.toDataURL(text, { width: opts.width ?? 260, margin: opts.margin ?? 2, errorCorrectionLevel: opts.ecl ?? 'M', color: { dark: opts.dark ?? '#000000', light: opts.light ?? '#FFFFFF', }, }); }