bits-qris
v3.0.0
Published
QRIS Static → Dynamic converter — parse, validate, convert + cetak gambar struk. Hybrid terbaik: Core TLV presisi + Cetak Struk siap print.
Maintainers
Readme
🧭 Daftar Isi
| Menu | Deskripsi |
| -------------------------------------- | -------------------------------------- |
| ✨ Features | Fitur utama — parser, converter, struk |
| 📦 Instalasi | Cara pasang — npm/pnpm/bun/yarn |
| ⚡ Quick Start | Mulai cepat — modern & legacy API |
| 🖨️ Cetak Struk | Generate struk JPG + QR |
| 🌐 Web Demo | Demo — dark/light, install tabs |
| 💻 CLI | npx bits-qris interactive |
| 📚 API Reference | Core, Image, Shared |
✨ Features
| Feature | Description |
| ----------------------- | ----------------------------------------------------------------------------------- |
| 🔍 Parser TLV | Decode QRIS → QrisData — merchant, kota, MCC, currency, amount, fee (EMVCo) |
| 🔄 Static → Dynamic | Inject amount & fee (fixed/percentage), recalculate CRC16-CCITT 0x1021 |
| ✅ Validator | Cek 000201, length, CRC, 8 required tags + merchant 26–51 |
| 🖨️ Cetak Struk | Composite QR → qris-receipt-template.png (1080×1920) + overlay NMID/ID/nama/NNS |
| 🌐 Browser Ready | makeFile(...,{base64:true}) → DataURL, tanpa fs |
| 📦 Dual Build | dist/cjs + dist/esm + types — tree-shakeable |
| 💻 CLI | npx bits-qris interactive atau --convert --batch --validate --parse |
| 📱 PWA | Offline-capable, manifest + sw.js (Workbox), installable — dark/light |
| 🗂️ Aset Clean | kebab-case semantik (title-bebas-neue, body-roboto-large) |
🛠️ Tech Stack
| Layer | Technology |
| --------------- | ------------------------------------------------------------------------ |
| Language | TypeScript 6.0, Node 24 LTS |
| QR Generate | qrcode 1.5.4 |
| Image | jimp 1.6.1 |
| Web Demo | Hono 4.13, Vite 8, vite-plugin-pwa 1.3, @cloudflare/vite-plugin 1.54 |
| Build | tsc dual ESM/CJS, Wrangler 4.127 |
| Lint/Format | ESLint 10, Prettier 3, Husky 9, lint-staged 17, commitlint 21 |
| Deploy | Cloudflare Workers (qris.bits.co.id), GitHub Actions, semantic-release |
📁 Project Structure
bits-qris/
├── src/
│ ├── core/ # pure, no I/O
│ │ ├── constants.ts # TAG, REQUIRED_TAGS
│ │ ├── types.ts # TlvElement, QrisData
│ │ ├── crc16.ts
│ │ ├── parser.ts # parseTlv, parseQris
│ │ ├── converter.ts
│ │ ├── validator.ts
│ │ └── index.ts
│ ├── shared/
│ │ ├── errors.ts
│ │ └── format.ts
│ ├── image/
│ │ ├── types.ts
│ │ ├── merchant-info.ts
│ │ ├── qr-renderer.ts
│ │ ├── font-loader.ts # cached Jimp
│ │ ├── template-resolver.ts
│ │ ├── receipt-generator.ts
│ │ └── index.ts
│ ├── cli/
│ │ ├── constants.ts
│ │ ├── parser.ts
│ │ ├── commands.ts
│ │ └── interactive.ts
│ ├── index.ts
│ └── cli.ts
├── assets/
│ ├── images/qris-receipt-template.png
│ └── fonts/ # kebab-case semantic
│ ├── title-bebas-neue/
│ ├── title-bebas-neue-compact/
│ ├── body-roboto-large/
│ ├── body-roboto-medium/
│ └── caption-roboto-small/
├── apps/web/ # Hono + Vite + Cloudflare demo (qris.bits.co.id)
│ ├── src/client.tsx
│ ├── src/style.css
│ ├── vite.config.ts
│ └── wrangler.jsonc
├── examples/
├── dist/ # build (esm + cjs)
└── package.json📦 Instalasi
| Manager | Perintah |
| ----------------- | -------------------------------------------------- |
| npm | npm i bits-qris |
| pnpm | pnpm add bits-qris |
| bun | bun add bits-qris |
| yarn | yarn add bits-qris |
| tanpa install | npx bits-qris convert "000201..." --amount 50000 |
# cek instalasi
node -e "import('bits-qris').then(m=>console.log(Object.keys(m).slice(0,5)))"Requirements: Node.js
>=24(LTS) · ESM + CJS + Types ·qrcode1.5 +jimp1.6
⚡ Quick Start
Modern API (Direkomendasikan)
import { convertQris, parseQris, validateQris } from 'bits-qris';
const staticQris =
'00020101021126560014ID.CO.QRIS.WWW0115ID10231625260990215ID10231625260995204581253033605802ID5919BANTEN IT SOLUTIONS6006SERANG6304DA44';
// 1. Validasi dulu (opsional tapi disarankan)
const { valid, errors } = validateQris(staticQris);
if (!valid) console.error(errors);
// 2. Parse info merchant
const info = parseQris(staticQris);
console.log(info.merchantName); // BANTEN IT SOLUTIONS
console.log(info.merchantCity); // SERANG
console.log(info.method); // static
// 3. Convert → Dynamic
const dynamic = convertQris(staticQris, {
amount: 50_000,
fee: { type: 'fixed', value: 1000 }, // atau { type: 'percentage', value: 2.5 }
});
console.log(dynamic); // ...540550000...6304ABCD (CRC baru)Migrasi ke v3
Bentuk opsi lama tidak lagi didukung; gunakan opsi modern berikut:
nominal→amounttaxtype: 'r'→fee: { type: 'fixed', value: ... }fee: '1000'→fee: { type: 'fixed', value: 1000 }makeString→convertQris
🖨️ Cetak Struk
import { makeFile, makeQrDataUrl, getMerchantInfo } from 'bits-qris';
// 1. Simpan JPG struk (Node.js)
const path = await makeFile(staticQris, {
amount: 75_000,
fee: { type: 'percentage', value: 2 },
path: 'output/struk-75000.jpg', // default: output/<MERCHANT>-<timestamp>.jpg
});
console.log(path); // output/struk-75000.jpg
// 2. Base64 untuk API / <img src>
const base64 = await makeFile(staticQris, { amount: 75_000, base64: true });
// <img src={base64} /> // data:image/jpeg;base64,...
// 3. QR ringan tanpa template (tanpa Jimp, 5KB)
const qrDataUrl = await makeQrDataUrl(staticQris, { amount: 50_000 });
console.log(qrDataUrl.slice(0, 30)); // data:image/png;base64,iVBORw...
// 3b. QR sebagai SVG string (pure, Workers-safe — tanpa fs)
const svg = await makeQrSvg(dynamicQris);
console.log(svg.slice(0, 20)); // <svg xmlns="http://...
// 4. Info merchant untuk overlay kustom
const merchant = getMerchantInfo(staticQris);
console.log(merchant.nmid); // ID1023162526099
console.log(merchant.merchantName); // BANTEN IT SOLUTIONS
console.log(merchant.nns); // 8-char NNS
// 5. Template kustom
await makeFile(staticQris, {
amount: 50_000,
templatePath: 'assets/images/custom-template.png', // 1080x1920 PNG
});Di Browser
makeFileotomatis fallback keQR DataURL(template butuhfs). Selalu pakai{ base64: true }.
🌐 Browser
import { convertQris, validateQris } from 'bits-qris';
const dynamic = convertQris(qris, { amount: 100_000 });
console.log(validateQris(dynamic).valid); // true
import { makeFile } from 'bits-qris';
const dataUrl = await makeFile(qris, { amount: 100_000, base64: true });
document.querySelector<HTMLImageElement>('#qr')!.src = dataUrl;💻 CLI
# Interactive wizard (parse → amount → fee → struk)
npx bits-qris
# atau
npx bits-qris
# One-liner
npx bits-qris --validate "000201010211..."
# {"valid":true,"errors":[]}
npx bits-qris --parse "000201010211..."
# {"merchantName":"BANTEN IT SOLUTIONS", "merchantCity":"SERANG", ...}
npx bits-qris --convert "000201010211..." 50000
# 000201010212...540550000...6304ABCD
npx bits-qris --convert "000201010211..." 50000 --fee 1000 --type fixed --image output/struk.jpg
# 000201010212... + [image] Saved to: output/struk.jpg
npx bits-qris --convert "000201010211..." 50000 --base64
# [base64] data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ...
# Batch — satu QRIS per baris (skip baris kosong & komentar #), output JSONL
npx bits-qris --batch list.txt --amount 10000
# {"input":"...","valid":true,"dynamic":"..."}
# {"input":"...","valid":false,"error":"CRC mismatch: ..."}
# Batch + struk per baris valid → output/batch-<n>.jpg
npx bits-qris --batch list.txt --amount 10000 --fee 1000 --type fixed --image
npx bits-qris --help📦 Tambahkan ke package.json (biar npm run lebih singkat)
Copy ini ke package.json project kamu (Next.js / SvelteKit / Express):
{
"scripts": {
"qris": "bits-qris",
"qris:help": "bits-qris --help",
"qris:validate": "bits-qris --validate",
"qris:parse": "bits-qris --parse",
"qris:convert": "bits-qris --convert",
"qris:interactive": "bits-qris",
"qris:demo": "bits-qris --convert \"00020101021126560014ID.CO.QRIS.WWW0115ID10231625260990215ID10231625260995204581253033605802ID5919BANTEN IT SOLUTIONS6006SERANG6304DA44\" 25000 --image output/demo.jpg"
}
}Pakai dengan -- double-dash agar argumen diteruskan:
npm run qris:interactive
npm run qris:validate -- "000201010211..."
npm run qris:parse -- "000201010211..."
npm run qris:convert -- "000201010211..." 50000
npm run qris:convert -- "000201010211..." 50000 --fee 1000 --type fixed
npm run qris:convert -- "000201010211..." 50000 --fee 2.5 --type percentage --image output/struk.jpg --base64
npm run qris:demoTips: Untuk QRIS panjang, simpan di
.env→QRIS_STATIC="000201..."lalunpm run qris:convert -- "$QRIS_STATIC" 50000
📚 API Reference
Core
| Fungsi | Params | Return | Deskripsi |
| ------------------------- | ------------------------ | ----------------- | -------------------------------- |
| parseTlv(data) | string | TlvElement[] | Low-level TLV EMVCo |
| parseQris(qris) | string | QrisData | Parse struktur lengkap |
| validateQris(qris) | string | {valid, errors} | Validasi 8 required tags + CRC |
| isValidQris(qris) | string | boolean | Shortcut |
| calculateCrc16(str) | string | string | CRC16-CCITT 0x1021 |
| convertQris(qris, opts) | string, ConvertOptions | string | Static → Dynamic |
| makeString(qris, opts) | string, opts | string | Alias modern untuk convertQris |
| getMerchantInfo(qris) | string | MerchantInfo | NMID, printer, NNS |
type ConvertOptions = {
amount: number | string;
fee?: { type: 'fixed' | 'percentage'; value: number | string };
};
type QrisData = {
version: string;
method: 'static' | 'dynamic';
merchantAccountInfo: MerchantAccountInfo[];
merchantCategoryCode: string;
currency: string; // 360 = IDR
amount?: string;
countryCode: string;
merchantName: string;
merchantCity: string;
crc: string;
raw: TlvElement[];
};Image
| Fungsi | Params | Return |
| --------------------------- | --------------- | ------------------------------------ |
| makeFile(qris, opts) | ImageOptions | Promise<string> — path atau base64 |
| makeQrDataUrl(qris, opts) | QrOnlyOptions | Promise<string> DataURL |
| makeQrBuffer(qris, opts) | QrOnlyOptions | Promise<Buffer> |
type ImageOptions = ConvertOptions & {
base64?: boolean; // default false
path?: string;
templatePath?: string; // default assets/images/qris-receipt-template.png
};Shared
import {
QrisError,
QrisParseError,
QrisConvertError,
QrisImageError,
padLength,
sanitizeFilename,
} from 'bits-qris';🧩 Examples
# lihat folder examples/
node examples/basic.mjs # ESM modern
node examples/legacy.cjs # CJS legacy// examples/basic.mjs
import { convertQris, makeFile, parseQris } from 'bits-qris';
const QRIS =
'00020101021126560014ID.CO.QRIS.WWW0115ID10231625260990215ID10231625260995204581253033605802ID5919BANTEN IT SOLUTIONS6006SERANG6304DA44';
const dynamic = convertQris(QRIS, { amount: 25_000 });
console.log(parseQris(dynamic).amount); // 25000
const file = await makeFile(QRIS, { amount: 25_000 });
console.log('Struk:', file); // output/BANTEN_IT_SOLUTIONS-...jpg🏗️ Arsitektur
flowchart LR
subgraph Core["src/core/ — Pure JS, no I/O"]
A[parseTlv] --> B[validateQris]
B --> C[convertQris]
end
subgraph Image["src/image/ — Jimp"]
D[makeFile]
end
subgraph Web["apps/web/ — Hono + Vite + Workers"]
E[client.tsx<br/>vanilla TS UI]
F[index.ts<br/>/api/convert]
G[style.css<br/>design tokens]
end
subgraph CLI["src/cli/"]
H[bits-qris CLI]
end
C -->|Dynamic String| D
C -->|Dynamic String| F
F --> E
F --> G🛡️ Keamanan
| Item | Detail |
| --------------- | ------------------------------------------------------------------------------------------------------------ |
| CRC16-CCITT | 0x1021 init 0xFFFF — sesuai EMVCo |
| Validasi | 8 required tags + merchant 26–51 + CRC mismatch |
| Aman | [email protected] (0 vuln) — bukan 0.16.1 vulnerable |
| No eval | Tidak pernah eval / split("5802ID") fragile — parser TLV murni |
| Rate limit | /api/convert 30 req/menit per IP via CF-Connecting-IP (tak bisa dipalsukan klien); 429 + Retry-After |
🧪 Testing & Publish
npm run build # clean + build:esm + build:cjs + copy:assets
npx tsc --noEmit
npx eslint src --ext .ts
npm pack --dry-run # 230 files, 952KB
npm publish --access public🤝 Contributing
PR sangat diterima!
- Fork →
git checkout -b feat/keren npm ci && npm run buildnpm run lint && npx tsc --noEmit- Commit
feat: ...(commitlint) → Push → PR
Lihat CONTRIBUTING.md & SECURITY.md.
💝 Dukung BITS
Jika library ini membantu bisnismu, traktir kopi via Saweria — 100% untuk open-source 🇮🇩
📄 License
MIT License. See LICENSE.
