103 lines
3.9 KiB
JavaScript
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',
|
|
},
|
|
});
|
|
}
|