Files
2026-06-06 18:43:00 +08:00

103 lines
3.9 KiB
JavaScript

/**
* 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<dataURL> 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<string>} 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',
},
});
}