initial: bootstrap from BukidBountyApp base
This commit is contained in:
102
resources/js/composables/useQrph.js
Normal file
102
resources/js/composables/useQrph.js
Normal file
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* 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',
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user