Skip to content

Instantly share code, notes, and snippets.

@natsu90
Forked from chengkiang/paynow.js
Last active April 11, 2025 16:15
Show Gist options
  • Save natsu90/f45dc88b38a037325ad9095163b82b42 to your computer and use it in GitHub Desktop.
Save natsu90/f45dc88b38a037325ad9095163b82b42 to your computer and use it in GitHub Desktop.
MY DuitNow QR Code Generator Sample
String.prototype.padLeft = function (n, str) {
if (n < String(this).length) {
return this.toString();
}
else {
return Array(n - String(this).length + 1).join(str || '0') + this;
}
}
function crc16(s) {
var crcTable = [0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5,
0x60c6, 0x70e7, 0x8108, 0x9129, 0xa14a, 0xb16b,
0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, 0x1231, 0x0210,
0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c,
0xf3ff, 0xe3de, 0x2462, 0x3443, 0x0420, 0x1401,
0x64e6, 0x74c7, 0x44a4, 0x5485, 0xa56a, 0xb54b,
0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6,
0x5695, 0x46b4, 0xb75b, 0xa77a, 0x9719, 0x8738,
0xf7df, 0xe7fe, 0xd79d, 0xc7bc, 0x48c4, 0x58e5,
0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823,
0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969,
0xa90a, 0xb92b, 0x5af5, 0x4ad4, 0x7ab7, 0x6a96,
0x1a71, 0x0a50, 0x3a33, 0x2a12, 0xdbfd, 0xcbdc,
0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03,
0x0c60, 0x1c41, 0xedae, 0xfd8f, 0xcdec, 0xddcd,
0xad2a, 0xbd0b, 0x8d68, 0x9d49, 0x7e97, 0x6eb6,
0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70,
0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a,
0x9f59, 0x8f78, 0x9188, 0x81a9, 0xb1ca, 0xa1eb,
0xd10c, 0xc12d, 0xf14e, 0xe16f, 0x1080, 0x00a1,
0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c,
0xe37f, 0xf35e, 0x02b1, 0x1290, 0x22f3, 0x32d2,
0x4235, 0x5214, 0x6277, 0x7256, 0xb5ea, 0xa5cb,
0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447,
0x5424, 0x4405, 0xa7db, 0xb7fa, 0x8799, 0x97b8,
0xe75f, 0xf77e, 0xc71d, 0xd73c, 0x26d3, 0x36f2,
0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9,
0xb98a, 0xa9ab, 0x5844, 0x4865, 0x7806, 0x6827,
0x18c0, 0x08e1, 0x3882, 0x28a3, 0xcb7d, 0xdb5c,
0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0,
0x2ab3, 0x3a92, 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d,
0xbdaa, 0xad8b, 0x9de8, 0x8dc9, 0x7c26, 0x6c07,
0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1,
0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba,
0x8fd9, 0x9ff8, 0x6e17, 0x7e36, 0x4e55, 0x5e74,
0x2e93, 0x3eb2, 0x0ed1, 0x1ef0];
var crc = 0xFFFF;
var j, i;
for (i = 0; i < s.length; i++) {
c = s.charCodeAt(i);
if (c > 255) {
throw new RangeError();
}
j = (c ^ (crc >> 8)) & 0xFF;
crc = crcTable[j] ^ (crc << 8);
}
return ((crc ^ 0) & 0xFFFF).toString(16).toUpperCase().padStart(4, '0');
}
function generateDuitNowStr( opts ) {
let p = [
{ id: '00', value: '02' }, // ID 00: Payload Format Indicator, PayNow is using 01, but DuitNow is using 02
{
id: '26', value: // ID 26: Merchant Account Info Template
[
{ id: '00', value: 'A0000006150001' }, // DuitNow Application Identifier
{ id: '01', value: opts.app || '588734' },// unknown value, 588734 is from MAE app
{ id: '02', value: opts.account } // unknown Account Number format
]
},
{ id: '52', value: opts.category || '0000' }, // ID 52: Merchant Category Code
{ id: '53', value: '458' }, // ID 53: Currency. MYR is 458
{ id: '58', value: 'MY' }, // ID 58: 2-letter Country Code (MY)
{ id: '59', value: opts.name ? opts.name.substring(0, 25) : 'NA' }, // ID 59: Merchant Name
{ id: '60', value: opts.city ? opts.city.substring(0, 15) : 'MY' } // ID 60: Merchant City
]
// add Merchant Postcode
if (opts.postcode) {
p.push({ id: '61', value: opts.postcode })
}
// add Amount if any
if (opts.amount) {
p.push({ id: '01', value: '12' }) // ID 01: Point of Initiation Method 11: static, 12: dynamic
p.push({ id: '54', value: opts.amount.toFixed(2) }) // ID 54: Transaction Amount
} else {
p.push({ id: '01', value: '11' }) // ID 01: Point of Initiation Method 11: static, 12: dynamic
}
// add Expiry Datetime
if (opts.expiry) {
p[p.findIndex(a => a.id == '26')].value.push({ id: '03', value: opts.expiry }) // Expiry datetime in Unix time in miliseconds
}
// add Ref Numbers
if (opts.ref || opts.ref3 || opts.ref5 || opts.ref6 || opts.ref7 || opts.ref8) {
let refObj = p.find(x => x.id === '62')
if (!refObj) {
refObj = { id: '62', value: []}
}
if (opts.ref) {
refObj.value.push({ id: '01', value: opts.ref }) // ID 01: Bill Number
}
if (opts.ref3) {
refObj.value.push({ id: '03', value: opts.ref3 }) // ID 03: Store Label
}
if (opts.ref5) {
refObj.value.push({ id: '05', value: opts.ref5 }) // ID 05: Reference Label
}
if (opts.ref6) {
refObj.value.push({ id: '06', value: opts.ref6 }) // ID 06: Customer Label
}
if (opts.ref7) {
refObj.value.push({ id: '07', value: opts.ref7 }) // ID 07: Terminal Label
}
if (opts.ref8) {
refObj.value.push({ id: '08', value: opts.ref8 }) // ID 08: Purpose of Transaction
}
p.push(refObj)
}
// add Extra Ref Number
if (opts.ref82) {
p.push({ id: '82', value: opts.ref82 })
}
// sorting object by ID
p.map((q) => Array.isArray(q.value) ? q.value.sort((a, b) => a.id.localeCompare(b.id)) : q)
p.sort((a, b) => a.id.localeCompare(b.id))
// start generating QR string
let str = p.reduce((final, current) => {
if (Array.isArray(current.value)) { //nest loop
current.value = current.value.reduce((f, c) => {
f += c.id + c.value.length.toString().padLeft(2) + c.value;
return f
}, "")
}
final += current.id + current.value.length.toString().padLeft(2) + current.value;
return final
}, "")
// Here we add "6304" to the previous string
// ID 63 (Checksum) 04 (4 characters)
// Do a CRC16 of the whole string including the "6304"
// then append it to the end.
str += '6304' + crc16(str + '6304');
return str;
}
module.exports = {
generateDuitNowStr
}
{
"version": "1.0.0",
"name": "duitnow-js",
"main": "duitnow.js"
}
@bilogic
Copy link

bilogic commented Apr 11, 2025

Hi. I am currently trying to use your code to produce a Dynamic DuitNow QR but failed. It seems that every Dynamic DuitNow QR generated will expire after 60 sec (and this 60 sec is not set in QR code).

Does that mean if I scan it within 60 secs, the payment will go through?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment