@poppackage/qris-payment-sdk
v1.3.3
Published
SDK JavaScript/TypeScript ringan untuk integrasi pembayaran QRIS melalui gateway Poppay. Mendukung tampilan **inline iframe** (di halaman utama) atau **popup**, dengan minimal konfigurasi dan data dinamis.
Keywords
Readme
QRIS Payment SDK
SDK JavaScript/TypeScript ringan untuk integrasi pembayaran QRIS melalui gateway Poppay. Mendukung tampilan inline iframe (di halaman utama) atau popup, dengan minimal konfigurasi dan data dinamis.
✨ Fitur
- 🚀 Ringan: Tanpa dependensi berat.
- 📦 Multi-Format: Mendukung ESM (NPM) dan UMD (CDN).
- 🛠 Dinamis: Data transaksi bisa diinput dari sisi client.
- 🔗 Auto-Inquiry: Sudah termasuk logika pengecekan status pembayaran otomatis.
- 🖼 Inline iframe: UI pembayaran bisa di-embed langsung di halaman (tanpa popup blocker).
⚙️ Instalasi
Menggunakan NPM (Next.js, React, Vue, Laravel Vite)
npm i @poppackage/qris-payment-sdk
Menggunakan CDN (Laravel Blade, HTML Statis)
Tambahkan script ini sebelum penutup tag </body>:
<script src="https://unpkg.com/@poppackage/qris-payment-sdk/dist/qris-sdk.umd.js"></script>
🚀 Cara Penggunaan
Parameter wajib: amount, invoice (string unik per transaksi/order dari sistem Anda), dan username (pengenal pembeli/transaksi di sistem Anda—misalnya user login, guest token, atau ID member). Nilai invoice dipakai agar gateway dan alur onSuccess dapat mengaitkan pembayaran QRIS dengan baris order yang benar; username dipakai gateway untuk lookup transaksi yang sudah ada (check-payment) sebelum membuat QR baru. Jangan memakai placeholder statis di production.
Mode tampilan: inline vs popup
| Mode | Config | Perilaku |
| --- | --- | --- |
| inline (disarankan) | displayMode: 'inline' + containerId | SDK inject <iframe> ke elemen #containerId di halaman Anda. Tidak kena popup blocker. |
| popup (default) | displayMode: 'popup' atau tidak diisi | Membuka jendela popup terpisah seperti sebelumnya. |
1. Penggunaan inline iframe (ESM)
UI pembayaran tampil langsung di halaman checkout:
import { QrisSDK } from '@poppackage/qris-payment-sdk';
// Buat instance saat halaman/component siap, bukan hanya di dalam handler klik.
// Dengan begitu health check bisa selesai lebih awal dan onUnavailable bisa menyembunyikan tombol.
const qris = new QrisSDK({
amount: 50000,
invoice: 'INV-1001',
username: 'user-123',
displayMode: 'inline',
containerId: 'qris-payment-frame', // wajib untuk inline
resultContainerId: 'payment-result', // opsional: banner sukses setelah bayar
onSuccess: (data) => {
console.log('Payment success:', data);
},
onFailed: (status) => {
console.log('Payment failed:', status);
},
onUnavailable: (health) => {
console.log('Payment unavailable:', health);
document.getElementById('pay-button')?.remove();
}
});
const handlePay = () => {
qris.openPayment();
};HTML yang diperlukan:
<div id="qris-payment-frame"></div>
<div id="payment-result"></div>
<button id="pay-button">Bayar Sekarang</button>2. Penggunaan di Framework Modern — popup (ESM)
Jika Anda menggunakan bundler seperti Vite atau Webpack:
import { QrisSDK } from '@poppackage/qris-payment-sdk';
// Buat instance lebih awal agar health check domain berjalan sebelum user klik tombol.
const qris = new QrisSDK({
amount: 50000,
invoice: 'INV-1001',
username: 'user-123',
resultContainerId: 'payment-result',
onSuccess: (data) => {
console.log('Payment success:', data);
// Handle success, misal redirect atau tampilkan notifikasi
},
onFailed: (status) => {
console.log('Payment failed:', status);
// Handle failure
},
onUnavailable: (health) => {
console.log('Payment unavailable:', health);
// Contoh: sembunyikan tombol jika domain belum terdaftar / dinonaktifkan
document.getElementById('pay-button')?.remove();
}
});
// Panggil fungsi ini pada event click tombol
const handlePay = () => {
qris.openPayment();
};
3. Penggunaan di Laravel / Vanilla JS (UMD)
Setelah memanggil script dari CDN, class QrisSDK akan tersedia di objek window.
<!-- Container iframe pembayaran (inline) -->
<div id="qris-payment-frame"></div>
<!-- Container hasil sukses (opsional) -->
<div id="payment-result"></div>
<button id="pay-button">Bayar Sekarang</button>
<script>
const btn = document.getElementById('pay-button');
// Buat instance saat script halaman berjalan agar onUnavailable bisa menghilangkan tombol sebelum diklik.
const qris = new QrisSDK({
amount: 15000,
invoice: 'INV-1002',
username: 'guest-abc',
displayMode: 'inline',
containerId: 'qris-payment-frame',
resultContainerId: 'payment-result',
onSuccess: (data) => {
console.log('Payment success:', data);
},
onFailed: (status) => {
console.log('Payment failed:', status);
},
onUnavailable: (health) => {
console.log('Payment unavailable:', health);
btn.remove();
}
});
btn.onclick = () => {
qris.openPayment();
};
</script>
Untuk mode popup, cukup hilangkan displayMode dan containerId (atau set displayMode: 'popup').
📌 USE CASES
Di semua skenario di bawah, invoice dan username wajib dikirim ke new QrisSDK({ ... }). invoice biasanya sama dengan nomor invoice/order di database Anda; username mengidentifikasi pembeli atau sesi transaksi (user login, guest token, dll.). Backend pada onSuccess harus memverifikasi invoice tersebut (bukan hanya mempercayai isi body dari browser).
SDK otomatis menjalankan health check domain ke server saat instance dibuat. Jika domain tidak terdaftar, sudah dinonaktifkan, atau konfigurasi merchant belum lengkap, openPayment() tidak akan membuat transaksi. Gunakan onUnavailable untuk menyembunyikan tombol bayar atau mengganti UI:
var btn = document.getElementById('pay-button');
var qris = new QrisSDK({
amount: 15000,
invoice: 'INV-1002',
username: 'guest-abc',
onUnavailable: function (health) {
console.log('QRIS tidak tersedia:', health);
btn.remove(); // atau btn.hidden = true
},
onFailed: function (status) {
console.log('Pembayaran gagal / timeout:', status);
}
});onUnavailable berbeda dari onFailed: onUnavailable terjadi sebelum transaksi dibuat, sedangkan onFailed terjadi setelah UI pembayaran berjalan tetapi pembayaran gagal/timeout.
Use case: tombol bayar hilang jika domain tidak tersedia
Gunakan pola ini jika tombol QRIS hanya boleh muncul pada domain yang sudah didaftarkan dan aktif. Kuncinya: buat instance QrisSDK saat halaman selesai load, jangan menunggu klik tombol. SDK akan memulai health check lebih awal; jika gagal, onUnavailable dipanggil dan tombol bisa langsung dihapus.
<div id="qris-payment-frame"></div>
<div id="payment-result"></div>
<button id="pay-button">Bayar QRIS PopPay</button>
<script src="https://unpkg.com/@poppackage/qris-payment-sdk/dist/qris-sdk.umd.js"></script>
<script>
const payButton = document.getElementById('pay-button');
const result = document.getElementById('payment-result');
const qris = new QrisSDK({
amount: 15000,
invoice: 'INV-1002',
username: 'guest-abc',
displayMode: 'inline',
containerId: 'qris-payment-frame',
resultContainerId: 'payment-result',
onUnavailable: function (health) {
payButton.remove();
result.innerHTML = '<p>Pembayaran QRIS tidak tersedia: ' + (health.message || 'Domain tidak valid') + '</p>';
},
onSuccess: function (data) {
console.log('Pembayaran berhasil:', data);
},
onFailed: function (status) {
console.log('Pembayaran gagal / timeout:', status);
}
});
payButton.addEventListener('click', function () {
qris.openPayment();
});
</script>Jika nominal, invoice, atau username bisa berubah dari input user, buat ulang instance saat klik dengan config terbaru. Tetap buat satu instance awal dengan onUnavailable untuk mendaftarkan callback health sejak halaman load.
1. WordPress
WordPress tidak menyediakan “plugin resmi” untuk SDK ini; integrasi dilakukan dengan muat script UMD, elemen HTML di template/shortcode, dan titik backend (AJAX atau REST API) bila Anda perlu memperbarui data di database (misalnya status pesanan) setelah onSuccess. Pola di bawah berlaku universal untuk tema custom, child theme, atau plugin buatan sendiri—bukan terikat satu plugin toko tertentu.
Prasyarat
- Domain situs production sudah didaftarkan ke admin PopPay (sama seperti penggunaan di stack lain).
- Pastikan halaman pembayaran memuat variabel global
ajaxurlbila memakaiadmin-ajax.php(banyak tema menambahkan ini diwp_head; jika belum ada, gunakanadmin_url('admin-ajax.php')diwp_localize_scriptatau atributdata-*pada elemen HTML).
Pola integrasi universal (ringkas)
- Muat SDK dari CDN hanya di halaman yang relevan (checkout, thank-you, atau halaman custom), agar performa tetap ringan:
- Di child theme
functions.php: hookwp_enqueue_scripts, cek kondisi halaman (is_page(),is_checkout()WooCommerce, dll.), laluwp_enqueue_scriptdengan URLhttps://unpkg.com/@poppackage/qris-payment-sdk/dist/qris-sdk.umd.js(atau salin URL yang sama ke tag<script>di template jika tidak memakai enqueue).
- Di child theme
- Output HTML di template halaman tersebut (atau lewat shortcode):
- Satu elemen untuk
resultContainerId(misalnya<div id="payment-result"></div>). - Satu tombol yang memicu
qris.openPayment().
- Satu elemen untuk
- Data dinamis dari PHP (disarankan):
- Nominal (
amount),invoice(wajib),username(wajib), catatan (notes), nama/email pembeli (payor_name,payor_email) diisi dari meta post, post meta order, atau objek order plugin yang Anda pakai—bukan hardcode. - Untuk keamanan pembaruan status di server: hasilkan nonce WordPress (
wp_create_nonce) dan kirim bersama permintaan AJAX darionSuccess.
- Nominal (
- Backend wajib jika status harus tersimpan di database:
- Daftarkan handler dengan
add_action('wp_ajax_...', ...)danadd_action('wp_ajax_nopriv_...', ...)untuk pengguna tidak login jika perlu. - Di handler: verifikasi nonce, validasi order/invoice, lalu
UPDATEdata (post meta, tabel custom, atau API plugin toko). Templat PHP checkout saja tidak ikut dieksekusi pada requestadmin-ajax.php, jadi logika update tidak boleh hanya berada di file template yang di-includesekali saat halaman tampil—harus di file yang selalu dimuat lewatfunctions.php/ plugin utama /ajax.phptheme.
- Daftarkan handler dengan
- JavaScript: setelah
QrisSDKtersedia (typeof QrisSDK !== 'undefined'), inisialisasi seperti pada bagian Laravel / Vanilla JS, lalu dionSuccesspanggilfetch(ajaxurl, { method: 'POST', body: URLSearchParams dengan action + nonce + id order })lalu perbarui UI (sembunyikan tombol, tampilkan pesan sukses, redirect, dll.).
Dengan pola di atas, Anda bisa memasang SDK di halaman statis, WooCommerce, Easy Digital Downloads, toko theme custom, atau kombinasi plugin—yang berubah hanya sumber data PHP (dari mana diambil amount, invoice, username, dan id pesanan) dan bentuk penyimpanan status di server.
Contoh referensi nyata: ayoborong.com
Situs ayoborong.com memakai WordPress dengan ekosistem toko berbasis theme vmplace dan alur Velocity (keranjang → checkout → pembayaran PGAuto / QRIS). Contoh implementasi yang selaras dengan panduan universal di atas:
| Lapisan | Peran |
| --- | --- |
| Tampilan & inisialisasi SDK | File template theme, misalnya payment-pgauto-display.php: menghitung nominal dan data pembeli dari POST atau dari baris order di database, menampilkan #poppay-payment-result, tombol bayar, serta tag <script> yang memuat UMD dan membuat instance QrisSDK dengan onSuccess / onFailed. |
| Pembaruan status “dibayar” di server | File PHP terpisah yang selalu dimuat dari functions.php (atau digabung ke file AJAX theme), mendefinisikan action admin-ajax yang memverifikasi nonce + invoice, memastikan metode pembayaran sesuai, lalu mengupdate status order di tabel custom toko. |
| Alur pengguna | Pembeli menyelesaikan checkout → halaman instruksi pembayaran menampilkan QR existing + opsi Bayar pakai QRIS PopPay → setelah sukses di SDK, browser memanggil AJAX → UI sukses dan data order konsisten dengan status dibayar di sistem. |
Contoh ini mengikuti prinsip universal: frontend (template + CDN + QrisSDK) dan backend (AJAX + nonce) terpisah dengan jelas. Anda dapat menyalin polanya ke proyek WordPress lain dengan mengganti sumber data order dan query update sesuai plugin/tema masing-masing.
Contoh kode: backend (PHP) — admin-ajax + update database
Letakkan di plugin custom, functions.php child theme, atau file include theme yang selalu dimuat. Ganti nama action, nama tabel/meta, dan logika update sesuai struktur data Anda (WooCommerce, tabel custom, CPT, dll.).
Pola umum: tabel custom ($wpdb->prefix . 'nama_tabel' — contoh field invoice + status):
<?php
/**
* Handler AJAX: tandai order lunas setelah onSuccess SDK (panggil dari fetch di frontend).
* Action: my_qris_mark_paid → POST field: action, invoice, nonce
*/
add_action( 'wp_ajax_my_qris_mark_paid', 'my_qris_mark_paid_handler' );
add_action( 'wp_ajax_nopriv_my_qris_mark_paid', 'my_qris_mark_paid_handler' );
function my_qris_mark_paid_handler() {
$invoice = isset( $_POST['invoice'] ) ? sanitize_text_field( wp_unslash( $_POST['invoice'] ) ) : '';
$nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
if ( ! $invoice || ! wp_verify_nonce( $nonce, 'my_qris_' . $invoice ) ) {
wp_send_json_error( array( 'message' => 'Permintaan tidak valid.' ), 403 );
}
global $wpdb;
$table = $wpdb->prefix . 'orders'; // SESUAIKAN nama tabel order Anda.
$row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE invoice = %s", $invoice ) );
if ( ! $row ) {
wp_send_json_error( array( 'message' => 'Order tidak ditemukan.' ), 404 );
}
// Opsional: pastikan pembeli yang login = pemilik order.
if ( is_user_logged_in() && isset( $row->user_id ) && (int) $row->user_id !== get_current_user_id() ) {
wp_send_json_error( array( 'message' => 'Akses ditolak.' ), 403 );
}
$updated = $wpdb->update(
$table,
array( 'status' => 'paid' ), // SESUAIKAN nilai / kolom status.
array( 'invoice' => $invoice ),
array( '%s' ),
array( '%s' )
);
if ( false === $updated ) {
wp_send_json_error( array( 'message' => 'Gagal memperbarui database.' ), 500 );
}
wp_send_json_success( array( 'message' => 'Status diperbarui.' ) );
}Pola alternatif: WooCommerce (jika order_id diketahui dan plugin aktif):
<?php
function my_qris_wc_mark_paid_handler() {
$order_id = isset( $_POST['order_id'] ) ? absint( $_POST['order_id'] ) : 0;
$nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
if ( ! $order_id || ! wp_verify_nonce( $nonce, 'my_qris_wc_' . $order_id ) ) {
wp_send_json_error( array( 'message' => 'Permintaan tidak valid.' ), 403 );
}
if ( ! function_exists( 'wc_get_order' ) ) {
wp_send_json_error( array( 'message' => 'WooCommerce tidak aktif.' ), 400 );
}
$order = wc_get_order( $order_id );
if ( ! $order ) {
wp_send_json_error( array( 'message' => 'Order tidak ditemukan.' ), 404 );
}
// Contoh: tandai sudah dibayar (sesuaikan dengan alur Anda).
$order->payment_complete();
// atau: $order->update_status( 'processing' );
wp_send_json_success( array( 'message' => 'Pembayaran tercatat.' ) );
}
// add_action( 'wp_ajax_my_qris_wc_mark_paid', 'my_qris_wc_mark_paid_handler' );
// add_action( 'wp_ajax_nopriv_my_qris_wc_mark_paid', 'my_qris_wc_mark_paid_handler' );Contoh kode: frontend universal WordPress — enqueue SDK + inisialisasi
Langkah A — muat SDK (dan opsional skrip init) dari functions.php atau plugin:
add_action( 'wp_enqueue_scripts', function () {
// SESUAIKAN kondisi halaman (slug, ID, is_checkout(), dll.).
if ( ! is_page( 'thank-you' ) ) {
return;
}
wp_enqueue_script(
'qris-poppay-sdk',
'https://unpkg.com/@poppackage/qris-payment-sdk/dist/qris-sdk.umd.js',
array(),
null,
true
);
} );Langkah B — di template halaman pembayaran (misalnya page-thank-you.php atau template checkout theme), output data dari PHP + tombol + skrip:
Nilai $amount, $invoice, $username, $payor_name, $payor_email harus berasal dari order sungguhan (query DB / fungsi plugin), bukan angka statis.
<?php
// Contoh data dari order (sesuaikan dengan proyek Anda):
$amount = 50000;
$invoice = 'INV-1001';
$username = is_user_logged_in() ? wp_get_current_user()->user_login : 'guest-' . $invoice;
$payor_name = 'Budi';
$payor_email = '[email protected]';
$nonce = wp_create_nonce( 'my_qris_' . $invoice );
$ajax_url = esc_url( admin_url( 'admin-ajax.php' ) );
?>
<div id="qris-payment-result"></div>
<button type="button" id="qris-pay-button" class="button">Bayar QRIS PopPay</button>
<script>
document.addEventListener('DOMContentLoaded', function () {
if (typeof QrisSDK === 'undefined') {
console.error('QrisSDK belum dimuat. Pastikan script CDN di-enqueue.');
return;
}
var btn = document.getElementById('qris-pay-button');
var invoice = <?php echo wp_json_encode( $invoice ); ?>;
var nonce = <?php echo wp_json_encode( $nonce ); ?>;
var qris = new QrisSDK({
amount: <?php echo (int) $amount; ?>,
invoice: invoice,
username: <?php echo wp_json_encode( $username ); ?>,
notes: 'Invoice ' + invoice,
payor_name: <?php echo wp_json_encode( $payor_name ); ?>,
payor_email: <?php echo wp_json_encode( $payor_email ); ?>,
resultContainerId: 'qris-payment-result',
onSuccess: function (data) {
var body = new URLSearchParams();
body.append('action', 'my_qris_mark_paid');
body.append('invoice', invoice);
body.append('nonce', nonce);
var ajaxUrl = (typeof ajaxurl !== 'undefined') ? ajaxurl : <?php echo wp_json_encode( $ajax_url ); ?>;
fetch(ajaxUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
body: body.toString(),
credentials: 'same-origin'
})
.then(function (r) { return r.json(); })
.then(function (json) {
if (json && json.success) {
alert('Pembayaran berhasil & status order diperbarui.');
} else {
alert((json && json.data && json.data.message) ? json.data.message : 'Gagal memperbarui server.');
}
})
.catch(function (err) { console.error(err); });
},
onFailed: function (status) {
console.log('QRIS gagal / timeout:', status);
}
});
btn.addEventListener('click', function () {
qris.openPayment();
});
});
</script>Catatan: Jika tema Anda belum mendefinisikan ajaxurl di frontend, gunakan variabel ajaxUrl seperti di atas (sudah memakai admin_url( 'admin-ajax.php' ) dari PHP). Nama action AJAX (my_qris_mark_paid) harus sama dengan add_action di backend.
2. Laravel
Di Laravel, pola universalnya: halaman Blade atau Inertia memuat SDK (CDN atau bundler), onSuccess memanggil route POST (biasanya di routes/web.php dengan CSRF + middleware auth bila pembeli login). Controller memverifikasi bahwa order milik user (atau token sekali pakai untuk guest—sesuaikan), lalu Eloquent / Query Builder meng-update baris order.
Prasyarat
- Domain production terdaftar di admin PopPay.
- Pastikan CSRF token tersedia di frontend (meta tag
csrf-tokendi layout, atau@csrfpada form) untuk requestPOSTdari JavaScript. - Untuk API stateless (mobile / SPA terpisah), pertimbangkan Laravel Sanctum atau signed URL alih-alir contoh
web+ session di bawah.
Pola integrasi universal (ringkas)
- Muat SDK di layout halaman pembayaran (
@push('scripts')atau Vite). - Saat inisialisasi
QrisSDK, sertakaninvoicewajib (sama dengan kolom invoice order di database) danusernamewajib (user login, guest token, atau ID member). - Controller menerima
invoice(dan opsionalorder_id), memvalidasi, mengupdate kolomstatus/payment_statuspada modelOrder. - Frontend di
onSuccess:fetchke route Laravel dengan headerX-CSRF-TOKENdanAccept: application/json, body JSON{ invoice }.
Contoh kode: backend (Laravel) — route + controller + update database
routes/web.php (contoh; sesuaikan prefix & middleware):
<?php
use App\Http\Controllers\OrderController;
use Illuminate\Support\Facades\Route;
Route::middleware(['auth'])->group(function () {
Route::post('/orders/qris-mark-paid', [OrderController::class, 'markPaidFromQris'])
->name('orders.qris-mark-paid');
});app/Http/Controllers/OrderController.php:
<?php
namespace App\Http\Controllers;
use App\Models\Order;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
class OrderController extends Controller
{
/**
* Dipanggil dari onSuccess SDK — update status order di database.
*/
public function markPaidFromQris(Request $request): JsonResponse
{
$validated = $request->validate([
'invoice' => ['required', 'string', 'max:100'],
]);
$order = Order::query()
->where('invoice', $validated['invoice'])
->where('user_id', auth()->id()) // pastikan pemilik order = user login
->first();
if (! $order) {
return response()->json(['message' => 'Order tidak ditemukan.'], 404);
}
// Opsional: cegah double-update jika sudah paid
if ($order->status === 'paid') {
return response()->json(['message' => 'Sudah dibayar.', 'already' => true]);
}
$order->update([
'status' => 'paid', // SESUAIKAN nama kolom / nilai enum Anda
]);
return response()->json(['message' => 'Status diperbarui.']);
}
}app/Models/Order.php: pastikan invoice, user_id, status ada di $fillable (atau gunakan $guarded = [] hanya untuk development).
Contoh kode: frontend (Blade + UMD dari CDN)
Letakkan di view Blade tempat data order sudah tersedia ($order). Untuk guest checkout, ganti middleware/route dengan pola signed URL atau token sekali pakai yang Anda generate di server—jangan mempercayai invoice saja tanpa verifikasi tambahan.
{{-- resources/views/orders/pay.blade.php --}}
<meta name="csrf-token" content="{{ csrf_token() }}">
<div id="qris-payment-result"></div>
<button type="button" id="qris-pay-button">Bayar QRIS PopPay</button>
<script src="https://unpkg.com/@poppackage/qris-payment-sdk/dist/qris-sdk.umd.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
if (typeof QrisSDK === 'undefined') return;
var invoice = @json($order->invoice);
var csrf = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
var url = @json(route('orders.qris-mark-paid'));
var qris = new QrisSDK({
amount: {{ (int) $order->total_amount }},
invoice: invoice,
username: @json($order->customer_username ?? auth()->user()?->username ?? 'guest-' . $order->invoice),
notes: 'Invoice ' + invoice,
payor_name: @json($order->customer_name ?? ''),
payor_email: @json($order->customer_email ?? ''),
resultContainerId: 'qris-payment-result',
onSuccess: function () {
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-CSRF-TOKEN': csrf,
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({ invoice: invoice }),
credentials: 'same-origin'
})
.then(function (r) { return r.json(); })
.then(function (data) {
if (data.message) { alert(data.message); }
})
.catch(console.error);
},
onFailed: function (status) {
console.log('QRIS gagal / timeout:', status);
}
});
document.getElementById('qris-pay-button').addEventListener('click', function () {
qris.openPayment();
});
});
</script>3. Next.js (App Router)
Di Next.js, jangan mengupdate database langsung dari komponen klien. Pola universal: Server Action atau Route Handler (app/api/.../route.ts) yang memverifikasi sesi (misalnya NextAuth, Clerk, atau cookie session Anda), lalu memanggil ORM / SQL (Prisma, Drizzle, Kysely, dll.). Komponen klien memuat ESM @poppackage/qris-payment-sdk, lalu di onSuccess memanggil fetch('/api/...') dengan credentials bila memakai cookie session.
Prasyarat
- Domain production terdaftar di admin PopPay.
- Pastikan route API tidak mengekspos secret server ke browser; gunakan session atau JWT server-side untuk membuktikan identitas pembeli.
Pola integrasi universal (ringkas)
- Pasang paket:
npm i @poppackage/qris-payment-sdk. - Buat Route Handler
POSTyang membaca body{ invoice }, memverifikasi sesi, mengupdate tabel order. - Di Client Component (
'use client'), inisialisasiQrisSDKdenganinvoicewajib,usernamewajib (string dari server, misalnya dari props halaman) dan padaonSuccesspanggil API denganfetch.
Contoh kode: backend (Next.js Route Handler) — update database
Sesuaikan impor DB dengan stack Anda (contoh di bawah memakai pola Prisma; ganti dengan query Anda).
app/api/orders/qris-mark-paid/route.ts:
import { NextResponse } from 'next/server';
// import { getServerSession } from 'next-auth'; // jika pakai NextAuth
// import { prisma } from '@/lib/prisma';
export async function POST(request: Request) {
try {
const body = await request.json();
const invoice = typeof body.invoice === 'string' ? body.invoice.trim() : '';
if (!invoice) {
return NextResponse.json({ message: 'invoice wajib diisi.' }, { status: 400 });
}
// --- Verifikasi sesi (contoh; sesuaikan dengan auth Anda) ---
// const session = await getServerSession(authOptions);
// if (!session?.user?.id) {
// return NextResponse.json({ message: 'Unauthorized' }, { status: 401 });
// }
// --- Contoh Prisma (uncomment & sesuaikan model) ---
// const order = await prisma.order.findFirst({
// where: {
// invoice,
// userId: session.user.id as string,
// },
// });
// if (!order) {
// return NextResponse.json({ message: 'Order tidak ditemukan.' }, { status: 404 });
// }
// await prisma.order.update({
// where: { id: order.id },
// data: { status: 'paid' },
// });
return NextResponse.json({ message: 'Status diperbarui.' });
} catch (e) {
console.error(e);
return NextResponse.json({ message: 'Server error.' }, { status: 500 });
}
}Uncomment dan sambungkan blok Prisma (atau db.execute mentah) sesuai skema Anda. Intinya: satu sumber kebenaran di server yang menulis ke database.
Contoh kode: frontend (Client Component + ESM)
components/QrisPayButton.tsx:
'use client';
import { useCallback, useEffect, useRef } from 'react';
import { QrisSDK } from '@poppackage/qris-payment-sdk';
type Props = {
amount: number;
invoice: string;
username: string;
payorName: string;
payorEmail: string;
};
export function QrisPayButton({ amount, invoice, username, payorName, payorEmail }: Props) {
const qrisRef = useRef<QrisSDK | null>(null);
useEffect(() => {
qrisRef.current = new QrisSDK({
amount,
invoice,
username,
notes: `Invoice ${invoice}`,
payor_name: payorName,
payor_email: payorEmail,
resultContainerId: 'qris-payment-result',
onSuccess: async () => {
const res = await fetch('/api/orders/qris-mark-paid', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ invoice }),
credentials: 'include', // penting jika auth berbasis cookie
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
alert(data.message ?? 'Gagal memperbarui order.');
return;
}
alert(data.message ?? 'Pembayaran tercatat.');
},
onFailed: (status) => {
console.log('QRIS gagal / timeout:', status);
},
});
}, [amount, invoice, username, payorName, payorEmail]);
const open = useCallback(() => {
qrisRef.current?.openPayment();
}, []);
return (
<>
<div id="qris-payment-result" />
<button type="button" onClick={open}>
Bayar QRIS PopPay
</button>
</>
);
}Halaman server (page.tsx) mengambil order dari database dan meneruskan amount, invoice, username, dll. ke <QrisPayButton /> sebagai props.
📖 Konfigurasi (Options)
Anda dapat mengirimkan objek konfigurasi saat inisialisasi new QrisSDK(config).
| Parameter | Wajib / Opsional | Tipe | Fungsi | Contoh isi |
| --- | --- | --- | --- | --- |
| amount | Wajib | number | Nominal pembayaran dalam Rupiah (IDR), integer (tanpa desimal). Dikirim ke gateway saat membuat transaksi. | 50000 |
| invoice | Wajib | string | Pengenal unik transaksi/order di sistem Anda (nomor invoice, order id publik, dll.). Harus konsisten dengan data di database agar backend dapat memvalidasi pembayaran di onSuccess. | 'INV-2026-0042' |
| username | Wajib | string | Pengenal pembeli atau sesi transaksi di sistem Anda (user login, guest token, ID member, dll.). Dipakai gateway untuk lookup transaksi (check-payment) dan callback share. Tidak boleh kosong. | 'user-123' atau 'guest-abc' |
| notes | Opsional | string | Catatan transaksi (mutasi/laporan). Jika tidak diisi, SDK memakai string kosong. | 'Pesanan #1001' |
| promotion | Opsional | string \| null | Kode atau label promosi yang ingin dicatat bersama transaksi. Jika tidak diisi, SDK mengirim undefined (tidak disertakan di payload). | 'PROMO2026' atau null |
| payor_email | Opsional | string | Email pembayar, jika ingin dicatat bersama transaksi. | '[email protected]' |
| payor_name | Opsional | string | Nama pembayar, jika ingin dicatat bersama transaksi. | 'Budi Santoso' |
| expirationInterval | Opsional | number | Masa berlaku QR dalam detik. Default SDK: 300 (5 menit). | 300 |
| onSuccess | Opsional | (data: any) => void | Dipanggil saat pembayaran berhasil. Jika tidak diisi, SDK hanya console.log. | (data) => { /* redirect / toast */ } |
| onFailed | Opsional | (status: any) => void | Dipanggil saat pembayaran gagal atau timeout setelah transaksi/QR berjalan. Tidak dipanggil untuk health check domain gagal. | (status) => { /* tampilkan error */ } |
| onUnavailable | Opsional | (health: any) => void | Dipanggil saat health check domain gagal sebelum transaksi dibuat. Untuk menyembunyikan tombol sebelum user klik, buat instance QrisSDK saat halaman load. Cocok jika domain belum terdaftar, soft-deleted, atau konfigurasi merchant belum lengkap. | (health) => { button.remove() } |
| displayMode | Opsional | 'popup' \| 'inline' | Mode tampilan UI pembayaran. Default: 'popup'. Gunakan 'inline' untuk embed iframe di halaman. | 'inline' |
| containerId | Wajib jika inline | string | ID elemen HTML tempat SDK inject iframe pembayaran. | 'qris-payment-frame' |
| resultContainerId | Opsional | string | ID elemen HTML tempat hasil pembayaran sukses akan di-inject secara otomatis. | 'payment-result' |
| healthCheckEnabled | Opsional | boolean | Menjalankan preflight /payment-health sebelum transaksi dibuat. Default: true. Biarkan aktif untuk mencegah tombol bayar bekerja pada domain yang tidak valid. | true |
NB: Wajib mendaftarkan domain ke admin terlebih dahulu
📦 Publish ke npm (Public)
Prasyarat
- Akun npmjs.com sudah terdaftar.
- npm CLI terinstall (
npm -v).
Langkah-langkah
1. Login ke npm
npm loginIkuti prompt untuk memasukkan username, password, email, dan OTP (jika mengaktifkan 2FA — sangat disarankan).
2. Pastikan build terbaru
npm run buildPastikan folder dist/ sudah ter-generate dengan file berikut:
dist/qris-sdk.es.js(ESM)dist/qris-sdk.umd.js(UMD)dist/src/main.d.ts(type declarations)
3. Review sebelum publish
Cek isi paket yang akan di-publish (npm hanya mempublish file yang tercantum di "files" di package.json, yaitu folder dist/):
npm pack --dry-runPastikan tidak ada file sensitif (.env, src/, dll.) yang ikut terbawa.
4. Update versi
Ikuti Semantic Versioning:
| Perubahan | Perintah |
| --- | --- |
| Patch (bug fix, doc update) | npm version patch → 1.2.0 → 1.2.1 |
| Minor (fitur baru, non-breaking) | npm version minor → 1.2.0 → 1.3.0 |
| Major (breaking change) | npm version major → 1.2.0 → 2.0.0 |
Contoh:
npm version patch -m "feat: add promotion field"Perintah ini otomatis:
- Update
"version"dipackage.json - Commit perubahan
- Buat Git tag (e.g.
v1.2.1)
5. Publish
npm publish --access publicFlag
--access publicwajib karena nama paket menggunakan scoped (@poppackage/). Tanpa flag ini, npm akan treat paket sebagai private dan publish akan gagal.
6. Verifikasi publish
Buka https://www.npmjs.com/package/@poppackage/qris-payment-sdk dan pastikan versi terbaru sudah tampil.
User bisa install dengan:
npm i @poppackage/qris-payment-sdkTips
- 2FA: Aktifkan two-factor authentication di akun npm untuk keamanan.
.npmignore: Jika ada file yang ingin di-exclude selain dari"files"dipackage.json, buat file.npmignore.- CI/CD (opsional): Untuk automation, bisa gunakan GitHub Actions untuk auto-publish saat push tag baru.
- Deprecate versi lama: Jika ada versi yang bermasalah, gunakan
npm deprecate @poppackage/[email protected] "pesan"untuk menandai versi tersebut sebagai deprecated.
