multi-payment-system 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,38 +1,71 @@
1
1
  # multi-payment-system
2
2
 
3
- npm package + optional Express server for **Xendit, Midtrans, Razorpay, Stripe, Omise** (SG / MY / SEA), plus Yokke EDC attempt helpers.
3
+ Payments for **Node** and **React** — Xendit, Midtrans, Razorpay, Stripe, Omise (+ Yokke EDC helpers).
4
+
5
+ | Import | Use in |
6
+ |--------|--------|
7
+ | `multi-payment-system` | Node / backend (`XenditGateway`, `charge`) |
8
+ | `multi-payment-system/client` | React / browser (`PaymentClient`) |
9
+ | `multi-payment-system/react` | React hook (`usePaymentClient`) |
4
10
 
5
11
  ---
6
12
 
7
- ## Install (as a library)
13
+ ## React client
8
14
 
9
15
  ```bash
10
16
  npm install multi-payment-system
11
17
  ```
12
18
 
13
- ### Use gateways as classes
19
+ 1. Run the payment API (`npm start` in this repo) so React has a backend.
20
+ 2. In React:
21
+
22
+ ```jsx
23
+ import { usePaymentClient } from "multi-payment-system/react";
24
+
25
+ export function PayButton() {
26
+ const { chargeAndRedirect, loading, error } = usePaymentClient({
27
+ baseUrl: import.meta.env.VITE_PAY_API || "http://localhost:3000",
28
+ });
29
+
30
+ return (
31
+ <button
32
+ disabled={loading}
33
+ onClick={() =>
34
+ chargeAndRedirect({
35
+ gateway: "xendit",
36
+ amount: 30,
37
+ currency: "SGD",
38
+ paymentMethod: "card",
39
+ keys: { secretKey: import.meta.env.VITE_XENDIT_SECRET }, // demo only — prefer server-held keys
40
+ returnUrl: window.location.origin + "/success",
41
+ cancelUrl: window.location.origin + "/cancel",
42
+ })
43
+ }
44
+ >
45
+ {loading ? "Opening…" : "Pay with card"}
46
+ </button>
47
+ );
48
+ }
49
+ ```
14
50
 
15
- ```js
16
- import { XenditGateway, StripeGateway, charge, getStatus, listGateways } from "multi-payment-system";
51
+ Or without the hook:
17
52
 
18
- const xendit = new XenditGateway();
19
- const pay = await xendit.createPayment({
20
- paymentId: "pay_1",
21
- amountMinor: 3000, // SGD 30.00
22
- currency: "SGD",
23
- paymentMethod: "card",
24
- keys: { secretKey: process.env.XENDIT_SECRET_KEY },
25
- returnUrl: "https://example.com/success",
26
- cancelUrl: "https://example.com/cancel",
27
- });
53
+ ```js
54
+ import { PaymentClient, redirectToCheckout } from "multi-payment-system/client";
28
55
 
29
- console.log(pay.checkoutUrl);
56
+ const client = new PaymentClient({ baseUrl: "http://localhost:3000" });
57
+ const { payment } = await client.charge({ /* same body as Postman */ });
58
+ redirectToCheckout(payment); // opens Xendit/Stripe checkout
30
59
  ```
31
60
 
32
- ### Or one-liner helpers
61
+ **Security:** do not ship production secret keys in a React bundle. Put keys on the server and have the client call your API without secrets.
62
+
63
+ ---
64
+
65
+ ## Node / backend SDK
33
66
 
34
67
  ```js
35
- import { charge, getStatus } from "multi-payment-system";
68
+ import { XenditGateway, charge } from "multi-payment-system";
36
69
 
