@rusoft/paycenter
v1.0.8
Published
Express router and helpers for Paycenter proxy and payment confirmation.
Maintainers
Readme
@rusoft/paycenter-proxy
Express router + helpers for Paycenter proxying and payment confirmation.
## Requirements
- Node.js >= 18 (uses built-in
fetchandcrypto.randomUUID)
Install
npm i @rusoft/paycenter-proxy
## .env Requirements
# Required
PAYCENTER_TOKEN=your_authtoken_here
HMAC_SECRET=your_hmac_secret_here
PAYCENTER_CLIENTID=00000000
PAYCENTER_URL=https://paycorp-xxx.prod.aws.paycorp.lk/rest/service/proxy
## paycenter and Return API setup
# api/paycenter/route.ts
import { NextRequest } from "next/server";
import { proxyToPaycenter } from "@rusoft/paycenter";
export const runtime = "nodejs";
const mustGet = (k: string) => {
const v = process.env[k];
if (!v) throw new Error(`Missing env ${k}`);
return v;
};
export async function POST(req: NextRequest) {
try {
const raw = await req.text(); // <-- raw JSON string
const { status, body } = await proxyToPaycenter(raw, {
paycenterUrl: mustGet("PAYCENTER_URL"),
token: mustGet("PAYCENTER_TOKEN"),
hmacSecret: mustGet("HMAC_SECRET"),
});
return new Response(body, {
status,
headers: { "content-type": "application/json" },
});
} catch (err: any) {
console.error("Proxy error:", err);
return new Response(
JSON.stringify({ error: "Proxy error", message: err?.message }),
{ status: 500, headers: { "content-type": "application/json" } }
);
}
}
# api/paycenter/return/route.ts
import { NextRequest, NextResponse } from "next/server";
import { confirmPayment } from "@rusoft/paycenter";
export const runtime = "nodejs";
const must = (k: string) => {
const v = process.env[k];
if (!v) throw new Error(`Missing env ${k}`);
return v;
};
export async function GET(req: NextRequest) {
const url = new URL(req.url);
const bridge = url.searchParams.get("bridge") === "1";
const reqid = url.searchParams.get("reqid") || "";
const clientRef = url.searchParams.get("clientRef") || "";
const { status, body } = await confirmPayment(reqid, {
paycenterUrl: must("PAYCENTER_URL"),
clientId: must("PAYCENTER_CLIENTID"),
token: must("PAYCENTER_TOKEN"),
hmacSecret: must("HMAC_SECRET"),
});
if (!bridge) {
return new Response(body, {
status,
headers: { "content-type": "application/json" },
});
}
const jsonStr = typeof body === "string" ? body : JSON.stringify(body);
const html = `<!doctype html>
<html><head><meta charset="utf-8"><title>Return</title></head>
<body>
<script>
(function(){
try{
var payload = ${JSON.stringify(jsonStr)};
var data; try{ data = JSON.parse(payload); }catch(e){ data = { raw: payload }; }
window.parent.postMessage(
{ source: "paycenter", status: ${status}, clientRef: ${JSON.stringify(clientRef)}, reqid: ${JSON.stringify(reqid)}, data },
location.origin
);
}catch(e){}
})();
</script>
</body></html>`;
return new Response(html, { status: 200, headers: { "content-type": "text/html; charset=utf-8" } });
}
## Page Implement
'Use Client'
# type paycenterMassage
type PaycenterMessage = {
source: 'paycenter';
status: number;
clientRef: string;
reqid: string;
data: any;
};
# States
const [iframeUrl, setIframeUrl] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [paymentResult, setPaymentResult] = useState<PaycenterMessage | null>(null);
const iframeRef = useRef<HTMLIFrameElement>(null);
# Setup Bridge return
const bridgeReturnUrl = useMemo(() => {
if (typeof window === 'undefined') return '';
return `${window.location.origin}/api/paycenter/return?bridge=1`;
}, []);
# Listen for return bridge → save in state
useEffect(() => {
function onMessage(event: MessageEvent) {
if (typeof window === 'undefined') return;
if (event.origin !== window.location.origin) return;
if (event.data?.source !== 'paycenter') return;
setPaymentResult(event.data as PaycenterMessage); // <-- save first
setIframeUrl(null); // close overlay
}
window.addEventListener('message', onMessage);
return () => window.removeEventListener('message', onMessage);
}, []);
// After state updated, alert it
useEffect(() => {
if (!paymentResult) return;
alert(
`Payment result:
status=${paymentResult.status}
clientRef=${paymentResult.clientRef}
reqid=${paymentResult.reqid}
payload=${JSON.stringify(paymentResult.data, null, 2)}`
);
}, [paymentResult]);
# Send Init Req with payload
async function sendInitRequest() {
setLoading(true);
try {
const payload = {
version: '1.5',
msgId: crypto.randomUUID(),
operation: 'PAYMENT_INIT',
requestDate: new Date().toISOString(),
validateOnly: false,
requestData: {
clientId: 'xxxxxxxx',
transactionType: 'PURCHASE',
transactionAmount: {
totalAmount: 0,
paymentAmount: 200,
serviceFeeAmount: 0,
currency: 'LKR',
},
redirect: {
returnUrl: bridgeReturnUrl, // bridge posts JSON to parent
cancelUrl: bridgeReturnUrl, // optional
returnMethod: 'GET',
},
clientRef: `ORDER-${crypto.randomUUID()}`,
comment: `Test ${new Date().toISOString()}`,
tokenize: false,
useReliability: true,
extraData: { st_id: '123456', batch_id: '102348748', group: '1231458' },
},
};
const res = await fetch('/api/paycenter', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(payload),
});
const text = await res.text();
let data: any;
try { data = JSON.parse(text); } catch { data = { raw: text }; }
const url =
data?.responseData?.paymentPageUrl ||
data?.paymentPageUrl ||
data?.redirectUrl;
if (!url) {
alert('No payment page URL received.');
return;
}
setIframeUrl(url);
} catch (e: any) {
console.error(e);
alert(`Init failed: ${e?.message || 'Unknown error'}`);
} finally {
setLoading(false);
}
}
## retun UI Sample
<main style={{ minHeight: '100vh', padding: 20, display: 'flex', flexDirection: 'column', gap: 16, background: '#f9f9f9', color: '#333', fontFamily: 'Arial, sans-serif' }}>
<h1>Paycenter API Test (Save then Alert)</h1>
<button onClick={sendInitRequest} disabled={loading} style={{ alignSelf: 'flex-start', padding: '10px 20px', fontSize: 16 }}>
{loading ? 'Sending…' : 'Send Init Request'}
</button>
{paymentResult && (
<pre style={{ background: '#fff', padding: 12, borderRadius: 8, maxWidth: 900, overflow: 'auto' }}>
{JSON.stringify(paymentResult, null, 2)}
</pre>
)}
{iframeUrl && (
<div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.6)', display: 'flex', justifyContent: 'center', alignItems: 'center', zIndex: 9999 }}>
<div style={{ position: 'relative', width: '90%', height: '90%' }}>
<button onClick={() => setIframeUrl(null)} style={{ position: 'absolute', top: 10, right: 10, background: '#fff', border: 'none', fontSize: 24, borderRadius: '50%', padding: '4px 10px', zIndex: 10000 }}>
×
</button>
<iframe ref={iframeRef} src={iframeUrl} style={{ width: '100%', height: '100%', border: 'none', background: '#fff' }} />
</div>
</div>
)}
</main>
## Authors
- [@rusoft](https://www.github.com/ruma-lk)
## Used By
This project is for Paycorp Iframe:
-bancstac
## Tech Advice
Above content for NextJS Example use you can use as Normal in Express:
// server.js
import 'dotenv/config';
import express from 'express';
import cors from 'cors';
// If using as a dependency in another app, use:
import { createPaycenterRouter } from '@rusoft/paycenter';
const app = express();
// CORS (allow your frontend origin; comma-separate multiple origins in ALLOWED_ORIGINS)
const allowList = (process.env.ALLOWED_ORIGINS || '').split(',').map(s => s.trim()).filter(Boolean);
app.use(cors({
origin: allowList.length ? allowList : true,
credentials: true,
}));
// (Optional) health check
app.get('/', (_req, res) => res.json({ ok: true }));
// Mount Paycenter router
app.use(
createPaycenterRouter({
cors: {
origin: allowList.length ? allowList : true,
credentials: true,
},
basePath: process.env.BASE_PATH || '/api', // gives /api/paycenter and /api/paycenter/return
})
);
const port = Number(process.env.PORT || 3001);
app.listen(port, () => {
console.log(`➜ Paycenter proxy up: http://localhost:${port}`);
console.log(` POST ${process.env.BASE_PATH || '/api'}/paycenter`);
console.log(` GET ${process.env.BASE_PATH || '/api'}/paycenter/return`);
console.log(` GET /confirm?reqid=...`);
});