37
70
  const result = await charge({
38
71
  gateway: "xendit",
@@ -42,34 +75,11 @@ const result = await charge({
42
75
  keys: { secretKey: process.env.XENDIT_SECRET_KEY },
43
76
  returnUrl: "https://example.com/success",
44
77
  });
45
-
46
- const status = await getStatus({
47
- gateway: "xendit",
48
- gatewayRef: result.gatewayRef,
49
- keys: { secretKey: process.env.XENDIT_SECRET_KEY },
50
- });
51
- ```
52
-
53
- ### Exports
54
-
55
- | Export | What |
56
- |--------|------|
57
- | `XenditGateway`, `MidtransGateway`, `RazorpayGateway`, `StripeGateway`, `OmiseGateway` | Gateway classes |
58
- | `charge`, `getStatus`, `listGateways` | Helper functions |
59
- | `getGateway("xendit")` | Resolve by name |
60
- | `createApp()` | Express app (HTTP API) |
61
- | `PaymentService` | In-memory payment tracker used by the server |
62
-
63
- ### Publish to npm
64
-
65
- ```bash
66
- npm login
67
- npm publish --access public
68
78
  ```
69
79
 
70
80
  ---
71
81
 
72
- ## Run as a local server
82
+ ## Run local API server
73
83
 
74
84
  ```bash
75
85
  npm install
@@ -79,7 +89,7 @@ npm start
79
89
  Server: `http://localhost:3000`
80
90
 
81
91
  ```bash
82
- npm run test:smoke # local EDC + validation checks (no real gateway keys needed)
92
+ npm run test:smoke
83
93
  ```
84
94
 
85
95
  ### Postman
package/package.json CHANGED
@@ -1,15 +1,34 @@
1
1
  {
2
2
  "name": "multi-payment-system",
3
- "version": "1.0.0",
4
- "description": "Multi-gateway payment SDK + optional Express server (Xendit, Midtrans, Razorpay, Stripe, Omise)",
3
+ "version": "1.1.0",
4
+ "description": "Multi-gateway payments for Node and React (Xendit, Midtrans, Razorpay, Stripe, Omise)",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
7
+ "module": "./src/index.js",
8
+ "react-native": "./src/client/index.js",
9
+ "browser": {
10
+ "./src/index.js": "./src/client/index.js",
11
+ "./src/server.js": false,
12
+ "./src/app.js": false,
13
+ "./src/config.js": false,
14
+ "express": false,
15
+ "cors": false,
16
+ "dotenv": false,
17
+ "zod": false
18
+ },
7
19
  "exports": {
8
- ".": "./src/index.js",
20
+ ".": {
21
+ "browser": "./src/client/index.js",
22
+ "import": "./src/index.js",
23
+ "default": "./src/index.js"
24
+ },
25
+ "./client": "./src/client/index.js",
26
+ "./react": "./src/client/react.js",
9
27
  "./server": "./src/server.js",
10
28
  "./app": "./src/app.js",
11
29
  "./package.json": "./package.json"
12
30
  },
31
+ "sideEffects": false,
13
32
  "files": [
14
33
  "src",
15
34
  "README.md",
@@ -19,28 +38,30 @@
19
38
  "start": "node src/server.js",
20
39
  "dev": "node --watch src/server.js",
21
40
  "test:smoke": "node scripts/smoke-test.js",
22
- "prepublishOnly": "node scripts/smoke-test.js"
41
+ "test:package": "node scripts/package-import-test.js",
42
+ "prepublishOnly": "npm run test:package && npm run test:smoke"
23
43
  },
24
44
  "engines": {
25
45
  "node": ">=18"
26
46
  },
27
47
  "keywords": [
28
48
  "payment",
49
+ "react",
29
50
  "xendit",
30
51
  "midtrans",
31
52
  "razorpay",
32
53
  "stripe",
33
54
  "omise",
34
- "sgd",
35
- "myr",
36
- "kiosk",
37
- "gateway"
55
+ "checkout"
38
56
  ],
39
- "author": "",
40
57
  "license": "MIT",
41
- "repository": {
42
- "type": "git",
43
- "url": ""
58
+ "peerDependencies": {
59
+ "react": ">=17"
60
+ },
61
+ "peerDependenciesMeta": {
62
+ "react": {
63
+ "optional": true
64
+ }
44
65
  },
45
66
  "dependencies": {
46
67
  "axios": "^1.7.9",
package/src/api.js CHANGED
@@ -1,22 +1,18 @@
1
- import { v4 as uuidv4 } from "uuid";
2
1
  import { getGateway, GATEWAY_NAMES, gatewayRegionsHint } from "./gateways/index.js";
3
2
  import { toMinorUnits, normalizeCurrency, SUPPORTED_CURRENCIES } from "./utils/money.js";
4
3
  import { PAYMENT_METHOD_HINTS, normalizeMethods } from "./utils/paymentMethods.js";
5
4
  import { AppError } from "./utils/errors.js";
6
5
 
6
+ function newId() {
7
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
8
+ return crypto.randomUUID();
9
+ }
10
+ return `pay_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
11
+ }
12
+
7
13
  /**
8
14
  * Create a payment on any supported gateway.
9
- *
10
- * @example
11
- * import { charge } from "multi-payment-system";
12
- * const result = await charge({
13
- * gateway: "xendit",
14
- * amount: 30,
15
- * currency: "SGD",
16
- * paymentMethod: "card",
17
- * keys: { secretKey: "xnd_development_..." },
18
- * returnUrl: "https://example.com/success",
19
- * });
15
+ * Prefer calling this on a server/backend — never ship secret keys in a React bundle.
20
16
  */
21
17
  export async function charge(input = {}) {
22
18
  const gatewayName = String(input.gateway || "").toLowerCase();
@@ -38,7 +34,7 @@ export async function charge(input = {}) {
38
34
  throw new AppError("amount must be greater than 0");
39
35
  }
40
36
 
41
- const paymentId = input.paymentId || uuidv4();
37
+ const paymentId = input.paymentId || newId();
42
38
  const gateway = getGateway(gatewayName);
43
39
 
44
40
  const result = await gateway.createPayment({
@@ -71,17 +67,7 @@ export async function charge(input = {}) {
71
67
  };
72
68
  }
73
69
 
74
- /**
75
- * Refresh / query payment status from the gateway.
76
- *
77
- * @example
78
- * import { getStatus } from "multi-payment-system";
79
- * const status = await getStatus({
80
- * gateway: "xendit",
81
- * gatewayRef: "inv_...",
82
- * keys: { secretKey: "xnd_development_..." },
83
- * });
84
- */
70
+ /** Refresh / query payment status from the gateway. */
85
71
  export async function getStatus(input = {}) {
86
72
  const gatewayName = String(input.gateway || "").toLowerCase();
87
73
  if (!gatewayName) throw new AppError("gateway is required");
@@ -106,7 +92,6 @@ export async function getStatus(input = {}) {
106
92
  };
107
93
  }
108
94
 
109
- /** List supported gateways, regions, currencies, payment method hints */
110
95
  export function listGateways() {
111
96
  return {
112
97
  gateways: GATEWAY_NAMES,
@@ -0,0 +1,173 @@
1
+ import { requestJson, ClientHttpError } from "./http.js";
2
+
3
+ /**
4
+ * Browser / React-safe client.
5
+ * Talks to YOUR payment backend (this package's Express server or any compatible API).
6
+ * Secret gateway keys stay on the server — do not put them in React.
7
+ *
8
+ * @example
9
+ * import { PaymentClient } from "multi-payment-system/client";
10
+ *
11
+ * const client = new PaymentClient({
12
+ * baseUrl: "http://localhost:3000",
13
+ * // apiKey: "optional-if-server-has-API_KEY",
14
+ * });
15
+ *
16
+ * const { payment } = await client.charge({
17
+ * gateway: "xendit",
18
+ * amount: 30,
19
+ * currency: "SGD",
20
+ * paymentMethod: "card",
21
+ * keys: { secretKey: "..." }, // only if your backend accepts keys per request
22
+ * returnUrl: window.location.origin + "/success",
23
+ * });
24
+ *
25
+ * client.openCheckout(payment);
26
+ */
27
+ export class PaymentClient {
28
+ constructor({ baseUrl, apiKey, headers } = {}) {
29
+ if (!baseUrl) throw new Error("PaymentClient: baseUrl is required");
30
+ this.baseUrl = baseUrl;
31
+ this.apiKey = apiKey || "";
32
+ this.headers = headers || {};
33
+ }
34
+
35
+ #opts(extra = {}) {
36
+ return {
37
+ apiKey: this.apiKey,
38
+ headers: this.headers,
39
+ ...extra,
40
+ };
41
+ }
42
+
43
+ /** GET /api/payments/meta */
44
+ async meta() {
45
+ return requestJson(this.baseUrl, "/api/payments/meta", this.#opts());
46
+ }
47
+
48
+ /**
49
+ * POST /api/payments/charge
50
+ * Body can include gateway, amount, currency, paymentMethod, keys, returnUrl, ...
51
+ */
52
+ async charge(body) {
53
+ return requestJson(this.baseUrl, "/api/payments/charge", {
54
+ ...this.#opts(),
55
+ method: "POST",
56
+ body,
57
+ });
58
+ }
59
+
60
+ /** GET /api/payments/:id */
61
+ async getPayment(id) {
62
+ return requestJson(this.baseUrl, `/api/payments/${encodeURIComponent(id)}`, this.#opts());
63
+ }
64
+
65
+ /** POST /api/payments/:id/refresh */
66
+ async refresh(id, keys) {
67
+ return requestJson(this.baseUrl, `/api/payments/${encodeURIComponent(id)}/refresh`, {
68
+ ...this.#opts(),
69
+ method: "POST",
70
+ body: keys ? { keys } : {},
71
+ });
72
+ }
73
+
74
+ /** Yokke EDC: create attempt */
75
+ async createEdcAttempt(body) {
76
+ return requestJson(this.baseUrl, "/api/payments/attempts", {
77
+ ...this.#opts(),
78
+ method: "POST",
79
+ body,
80
+ });
81
+ }
82
+
83
+ /** Yokke EDC: report terminal result */
84
+ async reportTerminalResult(id, body) {
85
+ return requestJson(
86
+ this.baseUrl,
87
+ `/api/payments/attempts/${encodeURIComponent(id)}/terminal-result`,
88
+ {
89
+ ...this.#opts(),
90
+ method: "POST",
91
+ body,
92
+ }
93
+ );
94
+ }
95
+
96
+ /**
97
+ * Open hosted checkout in the current window, new tab, or return the URL.
98
+ * Supports Xendit/Stripe/Omise checkoutUrl and Midtrans redirect_url.
99
+ */
100
+ openCheckout(paymentOrUrl, { target = "_self" } = {}) {
101
+ const url =
102
+ typeof paymentOrUrl === "string"
103
+ ? paymentOrUrl
104
+ : paymentOrUrl?.checkoutUrl ||
105
+ paymentOrUrl?.payment?.checkoutUrl ||
106
+ paymentOrUrl?.qrImageUrl ||
107
+ paymentOrUrl?.payment?.qrImageUrl;
108
+
109
+ if (!url) {
110
+ throw new Error("No checkoutUrl on payment result");
111
+ }
112
+
113
+ if (typeof window === "undefined") {
114
+ return url;
115
+ }
116
+
117
+ if (target === "_blank") {
118
+ window.open(url, "_blank", "noopener,noreferrer");
119
+ } else {
120
+ window.location.assign(url);
121
+ }
122
+ return url;
123
+ }
124
+
125
+ /** Razorpay Checkout helper — loads Razorpay.js and opens modal (browser only). */
126
+ async openRazorpayCheckout(payment, options = {}) {
127
+ const checkout = payment?.checkout || payment?.payment?.checkout;
128
+ if (!checkout?.keyId || !checkout?.orderId) {
129
+ throw new Error("Razorpay checkout payload missing (keyId / orderId)");
130
+ }
131
+
132
+ await loadScript("https://checkout.razorpay.com/v1/checkout.js");
133
+ if (typeof window === "undefined" || !window.Razorpay) {
134
+ throw new Error("Razorpay SDK failed to load");
135
+ }
136
+
137
+ return new Promise((resolve, reject) => {
138
+ const rzp = new window.Razorpay({
139
+ key: checkout.keyId,
140
+ amount: checkout.amount,
141
+ currency: checkout.currency,
142
+ name: checkout.name || "Payment",
143
+ order_id: checkout.orderId,
144
+ prefill: checkout.prefill || {},
145
+ handler: (response) => resolve(response),
146
+ modal: {
147
+ ondismiss: () => reject(new Error("Razorpay checkout dismissed")),
148
+ },
149
+ ...options,
150
+ });
151
+ rzp.open();
152
+ });
153
+ }
154
+ }
155
+
156
+ function loadScript(src) {
157
+ if (typeof document === "undefined") {
158
+ return Promise.reject(new Error("document not available"));
159
+ }
160
+ const existing = document.querySelector(`script[src="${src}"]`);
161
+ if (existing) return Promise.resolve();
162
+
163
+ return new Promise((resolve, reject) => {
164
+ const s = document.createElement("script");
165
+ s.src = src;
166
+ s.async = true;
167
+ s.onload = () => resolve();
168
+ s.onerror = () => reject(new Error(`Failed to load ${src}`));
169
+ document.head.appendChild(s);
170
+ });
171
+ }
172
+
173
+ export { ClientHttpError };
@@ -0,0 +1,39 @@
1
+ export class ClientHttpError extends Error {
2
+ constructor(message, status, details) {
3
+ super(message);
4
+ this.name = "ClientHttpError";
5
+ this.status = status;
6
+ this.details = details;
7
+ }
8
+ }
9
+
10
+ export async function requestJson(baseUrl, path, { method = "GET", body, apiKey, headers } = {}) {
11
+ const url = `${String(baseUrl).replace(/\/$/, "")}${path}`;
12
+ const res = await fetch(url, {
13
+ method,
14
+ headers: {
15
+ Accept: "application/json",
16
+ ...(body ? { "Content-Type": "application/json" } : {}),
17
+ ...(apiKey ? { "x-api-key": apiKey } : {}),
18
+ ...(headers || {}),
19
+ },
20
+ body: body ? JSON.stringify(body) : undefined,
21
+ });
22
+
23
+ let json = null;
24
+ try {
25
+ json = await res.json();
26
+ } catch {
27
+ json = null;
28
+ }
29
+
30
+ if (!res.ok) {
31
+ throw new ClientHttpError(
32
+ json?.error || `Request failed (${res.status})`,
33
+ res.status,
34
+ json?.details || json
35
+ );
36
+ }
37
+
38
+ return json;
39
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Browser / React entry — no Express.
3
+ *
4
+ * @example
5
+ * import { PaymentClient, redirectToCheckout } from "multi-payment-system/client";
6
+ */
7
+ export { PaymentClient, ClientHttpError } from "./PaymentClient.js";
8
+ export { requestJson } from "./http.js";
9
+
10
+ export {
11
+ listGateways,
12
+ GATEWAY_NAMES,
13
+ gatewayRegionsHint,
14
+ SUPPORTED_CURRENCIES,
15
+ } from "../meta.js";
16
+
17
+ export {
18
+ toMinorUnits,
19
+ toMajorUnits,
20
+ formatMajor,
21
+ normalizeCurrency,
22
+ } from "../utils/money.js";
23
+
24
+ export {
25
+ normalizeMethods,
26
+ mapMethodsForGateway,
27
+ PAYMENT_METHOD_HINTS,
28
+ } from "../utils/paymentMethods.js";
29
+
30
+ import { PaymentClient } from "./PaymentClient.js";
31
+
32
+ /** Open hosted checkout URL (Xendit / Stripe / Omise / Midtrans). */
33
+ export function redirectToCheckout(paymentOrUrl, options) {
34
+ return new PaymentClient({ baseUrl: "https://local" }).openCheckout(paymentOrUrl, options);
35
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * React helpers. Peer-depends on react.
3
+ *
4
+ * @example
5
+ * import { usePaymentClient } from "multi-payment-system/react";
6
+ *
7
+ * function PayButton() {
8
+ * const { chargeAndRedirect, loading, error } = usePaymentClient({
9
+ * baseUrl: import.meta.env.VITE_PAY_API,
10
+ * });
11
+ *
12
+ * return (
13
+ * <button
14
+ * disabled={loading}
15
+ * onClick={() =>
16
+ * chargeAndRedirect({
17
+ * gateway: "xendit",
18
+ * amount: 30,
19
+ * currency: "SGD",
20
+ * paymentMethod: "card",
21
+ * keys: { secretKey: "..." }, // better: store keys only on server
22
+ * returnUrl: window.location.origin + "/success",
23
+ * })
24
+ * }
25
+ * >
26
+ * {loading ? "Opening…" : "Pay"}
27
+ * </button>
28
+ * );
29
+ * }
30
+ */
31
+ import { useCallback, useMemo, useState } from "react";
32
+ import { PaymentClient } from "./PaymentClient.js";
33
+
34
+ export function usePaymentClient({ baseUrl, apiKey, headers } = {}) {
35
+ const client = useMemo(
36
+ () => new PaymentClient({ baseUrl, apiKey, headers }),
37
+ [baseUrl, apiKey, headers]
38
+ );
39
+
40
+ const [loading, setLoading] = useState(false);
41
+ const [error, setError] = useState(null);
42
+ const [payment, setPayment] = useState(null);
43
+
44
+ const charge = useCallback(
45
+ async (body) => {
46
+ setLoading(true);
47
+ setError(null);
48
+ try {
49
+ const res = await client.charge(body);
50
+ setPayment(res.payment || res);
51
+ return res;
52
+ } catch (err) {
53
+ setError(err);
54
+ throw err;
55
+ } finally {
56
+ setLoading(false);
57
+ }
58
+ },
59
+ [client]
60
+ );
61
+
62
+ const chargeAndRedirect = useCallback(
63
+ async (body, openOptions) => {
64
+ const res = await charge(body);
65
+ const pay = res.payment || res;
66
+ if (pay?.checkout) {
67
+ await client.openRazorpayCheckout(pay, openOptions);
68
+ } else {
69
+ client.openCheckout(pay, openOptions);
70
+ }
71
+ return res;
72
+ },
73
+ [charge, client]
74
+ );
75
+
76
+ const refresh = useCallback(
77
+ async (id, keys) => {
78
+ setLoading(true);
79
+ setError(null);
80
+ try {
81
+ const res = await client.refresh(id, keys);
82
+ setPayment(res.payment || res);
83
+ return res;
84
+ } catch (err) {
85
+ setError(err);
86
+ throw err;
87
+ } finally {
88
+ setLoading(false);
89
+ }
90
+ },
91
+ [client]
92
+ );
93
+
94
+ return {
95
+ client,
96
+ loading,
97
+ error,
98
+ payment,
99
+ charge,
100
+ chargeAndRedirect,
101
+ refresh,
102
+ openCheckout: (p, opts) => client.openCheckout(p, opts),
103
+ openRazorpayCheckout: (p, opts) => client.openRazorpayCheckout(p, opts),
104
+ };
105
+ }
106
+
107
+ export { PaymentClient } from "./PaymentClient.js";
@@ -3,6 +3,7 @@ import { BaseGateway } from "./BaseGateway.js";
3
3
  import { toMajorUnits, formatMajor } from "../utils/money.js";
4
4
  import { AppError } from "../utils/errors.js";
5
5
  import { normalizeMethods, mapMethodsForGateway } from "../utils/paymentMethods.js";
6
+ import { toBase64 } from "../utils/base64.js";
6
7
 
7
8
  /**
8
9
  * Midtrans — Indonesia-focused (Snap + Core API).
@@ -26,7 +27,7 @@ export class MidtransGateway extends BaseGateway {
26
27
  }
27
28
 
28
29
  authHeader(serverKey) {
29
- const token = Buffer.from(`${serverKey}:`).toString("base64");
30
+ const token = toBase64(`${serverKey}:`);
30
31
  return { Authorization: `Basic ${token}`, "Content-Type": "application/json" };
31
32
  }
32
33
 
@@ -61,34 +61,10 @@ export class XenditGateway extends BaseGateway {
61
61
  gatewayRef: data.id,
62
62
  checkoutUrl: data.invoice_url,
63
63
  paymentMethods: methods.length ? methods : ["all"],
64
- xenditPaymentMethods: xenditMethods,
65
64
  amountDisplay: formatMajor(ctx.amountMinor, ctx.currency),
66
65
  raw: data,
67
66
  };
68
67
  } catch (err) {
69
- // If filtered methods are wrong for this business, retry with all activated channels
70
- if (
71
- xenditMethods.length &&
72
- err.response?.data?.error_code === "UNAVAILABLE_PAYMENT_METHOD_ERROR"
73
- ) {
74
- delete payload.payment_methods;
75
- try {
76
- const { data } = await api.post("/v2/invoices", payload);
77
- return {
78
- status: this.normalizeStatus(data.status),
79
- gatewayRef: data.id,
80
- checkoutUrl: data.invoice_url,
81
- paymentMethods: methods.length ? methods : ["all"],
82
- xenditPaymentMethods: [],
83
- warning:
84
- `Requested channels ${xenditMethods.join(",")} unavailable on this Xendit business; invoice opened with all activated methods.`,
85
- amountDisplay: formatMajor(ctx.amountMinor, ctx.currency),
86
- raw: data,
87
- };
88
- } catch (retryErr) {
89
- throw this.wrap(retryErr);
90
- }
91
- }
92
68
  throw this.wrap(err);
93
69
  }
94
70
  }
package/src/index.js CHANGED
@@ -1,14 +1,12 @@
1
1
  /**
2
- * multi-payment-system
2
+ * multi-payment-system — Node / server SDK
3
3
  *
4
- * @example
5
- * import { XenditGateway, charge, getStatus } from "multi-payment-system";
6
- *
7
- * const xendit = new XenditGateway();
8
- * const pay = await xendit.createPayment({ ... });
4
+ * For React / browser use:
5
+ * import { PaymentClient } from "multi-payment-system/client";
6
+ * import { usePaymentClient } from "multi-payment-system/react";
9
7
  *
10
- * // or one-liner helpers:
11
- * const result = await charge({ gateway: "xendit", amount: 30, currency: "SGD", keys: {...}, paymentMethod: "card" });
8
+ * @example
9
+ * import { XenditGateway, charge } from "multi-payment-system";
12
10
  */
13
11
 
14
12
  export { BaseGateway } from "./gateways/BaseGateway.js";
@@ -45,3 +43,6 @@ export {
45
43
  export { AppError } from "./utils/errors.js";
46
44
 
47
45
  export { createApp } from "./app.js";
46
+
47
+ // Re-export browser client for convenience (still prefer /client in React apps)
48
+ export { PaymentClient, redirectToCheckout } from "./client/index.js";
package/src/meta.js ADDED
@@ -0,0 +1,35 @@
1
+ /** Lightweight meta — safe for browser bundles (no gateway class imports). */
2
+
3
+ export const GATEWAY_NAMES = ["xendit", "midtrans", "razorpay", "stripe", "omise"];
4
+
5
+ export function gatewayRegionsHint() {
6
+ return {
7
+ xendit: ["SG", "MY", "ID", "PH", "TH", "VN"],
8
+ midtrans: ["ID"],
9
+ razorpay: ["IN", "international"],
10
+ stripe: ["SG", "MY", "global"],
11
+ omise: ["SG", "MY", "TH", "JP"],
12
+ };
13
+ }
14
+
15
+ export const SUPPORTED_CURRENCIES = [
16
+ "SGD",
17
+ "MYR",
18
+ "IDR",
19
+ "THB",
20
+ "PHP",
21
+ "VND",
22
+ "USD",
23
+ "INR",
24
+ "JPY",
25
+ "AUD",
26
+ "HKD",
27
+ ];
28
+
29
+ export function listGateways() {
30
+ return {
31
+ gateways: GATEWAY_NAMES,
32
+ regions: gatewayRegionsHint(),
33
+ currencies: SUPPORTED_CURRENCIES,
34
+ };
35
+ }
@@ -88,8 +88,6 @@ export class PaymentService {
88
88
  draft.checkout = result.checkout || null;
89
89
  draft.qrImageUrl = result.qrImageUrl || null;
90
90
  draft.resolvedPaymentMethods = result.paymentMethods || null;
91
- draft.xenditPaymentMethods = result.xenditPaymentMethods || null;
92
- draft.warning = result.warning || null;
93
91
  draft.amountDisplay = result.amountDisplay || null;
94
92
  draft.gatewayRaw = result.raw || null;
95
93
  draft.updatedAt = new Date().toISOString();
@@ -0,0 +1,11 @@
1
+ /** Base64 encode that works in Node and browsers. */
2
+ export function toBase64(value) {
3
+ const str = String(value);
4
+ if (typeof Buffer !== "undefined") {
5
+ return Buffer.from(str, "utf8").toString("base64");
6
+ }
7
+ if (typeof btoa === "function") {
8
+ return btoa(unescape(encodeURIComponent(str)));
9
+ }
10
+ throw new Error("No Base64 encoder available in this environment");
11
+ }
@@ -44,9 +44,8 @@ export const PAYMENT_METHOD_HINTS = {
44
44
  qr: ["QRIS"],
45
45
  ewallet: ["GRABPAY", "OVO", "DANA", "LINKAJA", "SHOPEEPAY", "GCASH", "PAYMAYA"],
46
46
  bank: ["VIRTUAL_ACCOUNT", "DIRECT_DEBIT"],
47
- // Xendit SG PayNow QR channel code is SGQR (not PAYNOW)
48
- paynow: ["SGQR"],
49
- sgqr: ["SGQR"],
47
+ paynow: ["PAYNOW"],
48
+ sgqr: ["PAYNOW"],
50
49
  promptpay: ["PROMPTPAY"],
51
50
  grabpay: ["GRABPAY"],
52
51
  ovo: ["OVO"],
@@ -134,9 +133,8 @@ export function mapMethodsForGateway(gateway, methods, currency) {
134
133
  const out = [];
135
134
  for (const m of methods) {
136
135
  if (m === "qr" || m === "paynow" || m === "sgqr") {
137
- if (cur === "SGD") out.push("SGQR");
136
+ if (cur === "SGD") out.push("PAYNOW");
138
137
  else if (cur === "THB") out.push("PROMPTPAY");
139
- else if (cur === "PHP") out.push("QRPH");
140
138
  else out.push("QRIS");
141
139
  continue;
142
140
  }