@vexpay/js 0.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/LICENSE +21 -0
- package/README.md +107 -0
- package/dist/index.cjs +170 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +69 -0
- package/dist/index.d.ts +69 -0
- package/dist/index.js +147 -0
- package/dist/index.js.map +1 -0
- package/dist/vexpay.global.js +2 -0
- package/dist/vexpay.global.js.map +1 -0
- package/package.json +61 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 VEXPay
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# VEXPay.js
|
|
2
|
+
|
|
3
|
+
Embed [VEXPay](https://vexwallet.co/vexpay)'s hosted checkout on your own website. Buyers pay with Pago Móvil C2P or card inside a VEXPay iframe; card numbers, bank codes and OTPs never touch your page — you only receive ids and status codes.
|
|
4
|
+
|
|
5
|
+
- Zero dependencies, under 10 kB gzipped
|
|
6
|
+
- ESM, CommonJS and a `<script>` build (`window.VexPay`)
|
|
7
|
+
- Typed events: `ready`, `completed`, `failed`, `expired`, `error`
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm install @vexpay/js
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or from a CDN:
|
|
16
|
+
|
|
17
|
+
```html
|
|
18
|
+
<script src="https://cdn.jsdelivr.net/npm/@vexpay/js@0.1.0/dist/vexpay.global.js"></script>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## How it works
|
|
22
|
+
|
|
23
|
+
1. **Server:** create a checkout session with your secret API key (e.g. with [`@vexpay/node`](https://www.npmjs.com/package/@vexpay/node)), listing the site origins allowed to embed it.
|
|
24
|
+
2. **Browser:** pass the session's `clientSecret` to `VexPay.checkout()` and mount it.
|
|
25
|
+
3. **Server:** fulfil the order only after confirming the payment — via the `payment.*` webhook or by retrieving the session.
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
// server.js
|
|
29
|
+
const session = await vexpay.checkout.sessions.create({
|
|
30
|
+
amountUsd: 25,
|
|
31
|
+
reference: 'order-1042',
|
|
32
|
+
allowedOrigins: ['https://shop.example'],
|
|
33
|
+
});
|
|
34
|
+
// Send session.clientSecret to the browser. Never log or store it.
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Embed the checkout
|
|
38
|
+
|
|
39
|
+
```html
|
|
40
|
+
<div id="vexpay-checkout"></div>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
import { checkout } from '@vexpay/js';
|
|
45
|
+
|
|
46
|
+
const pay = checkout({ clientSecret }).mount('#vexpay-checkout');
|
|
47
|
+
|
|
48
|
+
pay.on('ready', ({ sessionId }) => {
|
|
49
|
+
// The checkout loaded and accepted this page.
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
pay.on('completed', ({ sessionId, paymentId }) => {
|
|
53
|
+
// Advisory: confirm on your server before fulfilling.
|
|
54
|
+
window.location.assign(`/orders/confirm?session=${sessionId}`);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
pay.on('failed', ({ code }) => {
|
|
58
|
+
// The buyer can retry until the session expires.
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
pay.on('expired', () => {
|
|
62
|
+
// Create a new session.
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
pay.on('error', ({ code, message }) => {
|
|
66
|
+
// origin_not_allowed | invalid_client_secret | mount_target_not_found | …
|
|
67
|
+
console.error(code, message);
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
With the `<script>` build, the same API lives on `window.VexPay`:
|
|
72
|
+
|
|
73
|
+
```html
|
|
74
|
+
<script>
|
|
75
|
+
VexPay.checkout({ clientSecret: '{{ client_secret }}' }).mount('#vexpay-checkout');
|
|
76
|
+
</script>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`on()` returns an unsubscribe function; `off(event, handler)` also works. Call `unmount()` to remove the checkout and stop listening.
|
|
80
|
+
|
|
81
|
+
## Redirect instead of embedding
|
|
82
|
+
|
|
83
|
+
```js
|
|
84
|
+
import { redirectToCheckout } from '@vexpay/js';
|
|
85
|
+
|
|
86
|
+
redirectToCheckout({ clientSecret });
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Sends the buyer to the hosted checkout page. No `allowedOrigins` needed.
|
|
90
|
+
|
|
91
|
+
## Options
|
|
92
|
+
|
|
93
|
+
| Option | Default | |
|
|
94
|
+
|---|---|---|
|
|
95
|
+
| `clientSecret` | — | From `POST /v1/checkout/sessions`. Required. |
|
|
96
|
+
| `checkoutOrigin` | `https://pay.vexwallet.co` | Where the hosted checkout lives. |
|
|
97
|
+
| `readyTimeoutMs` | `15000` | How long to wait for the checkout to load before emitting `error` with `origin_not_allowed`. |
|
|
98
|
+
|
|
99
|
+
## Troubleshooting
|
|
100
|
+
|
|
101
|
+
- **`origin_not_allowed`** — the page's origin is not in the session's `allowedOrigins`. Live sessions accept `https://` origins only; test-mode sessions also accept `http://localhost:*`.
|
|
102
|
+
- **`invalid_client_secret`** — the secret is wrong or belongs to another session. Create a fresh session per checkout.
|
|
103
|
+
- **Nothing happens on `completed`** — events are advisory. Always confirm the payment on your server before shipping.
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
Checkout: () => Checkout,
|
|
24
|
+
DEFAULT_CHECKOUT_ORIGIN: () => DEFAULT_CHECKOUT_ORIGIN,
|
|
25
|
+
VERSION: () => VERSION,
|
|
26
|
+
checkout: () => checkout,
|
|
27
|
+
redirectToCheckout: () => redirectToCheckout
|
|
28
|
+
});
|
|
29
|
+
module.exports = __toCommonJS(index_exports);
|
|
30
|
+
|
|
31
|
+
// src/checkout.ts
|
|
32
|
+
var DEFAULT_CHECKOUT_ORIGIN = "https://pay.vexwallet.co";
|
|
33
|
+
var DEFAULT_READY_TIMEOUT_MS = 15e3;
|
|
34
|
+
var PREFIX = "vexpay:";
|
|
35
|
+
var SECRET_PATTERN = /^([a-z0-9-]+)_secret_[A-Za-z0-9_-]{20,}$/;
|
|
36
|
+
function parseSecret(clientSecret) {
|
|
37
|
+
const match = typeof clientSecret === "string" ? SECRET_PATTERN.exec(clientSecret) : null;
|
|
38
|
+
if (!match) {
|
|
39
|
+
throw new Error("VexPay: clientSecret is missing or malformed. Pass the value returned by POST /v1/checkout/sessions.");
|
|
40
|
+
}
|
|
41
|
+
return match[1];
|
|
42
|
+
}
|
|
43
|
+
function normalizeOrigin(origin) {
|
|
44
|
+
return new URL(origin).origin;
|
|
45
|
+
}
|
|
46
|
+
var Checkout = class {
|
|
47
|
+
constructor(options) {
|
|
48
|
+
this.options = options;
|
|
49
|
+
this.handlers = /* @__PURE__ */ new Map();
|
|
50
|
+
this.iframe = null;
|
|
51
|
+
this.sessionId = "";
|
|
52
|
+
this.onMessage = (event) => this.handleMessage(event);
|
|
53
|
+
var _a, _b;
|
|
54
|
+
this.slug = parseSecret(options.clientSecret);
|
|
55
|
+
this.origin = normalizeOrigin((_a = options.checkoutOrigin) != null ? _a : DEFAULT_CHECKOUT_ORIGIN);
|
|
56
|
+
this.readyTimeoutMs = (_b = options.readyTimeoutMs) != null ? _b : DEFAULT_READY_TIMEOUT_MS;
|
|
57
|
+
}
|
|
58
|
+
/** Subscribe to an event. Returns an unsubscribe function. */
|
|
59
|
+
on(event, handler) {
|
|
60
|
+
let set = this.handlers.get(event);
|
|
61
|
+
if (!set) this.handlers.set(event, set = /* @__PURE__ */ new Set());
|
|
62
|
+
set.add(handler);
|
|
63
|
+
return () => this.off(event, handler);
|
|
64
|
+
}
|
|
65
|
+
off(event, handler) {
|
|
66
|
+
var _a;
|
|
67
|
+
(_a = this.handlers.get(event)) == null ? void 0 : _a.delete(handler);
|
|
68
|
+
}
|
|
69
|
+
/** Render the checkout inside `target` (an element or a CSS selector). */
|
|
70
|
+
mount(target) {
|
|
71
|
+
if (this.iframe) this.unmount();
|
|
72
|
+
const container = typeof target === "string" ? document.querySelector(target) : target;
|
|
73
|
+
if (!container) {
|
|
74
|
+
this.emit("error", { code: "mount_target_not_found", message: `VexPay: no element matches ${String(target)}.` });
|
|
75
|
+
return this;
|
|
76
|
+
}
|
|
77
|
+
const iframe = document.createElement("iframe");
|
|
78
|
+
iframe.src = `${this.origin}/pay/${encodeURIComponent(this.slug)}?embed=1`;
|
|
79
|
+
iframe.title = "VEXPay checkout";
|
|
80
|
+
iframe.allow = "payment";
|
|
81
|
+
iframe.setAttribute(
|
|
82
|
+
"sandbox",
|
|
83
|
+
"allow-scripts allow-forms allow-same-origin allow-popups allow-top-navigation-by-user-activation"
|
|
84
|
+
);
|
|
85
|
+
iframe.style.cssText = "width:100%;min-height:480px;border:0;display:block;background:transparent;";
|
|
86
|
+
this.iframe = iframe;
|
|
87
|
+
window.addEventListener("message", this.onMessage);
|
|
88
|
+
container.appendChild(iframe);
|
|
89
|
+
this.readyTimer = setTimeout(() => {
|
|
90
|
+
this.emit("error", {
|
|
91
|
+
code: "origin_not_allowed",
|
|
92
|
+
message: `VexPay: the checkout did not load. Add ${window.location.origin} to the session's allowedOrigins.`
|
|
93
|
+
});
|
|
94
|
+
}, this.readyTimeoutMs);
|
|
95
|
+
return this;
|
|
96
|
+
}
|
|
97
|
+
/** Remove the checkout from the page and stop listening. */
|
|
98
|
+
unmount() {
|
|
99
|
+
var _a;
|
|
100
|
+
clearTimeout(this.readyTimer);
|
|
101
|
+
window.removeEventListener("message", this.onMessage);
|
|
102
|
+
(_a = this.iframe) == null ? void 0 : _a.remove();
|
|
103
|
+
this.iframe = null;
|
|
104
|
+
}
|
|
105
|
+
emit(event, payload) {
|
|
106
|
+
var _a;
|
|
107
|
+
for (const handler of (_a = this.handlers.get(event)) != null ? _a : []) {
|
|
108
|
+
try {
|
|
109
|
+
handler(payload);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
setTimeout(() => {
|
|
112
|
+
throw error;
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
handleMessage(event) {
|
|
118
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
119
|
+
if (!this.iframe || event.source !== this.iframe.contentWindow || event.origin !== this.origin) return;
|
|
120
|
+
const data = event.data;
|
|
121
|
+
if (!data || typeof data.type !== "string" || !data.type.startsWith(PREFIX)) return;
|
|
122
|
+
switch (data.type.slice(PREFIX.length)) {
|
|
123
|
+
case "ready":
|
|
124
|
+
(_a = this.iframe.contentWindow) == null ? void 0 : _a.postMessage(
|
|
125
|
+
{ type: `${PREFIX}init`, clientSecret: this.options.clientSecret },
|
|
126
|
+
this.origin
|
|
127
|
+
);
|
|
128
|
+
return;
|
|
129
|
+
case "verified":
|
|
130
|
+
clearTimeout(this.readyTimer);
|
|
131
|
+
this.sessionId = String((_b = data.sessionId) != null ? _b : "");
|
|
132
|
+
this.emit("ready", { sessionId: this.sessionId });
|
|
133
|
+
return;
|
|
134
|
+
case "resize":
|
|
135
|
+
if (typeof data.height === "number" && data.height > 0) {
|
|
136
|
+
this.iframe.style.height = `${Math.ceil(data.height)}px`;
|
|
137
|
+
}
|
|
138
|
+
return;
|
|
139
|
+
case "completed":
|
|
140
|
+
this.emit("completed", {
|
|
141
|
+
sessionId: String((_c = data.sessionId) != null ? _c : this.sessionId),
|
|
142
|
+
paymentId: typeof data.paymentId === "string" ? data.paymentId : null
|
|
143
|
+
});
|
|
144
|
+
return;
|
|
145
|
+
case "failed":
|
|
146
|
+
this.emit("failed", { sessionId: String((_d = data.sessionId) != null ? _d : this.sessionId), code: String((_e = data.code) != null ? _e : "payment_failed") });
|
|
147
|
+
return;
|
|
148
|
+
case "expired":
|
|
149
|
+
this.emit("expired", { sessionId: String((_f = data.sessionId) != null ? _f : this.sessionId) });
|
|
150
|
+
return;
|
|
151
|
+
case "error":
|
|
152
|
+
clearTimeout(this.readyTimer);
|
|
153
|
+
this.emit("error", { code: String((_g = data.code) != null ? _g : "unknown"), message: String((_h = data.message) != null ? _h : "Checkout error.") });
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
function checkout(options) {
|
|
159
|
+
return new Checkout(options);
|
|
160
|
+
}
|
|
161
|
+
function redirectToCheckout(options) {
|
|
162
|
+
var _a;
|
|
163
|
+
const slug = parseSecret(options.clientSecret);
|
|
164
|
+
const origin = normalizeOrigin((_a = options.checkoutOrigin) != null ? _a : DEFAULT_CHECKOUT_ORIGIN);
|
|
165
|
+
window.location.assign(`${origin}/pay/${encodeURIComponent(slug)}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/index.ts
|
|
169
|
+
var VERSION = "0.1.0";
|
|
170
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/checkout.ts"],"sourcesContent":["export {\n Checkout,\n checkout,\n redirectToCheckout,\n DEFAULT_CHECKOUT_ORIGIN,\n type CheckoutEventName,\n type CheckoutEvents,\n type CheckoutOptions,\n} from './checkout';\n\nexport const VERSION = '0.1.0';\n","export const DEFAULT_CHECKOUT_ORIGIN = 'https://pay.vexwallet.co';\nconst DEFAULT_READY_TIMEOUT_MS = 15_000;\nconst PREFIX = 'vexpay:';\nconst SECRET_PATTERN = /^([a-z0-9-]+)_secret_[A-Za-z0-9_-]{20,}$/;\n\nexport interface CheckoutOptions {\n /** `clientSecret` from `POST /v1/checkout/sessions` (created on your server). */\n clientSecret: string;\n /** Where the hosted checkout lives. Default `https://pay.vexwallet.co`. */\n checkoutOrigin?: string;\n /** How long to wait for the checkout to load before emitting `origin_not_allowed`. Default 15 s. */\n readyTimeoutMs?: number;\n}\n\nexport interface CheckoutEvents {\n /** The checkout loaded and accepted this page. */\n ready: { sessionId: string };\n /** Payment succeeded. Advisory — confirm on your server before fulfilling. */\n completed: { sessionId: string; paymentId: string | null };\n /** A payment attempt failed; the buyer can retry until the session expires. */\n failed: { sessionId: string; code: string };\n /** The session expired. Create a new one. */\n expired: { sessionId: string };\n /** The checkout cannot be shown or used here. */\n error: { code: 'origin_not_allowed' | 'invalid_client_secret' | 'mount_target_not_found' | string; message: string };\n}\n\nexport type CheckoutEventName = keyof CheckoutEvents;\ntype Handler<E extends CheckoutEventName> = (payload: CheckoutEvents[E]) => void;\n\nfunction parseSecret(clientSecret: unknown): string {\n const match = typeof clientSecret === 'string' ? SECRET_PATTERN.exec(clientSecret) : null;\n if (!match) {\n throw new Error('VexPay: clientSecret is missing or malformed. Pass the value returned by POST /v1/checkout/sessions.');\n }\n return match[1]!;\n}\n\nfunction normalizeOrigin(origin: string): string {\n return new URL(origin).origin;\n}\n\n/**\n * An embedded checkout. Card data and bank codes are entered inside VEXPay's\n * iframe and never reach your page — you only receive ids and status codes.\n */\nexport class Checkout {\n private readonly slug: string;\n private readonly origin: string;\n private readonly readyTimeoutMs: number;\n private readonly handlers = new Map<CheckoutEventName, Set<(payload: never) => void>>();\n private iframe: HTMLIFrameElement | null = null;\n private sessionId = '';\n private readyTimer: ReturnType<typeof setTimeout> | undefined;\n private readonly onMessage = (event: MessageEvent) => this.handleMessage(event);\n\n constructor(private readonly options: CheckoutOptions) {\n this.slug = parseSecret(options.clientSecret);\n this.origin = normalizeOrigin(options.checkoutOrigin ?? DEFAULT_CHECKOUT_ORIGIN);\n this.readyTimeoutMs = options.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS;\n }\n\n /** Subscribe to an event. Returns an unsubscribe function. */\n on<E extends CheckoutEventName>(event: E, handler: Handler<E>): () => void {\n let set = this.handlers.get(event);\n if (!set) this.handlers.set(event, (set = new Set()));\n set.add(handler as (payload: never) => void);\n return () => this.off(event, handler);\n }\n\n off<E extends CheckoutEventName>(event: E, handler: Handler<E>): void {\n this.handlers.get(event)?.delete(handler as (payload: never) => void);\n }\n\n /** Render the checkout inside `target` (an element or a CSS selector). */\n mount(target: string | HTMLElement): this {\n if (this.iframe) this.unmount();\n const container = typeof target === 'string' ? document.querySelector<HTMLElement>(target) : target;\n if (!container) {\n this.emit('error', { code: 'mount_target_not_found', message: `VexPay: no element matches ${String(target)}.` });\n return this;\n }\n\n const iframe = document.createElement('iframe');\n iframe.src = `${this.origin}/pay/${encodeURIComponent(this.slug)}?embed=1`;\n iframe.title = 'VEXPay checkout';\n iframe.allow = 'payment';\n iframe.setAttribute(\n 'sandbox',\n 'allow-scripts allow-forms allow-same-origin allow-popups allow-top-navigation-by-user-activation',\n );\n iframe.style.cssText = 'width:100%;min-height:480px;border:0;display:block;background:transparent;';\n this.iframe = iframe;\n\n window.addEventListener('message', this.onMessage);\n container.appendChild(iframe);\n\n // A frame blocked by CSP frame-ancestors never announces itself.\n this.readyTimer = setTimeout(() => {\n this.emit('error', {\n code: 'origin_not_allowed',\n message: `VexPay: the checkout did not load. Add ${window.location.origin} to the session's allowedOrigins.`,\n });\n }, this.readyTimeoutMs);\n return this;\n }\n\n /** Remove the checkout from the page and stop listening. */\n unmount(): void {\n clearTimeout(this.readyTimer);\n window.removeEventListener('message', this.onMessage);\n this.iframe?.remove();\n this.iframe = null;\n }\n\n private emit<E extends CheckoutEventName>(event: E, payload: CheckoutEvents[E]): void {\n for (const handler of this.handlers.get(event) ?? []) {\n try {\n (handler as Handler<E>)(payload);\n } catch (error) {\n setTimeout(() => {\n throw error;\n });\n }\n }\n }\n\n private handleMessage(event: MessageEvent): void {\n // Only our own iframe, on the checkout origin, is trusted.\n if (!this.iframe || event.source !== this.iframe.contentWindow || event.origin !== this.origin) return;\n const data = event.data as { type?: unknown } & Record<string, unknown>;\n if (!data || typeof data.type !== 'string' || !data.type.startsWith(PREFIX)) return;\n\n switch (data.type.slice(PREFIX.length)) {\n case 'ready':\n // The secret goes only to the checkout origin, never '*'.\n this.iframe.contentWindow?.postMessage(\n { type: `${PREFIX}init`, clientSecret: this.options.clientSecret },\n this.origin,\n );\n return;\n case 'verified':\n clearTimeout(this.readyTimer);\n this.sessionId = String(data.sessionId ?? '');\n this.emit('ready', { sessionId: this.sessionId });\n return;\n case 'resize':\n if (typeof data.height === 'number' && data.height > 0) {\n this.iframe.style.height = `${Math.ceil(data.height)}px`;\n }\n return;\n case 'completed':\n this.emit('completed', {\n sessionId: String(data.sessionId ?? this.sessionId),\n paymentId: typeof data.paymentId === 'string' ? data.paymentId : null,\n });\n return;\n case 'failed':\n this.emit('failed', { sessionId: String(data.sessionId ?? this.sessionId), code: String(data.code ?? 'payment_failed') });\n return;\n case 'expired':\n this.emit('expired', { sessionId: String(data.sessionId ?? this.sessionId) });\n return;\n case 'error':\n clearTimeout(this.readyTimer);\n this.emit('error', { code: String(data.code ?? 'unknown'), message: String(data.message ?? 'Checkout error.') });\n return;\n }\n }\n}\n\n/** Create an embedded checkout for a session. Call `.mount(el)` to show it. */\nexport function checkout(options: CheckoutOptions): Checkout {\n return new Checkout(options);\n}\n\n/** Send the buyer to the hosted checkout page instead of embedding it. */\nexport function redirectToCheckout(options: Pick<CheckoutOptions, 'clientSecret' | 'checkoutOrigin'>): void {\n const slug = parseSecret(options.clientSecret);\n const origin = normalizeOrigin(options.checkoutOrigin ?? DEFAULT_CHECKOUT_ORIGIN);\n window.location.assign(`${origin}/pay/${encodeURIComponent(slug)}`);\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
declare const DEFAULT_CHECKOUT_ORIGIN = "https://pay.vexwallet.co";
|
|
2
|
+
interface CheckoutOptions {
|
|
3
|
+
/** `clientSecret` from `POST /v1/checkout/sessions` (created on your server). */
|
|
4
|
+
clientSecret: string;
|
|
5
|
+
/** Where the hosted checkout lives. Default `https://pay.vexwallet.co`. */
|
|
6
|
+
checkoutOrigin?: string;
|
|
7
|
+
/** How long to wait for the checkout to load before emitting `origin_not_allowed`. Default 15 s. */
|
|
8
|
+
readyTimeoutMs?: number;
|
|
9
|
+
}
|
|
10
|
+
interface CheckoutEvents {
|
|
11
|
+
/** The checkout loaded and accepted this page. */
|
|
12
|
+
ready: {
|
|
13
|
+
sessionId: string;
|
|
14
|
+
};
|
|
15
|
+
/** Payment succeeded. Advisory — confirm on your server before fulfilling. */
|
|
16
|
+
completed: {
|
|
17
|
+
sessionId: string;
|
|
18
|
+
paymentId: string | null;
|
|
19
|
+
};
|
|
20
|
+
/** A payment attempt failed; the buyer can retry until the session expires. */
|
|
21
|
+
failed: {
|
|
22
|
+
sessionId: string;
|
|
23
|
+
code: string;
|
|
24
|
+
};
|
|
25
|
+
/** The session expired. Create a new one. */
|
|
26
|
+
expired: {
|
|
27
|
+
sessionId: string;
|
|
28
|
+
};
|
|
29
|
+
/** The checkout cannot be shown or used here. */
|
|
30
|
+
error: {
|
|
31
|
+
code: 'origin_not_allowed' | 'invalid_client_secret' | 'mount_target_not_found' | string;
|
|
32
|
+
message: string;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
type CheckoutEventName = keyof CheckoutEvents;
|
|
36
|
+
type Handler<E extends CheckoutEventName> = (payload: CheckoutEvents[E]) => void;
|
|
37
|
+
/**
|
|
38
|
+
* An embedded checkout. Card data and bank codes are entered inside VEXPay's
|
|
39
|
+
* iframe and never reach your page — you only receive ids and status codes.
|
|
40
|
+
*/
|
|
41
|
+
declare class Checkout {
|
|
42
|
+
private readonly options;
|
|
43
|
+
private readonly slug;
|
|
44
|
+
private readonly origin;
|
|
45
|
+
private readonly readyTimeoutMs;
|
|
46
|
+
private readonly handlers;
|
|
47
|
+
private iframe;
|
|
48
|
+
private sessionId;
|
|
49
|
+
private readyTimer;
|
|
50
|
+
private readonly onMessage;
|
|
51
|
+
constructor(options: CheckoutOptions);
|
|
52
|
+
/** Subscribe to an event. Returns an unsubscribe function. */
|
|
53
|
+
on<E extends CheckoutEventName>(event: E, handler: Handler<E>): () => void;
|
|
54
|
+
off<E extends CheckoutEventName>(event: E, handler: Handler<E>): void;
|
|
55
|
+
/** Render the checkout inside `target` (an element or a CSS selector). */
|
|
56
|
+
mount(target: string | HTMLElement): this;
|
|
57
|
+
/** Remove the checkout from the page and stop listening. */
|
|
58
|
+
unmount(): void;
|
|
59
|
+
private emit;
|
|
60
|
+
private handleMessage;
|
|
61
|
+
}
|
|
62
|
+
/** Create an embedded checkout for a session. Call `.mount(el)` to show it. */
|
|
63
|
+
declare function checkout(options: CheckoutOptions): Checkout;
|
|
64
|
+
/** Send the buyer to the hosted checkout page instead of embedding it. */
|
|
65
|
+
declare function redirectToCheckout(options: Pick<CheckoutOptions, 'clientSecret' | 'checkoutOrigin'>): void;
|
|
66
|
+
|
|
67
|
+
declare const VERSION = "0.1.0";
|
|
68
|
+
|
|
69
|
+
export { Checkout, type CheckoutEventName, type CheckoutEvents, type CheckoutOptions, DEFAULT_CHECKOUT_ORIGIN, VERSION, checkout, redirectToCheckout };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
declare const DEFAULT_CHECKOUT_ORIGIN = "https://pay.vexwallet.co";
|
|
2
|
+
interface CheckoutOptions {
|
|
3
|
+
/** `clientSecret` from `POST /v1/checkout/sessions` (created on your server). */
|
|
4
|
+
clientSecret: string;
|
|
5
|
+
/** Where the hosted checkout lives. Default `https://pay.vexwallet.co`. */
|
|
6
|
+
checkoutOrigin?: string;
|
|
7
|
+
/** How long to wait for the checkout to load before emitting `origin_not_allowed`. Default 15 s. */
|
|
8
|
+
readyTimeoutMs?: number;
|
|
9
|
+
}
|
|
10
|
+
interface CheckoutEvents {
|
|
11
|
+
/** The checkout loaded and accepted this page. */
|
|
12
|
+
ready: {
|
|
13
|
+
sessionId: string;
|
|
14
|
+
};
|
|
15
|
+
/** Payment succeeded. Advisory — confirm on your server before fulfilling. */
|
|
16
|
+
completed: {
|
|
17
|
+
sessionId: string;
|
|
18
|
+
paymentId: string | null;
|
|
19
|
+
};
|
|
20
|
+
/** A payment attempt failed; the buyer can retry until the session expires. */
|
|
21
|
+
failed: {
|
|
22
|
+
sessionId: string;
|
|
23
|
+
code: string;
|
|
24
|
+
};
|
|
25
|
+
/** The session expired. Create a new one. */
|
|
26
|
+
expired: {
|
|
27
|
+
sessionId: string;
|
|
28
|
+
};
|
|
29
|
+
/** The checkout cannot be shown or used here. */
|
|
30
|
+
error: {
|
|
31
|
+
code: 'origin_not_allowed' | 'invalid_client_secret' | 'mount_target_not_found' | string;
|
|
32
|
+
message: string;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
type CheckoutEventName = keyof CheckoutEvents;
|
|
36
|
+
type Handler<E extends CheckoutEventName> = (payload: CheckoutEvents[E]) => void;
|
|
37
|
+
/**
|
|
38
|
+
* An embedded checkout. Card data and bank codes are entered inside VEXPay's
|
|
39
|
+
* iframe and never reach your page — you only receive ids and status codes.
|
|
40
|
+
*/
|
|
41
|
+
declare class Checkout {
|
|
42
|
+
private readonly options;
|
|
43
|
+
private readonly slug;
|
|
44
|
+
private readonly origin;
|
|
45
|
+
private readonly readyTimeoutMs;
|
|
46
|
+
private readonly handlers;
|
|
47
|
+
private iframe;
|
|
48
|
+
private sessionId;
|
|
49
|
+
private readyTimer;
|
|
50
|
+
private readonly onMessage;
|
|
51
|
+
constructor(options: CheckoutOptions);
|
|
52
|
+
/** Subscribe to an event. Returns an unsubscribe function. */
|
|
53
|
+
on<E extends CheckoutEventName>(event: E, handler: Handler<E>): () => void;
|
|
54
|
+
off<E extends CheckoutEventName>(event: E, handler: Handler<E>): void;
|
|
55
|
+
/** Render the checkout inside `target` (an element or a CSS selector). */
|
|
56
|
+
mount(target: string | HTMLElement): this;
|
|
57
|
+
/** Remove the checkout from the page and stop listening. */
|
|
58
|
+
unmount(): void;
|
|
59
|
+
private emit;
|
|
60
|
+
private handleMessage;
|
|
61
|
+
}
|
|
62
|
+
/** Create an embedded checkout for a session. Call `.mount(el)` to show it. */
|
|
63
|
+
declare function checkout(options: CheckoutOptions): Checkout;
|
|
64
|
+
/** Send the buyer to the hosted checkout page instead of embedding it. */
|
|
65
|
+
declare function redirectToCheckout(options: Pick<CheckoutOptions, 'clientSecret' | 'checkoutOrigin'>): void;
|
|
66
|
+
|
|
67
|
+
declare const VERSION = "0.1.0";
|
|
68
|
+
|
|
69
|
+
export { Checkout, type CheckoutEventName, type CheckoutEvents, type CheckoutOptions, DEFAULT_CHECKOUT_ORIGIN, VERSION, checkout, redirectToCheckout };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// src/checkout.ts
|
|
2
|
+
var DEFAULT_CHECKOUT_ORIGIN = "https://pay.vexwallet.co";
|
|
3
|
+
var DEFAULT_READY_TIMEOUT_MS = 15e3;
|
|
4
|
+
var PREFIX = "vexpay:";
|
|
5
|
+
var SECRET_PATTERN = /^([a-z0-9-]+)_secret_[A-Za-z0-9_-]{20,}$/;
|
|
6
|
+
function parseSecret(clientSecret) {
|
|
7
|
+
const match = typeof clientSecret === "string" ? SECRET_PATTERN.exec(clientSecret) : null;
|
|
8
|
+
if (!match) {
|
|
9
|
+
throw new Error("VexPay: clientSecret is missing or malformed. Pass the value returned by POST /v1/checkout/sessions.");
|
|
10
|
+
}
|
|
11
|
+
return match[1];
|
|
12
|
+
}
|
|
13
|
+
function normalizeOrigin(origin) {
|
|
14
|
+
return new URL(origin).origin;
|
|
15
|
+
}
|
|
16
|
+
var Checkout = class {
|
|
17
|
+
constructor(options) {
|
|
18
|
+
this.options = options;
|
|
19
|
+
this.handlers = /* @__PURE__ */ new Map();
|
|
20
|
+
this.iframe = null;
|
|
21
|
+
this.sessionId = "";
|
|
22
|
+
this.onMessage = (event) => this.handleMessage(event);
|
|
23
|
+
var _a, _b;
|
|
24
|
+
this.slug = parseSecret(options.clientSecret);
|
|
25
|
+
this.origin = normalizeOrigin((_a = options.checkoutOrigin) != null ? _a : DEFAULT_CHECKOUT_ORIGIN);
|
|
26
|
+
this.readyTimeoutMs = (_b = options.readyTimeoutMs) != null ? _b : DEFAULT_READY_TIMEOUT_MS;
|
|
27
|
+
}
|
|
28
|
+
/** Subscribe to an event. Returns an unsubscribe function. */
|
|
29
|
+
on(event, handler) {
|
|
30
|
+
let set = this.handlers.get(event);
|
|
31
|
+
if (!set) this.handlers.set(event, set = /* @__PURE__ */ new Set());
|
|
32
|
+
set.add(handler);
|
|
33
|
+
return () => this.off(event, handler);
|
|
34
|
+
}
|
|
35
|
+
off(event, handler) {
|
|
36
|
+
var _a;
|
|
37
|
+
(_a = this.handlers.get(event)) == null ? void 0 : _a.delete(handler);
|
|
38
|
+
}
|
|
39
|
+
/** Render the checkout inside `target` (an element or a CSS selector). */
|
|
40
|
+
mount(target) {
|
|
41
|
+
if (this.iframe) this.unmount();
|
|
42
|
+
const container = typeof target === "string" ? document.querySelector(target) : target;
|
|
43
|
+
if (!container) {
|
|
44
|
+
this.emit("error", { code: "mount_target_not_found", message: `VexPay: no element matches ${String(target)}.` });
|
|
45
|
+
return this;
|
|
46
|
+
}
|
|
47
|
+
const iframe = document.createElement("iframe");
|
|
48
|
+
iframe.src = `${this.origin}/pay/${encodeURIComponent(this.slug)}?embed=1`;
|
|
49
|
+
iframe.title = "VEXPay checkout";
|
|
50
|
+
iframe.allow = "payment";
|
|
51
|
+
iframe.setAttribute(
|
|
52
|
+
"sandbox",
|
|
53
|
+
"allow-scripts allow-forms allow-same-origin allow-popups allow-top-navigation-by-user-activation"
|
|
54
|
+
);
|
|
55
|
+
iframe.style.cssText = "width:100%;min-height:480px;border:0;display:block;background:transparent;";
|
|
56
|
+
this.iframe = iframe;
|
|
57
|
+
window.addEventListener("message", this.onMessage);
|
|
58
|
+
container.appendChild(iframe);
|
|
59
|
+
this.readyTimer = setTimeout(() => {
|
|
60
|
+
this.emit("error", {
|
|
61
|
+
code: "origin_not_allowed",
|
|
62
|
+
message: `VexPay: the checkout did not load. Add ${window.location.origin} to the session's allowedOrigins.`
|
|
63
|
+
});
|
|
64
|
+
}, this.readyTimeoutMs);
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
/** Remove the checkout from the page and stop listening. */
|
|
68
|
+
unmount() {
|
|
69
|
+
var _a;
|
|
70
|
+
clearTimeout(this.readyTimer);
|
|
71
|
+
window.removeEventListener("message", this.onMessage);
|
|
72
|
+
(_a = this.iframe) == null ? void 0 : _a.remove();
|
|
73
|
+
this.iframe = null;
|
|
74
|
+
}
|
|
75
|
+
emit(event, payload) {
|
|
76
|
+
var _a;
|
|
77
|
+
for (const handler of (_a = this.handlers.get(event)) != null ? _a : []) {
|
|
78
|
+
try {
|
|
79
|
+
handler(payload);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
setTimeout(() => {
|
|
82
|
+
throw error;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
handleMessage(event) {
|
|
88
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
89
|
+
if (!this.iframe || event.source !== this.iframe.contentWindow || event.origin !== this.origin) return;
|
|
90
|
+
const data = event.data;
|
|
91
|
+
if (!data || typeof data.type !== "string" || !data.type.startsWith(PREFIX)) return;
|
|
92
|
+
switch (data.type.slice(PREFIX.length)) {
|
|
93
|
+
case "ready":
|
|
94
|
+
(_a = this.iframe.contentWindow) == null ? void 0 : _a.postMessage(
|
|
95
|
+
{ type: `${PREFIX}init`, clientSecret: this.options.clientSecret },
|
|
96
|
+
this.origin
|
|
97
|
+
);
|
|
98
|
+
return;
|
|
99
|
+
case "verified":
|
|
100
|
+
clearTimeout(this.readyTimer);
|
|
101
|
+
this.sessionId = String((_b = data.sessionId) != null ? _b : "");
|
|
102
|
+
this.emit("ready", { sessionId: this.sessionId });
|
|
103
|
+
return;
|
|
104
|
+
case "resize":
|
|
105
|
+
if (typeof data.height === "number" && data.height > 0) {
|
|
106
|
+
this.iframe.style.height = `${Math.ceil(data.height)}px`;
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
case "completed":
|
|
110
|
+
this.emit("completed", {
|
|
111
|
+
sessionId: String((_c = data.sessionId) != null ? _c : this.sessionId),
|
|
112
|
+
paymentId: typeof data.paymentId === "string" ? data.paymentId : null
|
|
113
|
+
});
|
|
114
|
+
return;
|
|
115
|
+
case "failed":
|
|
116
|
+
this.emit("failed", { sessionId: String((_d = data.sessionId) != null ? _d : this.sessionId), code: String((_e = data.code) != null ? _e : "payment_failed") });
|
|
117
|
+
return;
|
|
118
|
+
case "expired":
|
|
119
|
+
this.emit("expired", { sessionId: String((_f = data.sessionId) != null ? _f : this.sessionId) });
|
|
120
|
+
return;
|
|
121
|
+
case "error":
|
|
122
|
+
clearTimeout(this.readyTimer);
|
|
123
|
+
this.emit("error", { code: String((_g = data.code) != null ? _g : "unknown"), message: String((_h = data.message) != null ? _h : "Checkout error.") });
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
function checkout(options) {
|
|
129
|
+
return new Checkout(options);
|
|
130
|
+
}
|
|
131
|
+
function redirectToCheckout(options) {
|
|
132
|
+
var _a;
|
|
133
|
+
const slug = parseSecret(options.clientSecret);
|
|
134
|
+
const origin = normalizeOrigin((_a = options.checkoutOrigin) != null ? _a : DEFAULT_CHECKOUT_ORIGIN);
|
|
135
|
+
window.location.assign(`${origin}/pay/${encodeURIComponent(slug)}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/index.ts
|
|
139
|
+
var VERSION = "0.1.0";
|
|
140
|
+
export {
|
|
141
|
+
Checkout,
|
|
142
|
+
DEFAULT_CHECKOUT_ORIGIN,
|
|
143
|
+
VERSION,
|
|
144
|
+
checkout,
|
|
145
|
+
redirectToCheckout
|
|
146
|
+
};
|
|
147
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/checkout.ts","../src/index.ts"],"sourcesContent":["export const DEFAULT_CHECKOUT_ORIGIN = 'https://pay.vexwallet.co';\nconst DEFAULT_READY_TIMEOUT_MS = 15_000;\nconst PREFIX = 'vexpay:';\nconst SECRET_PATTERN = /^([a-z0-9-]+)_secret_[A-Za-z0-9_-]{20,}$/;\n\nexport interface CheckoutOptions {\n /** `clientSecret` from `POST /v1/checkout/sessions` (created on your server). */\n clientSecret: string;\n /** Where the hosted checkout lives. Default `https://pay.vexwallet.co`. */\n checkoutOrigin?: string;\n /** How long to wait for the checkout to load before emitting `origin_not_allowed`. Default 15 s. */\n readyTimeoutMs?: number;\n}\n\nexport interface CheckoutEvents {\n /** The checkout loaded and accepted this page. */\n ready: { sessionId: string };\n /** Payment succeeded. Advisory — confirm on your server before fulfilling. */\n completed: { sessionId: string; paymentId: string | null };\n /** A payment attempt failed; the buyer can retry until the session expires. */\n failed: { sessionId: string; code: string };\n /** The session expired. Create a new one. */\n expired: { sessionId: string };\n /** The checkout cannot be shown or used here. */\n error: { code: 'origin_not_allowed' | 'invalid_client_secret' | 'mount_target_not_found' | string; message: string };\n}\n\nexport type CheckoutEventName = keyof CheckoutEvents;\ntype Handler<E extends CheckoutEventName> = (payload: CheckoutEvents[E]) => void;\n\nfunction parseSecret(clientSecret: unknown): string {\n const match = typeof clientSecret === 'string' ? SECRET_PATTERN.exec(clientSecret) : null;\n if (!match) {\n throw new Error('VexPay: clientSecret is missing or malformed. Pass the value returned by POST /v1/checkout/sessions.');\n }\n return match[1]!;\n}\n\nfunction normalizeOrigin(origin: string): string {\n return new URL(origin).origin;\n}\n\n/**\n * An embedded checkout. Card data and bank codes are entered inside VEXPay's\n * iframe and never reach your page — you only receive ids and status codes.\n */\nexport class Checkout {\n private readonly slug: string;\n private readonly origin: string;\n private readonly readyTimeoutMs: number;\n private readonly handlers = new Map<CheckoutEventName, Set<(payload: never) => void>>();\n private iframe: HTMLIFrameElement | null = null;\n private sessionId = '';\n private readyTimer: ReturnType<typeof setTimeout> | undefined;\n private readonly onMessage = (event: MessageEvent) => this.handleMessage(event);\n\n constructor(private readonly options: CheckoutOptions) {\n this.slug = parseSecret(options.clientSecret);\n this.origin = normalizeOrigin(options.checkoutOrigin ?? DEFAULT_CHECKOUT_ORIGIN);\n this.readyTimeoutMs = options.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS;\n }\n\n /** Subscribe to an event. Returns an unsubscribe function. */\n on<E extends CheckoutEventName>(event: E, handler: Handler<E>): () => void {\n let set = this.handlers.get(event);\n if (!set) this.handlers.set(event, (set = new Set()));\n set.add(handler as (payload: never) => void);\n return () => this.off(event, handler);\n }\n\n off<E extends CheckoutEventName>(event: E, handler: Handler<E>): void {\n this.handlers.get(event)?.delete(handler as (payload: never) => void);\n }\n\n /** Render the checkout inside `target` (an element or a CSS selector). */\n mount(target: string | HTMLElement): this {\n if (this.iframe) this.unmount();\n const container = typeof target === 'string' ? document.querySelector<HTMLElement>(target) : target;\n if (!container) {\n this.emit('error', { code: 'mount_target_not_found', message: `VexPay: no element matches ${String(target)}.` });\n return this;\n }\n\n const iframe = document.createElement('iframe');\n iframe.src = `${this.origin}/pay/${encodeURIComponent(this.slug)}?embed=1`;\n iframe.title = 'VEXPay checkout';\n iframe.allow = 'payment';\n iframe.setAttribute(\n 'sandbox',\n 'allow-scripts allow-forms allow-same-origin allow-popups allow-top-navigation-by-user-activation',\n );\n iframe.style.cssText = 'width:100%;min-height:480px;border:0;display:block;background:transparent;';\n this.iframe = iframe;\n\n window.addEventListener('message', this.onMessage);\n container.appendChild(iframe);\n\n // A frame blocked by CSP frame-ancestors never announces itself.\n this.readyTimer = setTimeout(() => {\n this.emit('error', {\n code: 'origin_not_allowed',\n message: `VexPay: the checkout did not load. Add ${window.location.origin} to the session's allowedOrigins.`,\n });\n }, this.readyTimeoutMs);\n return this;\n }\n\n /** Remove the checkout from the page and stop listening. */\n unmount(): void {\n clearTimeout(this.readyTimer);\n window.removeEventListener('message', this.onMessage);\n this.iframe?.remove();\n this.iframe = null;\n }\n\n private emit<E extends CheckoutEventName>(event: E, payload: CheckoutEvents[E]): void {\n for (const handler of this.handlers.get(event) ?? []) {\n try {\n (handler as Handler<E>)(payload);\n } catch (error) {\n setTimeout(() => {\n throw error;\n });\n }\n }\n }\n\n private handleMessage(event: MessageEvent): void {\n // Only our own iframe, on the checkout origin, is trusted.\n if (!this.iframe || event.source !== this.iframe.contentWindow || event.origin !== this.origin) return;\n const data = event.data as { type?: unknown } & Record<string, unknown>;\n if (!data || typeof data.type !== 'string' || !data.type.startsWith(PREFIX)) return;\n\n switch (data.type.slice(PREFIX.length)) {\n case 'ready':\n // The secret goes only to the checkout origin, never '*'.\n this.iframe.contentWindow?.postMessage(\n { type: `${PREFIX}init`, clientSecret: this.options.clientSecret },\n this.origin,\n );\n return;\n case 'verified':\n clearTimeout(this.readyTimer);\n this.sessionId = String(data.sessionId ?? '');\n this.emit('ready', { sessionId: this.sessionId });\n return;\n case 'resize':\n if (typeof data.height === 'number' && data.height > 0) {\n this.iframe.style.height = `${Math.ceil(data.height)}px`;\n }\n return;\n case 'completed':\n this.emit('completed', {\n sessionId: String(data.sessionId ?? this.sessionId),\n paymentId: typeof data.paymentId === 'string' ? data.paymentId : null,\n });\n return;\n case 'failed':\n this.emit('failed', { sessionId: String(data.sessionId ?? this.sessionId), code: String(data.code ?? 'payment_failed') });\n return;\n case 'expired':\n this.emit('expired', { sessionId: String(data.sessionId ?? this.sessionId) });\n return;\n case 'error':\n clearTimeout(this.readyTimer);\n this.emit('error', { code: String(data.code ?? 'unknown'), message: String(data.message ?? 'Checkout error.') });\n return;\n }\n }\n}\n\n/** Create an embedded checkout for a session. Call `.mount(el)` to show it. */\nexport function checkout(options: CheckoutOptions): Checkout {\n return new Checkout(options);\n}\n\n/** Send the buyer to the hosted checkout page instead of embedding it. */\nexport function redirectToCheckout(options: Pick<CheckoutOptions, 'clientSecret' | 'checkoutOrigin'>): void {\n const slug = parseSecret(options.clientSecret);\n const origin = normalizeOrigin(options.checkoutOrigin ?? DEFAULT_CHECKOUT_ORIGIN);\n window.location.assign(`${origin}/pay/${encodeURIComponent(slug)}`);\n}\n","export {\n Checkout,\n checkout,\n redirectToCheckout,\n DEFAULT_CHECKOUT_ORIGIN,\n type CheckoutEventName,\n type CheckoutEvents,\n type CheckoutOptions,\n} from './checkout';\n\nexport const VERSION = '0.1.0';\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";var VexPay=(()=>{var d=Object.defineProperty;var k=Object.getOwnPropertyDescriptor;var T=Object.getOwnPropertyNames;var I=Object.prototype.hasOwnProperty;var w=(s,e)=>{for(var t in e)d(s,t,{get:e[t],enumerable:!0})},C=(s,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of T(e))!I.call(s,n)&&n!==t&&d(s,n,{get:()=>e[n],enumerable:!(i=k(e,n))||i.enumerable});return s};var _=s=>C(d({},"__esModule",{value:!0}),s);var S={};w(S,{DEFAULT_CHECKOUT_ORIGIN:()=>o,VERSION:()=>v,checkout:()=>h,redirectToCheckout:()=>u});var o="https://pay.vexwallet.co";var c="vexpay:",x=/^([a-z0-9-]+)_secret_[A-Za-z0-9_-]{20,}$/;function E(s){let e=typeof s=="string"?x.exec(s):null;if(!e)throw new Error("VexPay: clientSecret is missing or malformed. Pass the value returned by POST /v1/checkout/sessions.");return e[1]}function f(s){return new URL(s).origin}var a=class{constructor(e){this.options=e;this.handlers=new Map;this.iframe=null;this.sessionId="";this.onMessage=e=>this.handleMessage(e);var t,i;this.slug=E(e.clientSecret),this.origin=f((t=e.checkoutOrigin)!=null?t:o),this.readyTimeoutMs=(i=e.readyTimeoutMs)!=null?i:15e3}on(e,t){let i=this.handlers.get(e);return i||this.handlers.set(e,i=new Set),i.add(t),()=>this.off(e,t)}off(e,t){var i;(i=this.handlers.get(e))==null||i.delete(t)}mount(e){this.iframe&&this.unmount();let t=typeof e=="string"?document.querySelector(e):e;if(!t)return this.emit("error",{code:"mount_target_not_found",message:`VexPay: no element matches ${String(e)}.`}),this;let i=document.createElement("iframe");return i.src=`${this.origin}/pay/${encodeURIComponent(this.slug)}?embed=1`,i.title="VEXPay checkout",i.allow="payment",i.setAttribute("sandbox","allow-scripts allow-forms allow-same-origin allow-popups allow-top-navigation-by-user-activation"),i.style.cssText="width:100%;min-height:480px;border:0;display:block;background:transparent;",this.iframe=i,window.addEventListener("message",this.onMessage),t.appendChild(i),this.readyTimer=setTimeout(()=>{this.emit("error",{code:"origin_not_allowed",message:`VexPay: the checkout did not load. Add ${window.location.origin} to the session's allowedOrigins.`})},this.readyTimeoutMs),this}unmount(){var e;clearTimeout(this.readyTimer),window.removeEventListener("message",this.onMessage),(e=this.iframe)==null||e.remove(),this.iframe=null}emit(e,t){var i;for(let n of(i=this.handlers.get(e))!=null?i:[])try{n(t)}catch(r){setTimeout(()=>{throw r})}}handleMessage(e){var i,n,r,l,m,g,p,y;if(!this.iframe||e.source!==this.iframe.contentWindow||e.origin!==this.origin)return;let t=e.data;if(!(!t||typeof t.type!="string"||!t.type.startsWith(c)))switch(t.type.slice(c.length)){case"ready":(i=this.iframe.contentWindow)==null||i.postMessage({type:`${c}init`,clientSecret:this.options.clientSecret},this.origin);return;case"verified":clearTimeout(this.readyTimer),this.sessionId=String((n=t.sessionId)!=null?n:""),this.emit("ready",{sessionId:this.sessionId});return;case"resize":typeof t.height=="number"&&t.height>0&&(this.iframe.style.height=`${Math.ceil(t.height)}px`);return;case"completed":this.emit("completed",{sessionId:String((r=t.sessionId)!=null?r:this.sessionId),paymentId:typeof t.paymentId=="string"?t.paymentId:null});return;case"failed":this.emit("failed",{sessionId:String((l=t.sessionId)!=null?l:this.sessionId),code:String((m=t.code)!=null?m:"payment_failed")});return;case"expired":this.emit("expired",{sessionId:String((g=t.sessionId)!=null?g:this.sessionId)});return;case"error":clearTimeout(this.readyTimer),this.emit("error",{code:String((p=t.code)!=null?p:"unknown"),message:String((y=t.message)!=null?y:"Checkout error.")});return}}};function h(s){return new a(s)}function u(s){var i;let e=E(s.clientSecret),t=f((i=s.checkoutOrigin)!=null?i:o);window.location.assign(`${t}/pay/${encodeURIComponent(e)}`)}var v="0.1.0";return _(S);})();
|
|
2
|
+
//# sourceMappingURL=vexpay.global.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/global.ts","../src/checkout.ts","../src/index.ts"],"sourcesContent":["// CDN entry: the IIFE build assigns these exports to window.VexPay.\nexport { checkout, redirectToCheckout, DEFAULT_CHECKOUT_ORIGIN, VERSION } from './index';\n","export const DEFAULT_CHECKOUT_ORIGIN = 'https://pay.vexwallet.co';\nconst DEFAULT_READY_TIMEOUT_MS = 15_000;\nconst PREFIX = 'vexpay:';\nconst SECRET_PATTERN = /^([a-z0-9-]+)_secret_[A-Za-z0-9_-]{20,}$/;\n\nexport interface CheckoutOptions {\n /** `clientSecret` from `POST /v1/checkout/sessions` (created on your server). */\n clientSecret: string;\n /** Where the hosted checkout lives. Default `https://pay.vexwallet.co`. */\n checkoutOrigin?: string;\n /** How long to wait for the checkout to load before emitting `origin_not_allowed`. Default 15 s. */\n readyTimeoutMs?: number;\n}\n\nexport interface CheckoutEvents {\n /** The checkout loaded and accepted this page. */\n ready: { sessionId: string };\n /** Payment succeeded. Advisory — confirm on your server before fulfilling. */\n completed: { sessionId: string; paymentId: string | null };\n /** A payment attempt failed; the buyer can retry until the session expires. */\n failed: { sessionId: string; code: string };\n /** The session expired. Create a new one. */\n expired: { sessionId: string };\n /** The checkout cannot be shown or used here. */\n error: { code: 'origin_not_allowed' | 'invalid_client_secret' | 'mount_target_not_found' | string; message: string };\n}\n\nexport type CheckoutEventName = keyof CheckoutEvents;\ntype Handler<E extends CheckoutEventName> = (payload: CheckoutEvents[E]) => void;\n\nfunction parseSecret(clientSecret: unknown): string {\n const match = typeof clientSecret === 'string' ? SECRET_PATTERN.exec(clientSecret) : null;\n if (!match) {\n throw new Error('VexPay: clientSecret is missing or malformed. Pass the value returned by POST /v1/checkout/sessions.');\n }\n return match[1]!;\n}\n\nfunction normalizeOrigin(origin: string): string {\n return new URL(origin).origin;\n}\n\n/**\n * An embedded checkout. Card data and bank codes are entered inside VEXPay's\n * iframe and never reach your page — you only receive ids and status codes.\n */\nexport class Checkout {\n private readonly slug: string;\n private readonly origin: string;\n private readonly readyTimeoutMs: number;\n private readonly handlers = new Map<CheckoutEventName, Set<(payload: never) => void>>();\n private iframe: HTMLIFrameElement | null = null;\n private sessionId = '';\n private readyTimer: ReturnType<typeof setTimeout> | undefined;\n private readonly onMessage = (event: MessageEvent) => this.handleMessage(event);\n\n constructor(private readonly options: CheckoutOptions) {\n this.slug = parseSecret(options.clientSecret);\n this.origin = normalizeOrigin(options.checkoutOrigin ?? DEFAULT_CHECKOUT_ORIGIN);\n this.readyTimeoutMs = options.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS;\n }\n\n /** Subscribe to an event. Returns an unsubscribe function. */\n on<E extends CheckoutEventName>(event: E, handler: Handler<E>): () => void {\n let set = this.handlers.get(event);\n if (!set) this.handlers.set(event, (set = new Set()));\n set.add(handler as (payload: never) => void);\n return () => this.off(event, handler);\n }\n\n off<E extends CheckoutEventName>(event: E, handler: Handler<E>): void {\n this.handlers.get(event)?.delete(handler as (payload: never) => void);\n }\n\n /** Render the checkout inside `target` (an element or a CSS selector). */\n mount(target: string | HTMLElement): this {\n if (this.iframe) this.unmount();\n const container = typeof target === 'string' ? document.querySelector<HTMLElement>(target) : target;\n if (!container) {\n this.emit('error', { code: 'mount_target_not_found', message: `VexPay: no element matches ${String(target)}.` });\n return this;\n }\n\n const iframe = document.createElement('iframe');\n iframe.src = `${this.origin}/pay/${encodeURIComponent(this.slug)}?embed=1`;\n iframe.title = 'VEXPay checkout';\n iframe.allow = 'payment';\n iframe.setAttribute(\n 'sandbox',\n 'allow-scripts allow-forms allow-same-origin allow-popups allow-top-navigation-by-user-activation',\n );\n iframe.style.cssText = 'width:100%;min-height:480px;border:0;display:block;background:transparent;';\n this.iframe = iframe;\n\n window.addEventListener('message', this.onMessage);\n container.appendChild(iframe);\n\n // A frame blocked by CSP frame-ancestors never announces itself.\n this.readyTimer = setTimeout(() => {\n this.emit('error', {\n code: 'origin_not_allowed',\n message: `VexPay: the checkout did not load. Add ${window.location.origin} to the session's allowedOrigins.`,\n });\n }, this.readyTimeoutMs);\n return this;\n }\n\n /** Remove the checkout from the page and stop listening. */\n unmount(): void {\n clearTimeout(this.readyTimer);\n window.removeEventListener('message', this.onMessage);\n this.iframe?.remove();\n this.iframe = null;\n }\n\n private emit<E extends CheckoutEventName>(event: E, payload: CheckoutEvents[E]): void {\n for (const handler of this.handlers.get(event) ?? []) {\n try {\n (handler as Handler<E>)(payload);\n } catch (error) {\n setTimeout(() => {\n throw error;\n });\n }\n }\n }\n\n private handleMessage(event: MessageEvent): void {\n // Only our own iframe, on the checkout origin, is trusted.\n if (!this.iframe || event.source !== this.iframe.contentWindow || event.origin !== this.origin) return;\n const data = event.data as { type?: unknown } & Record<string, unknown>;\n if (!data || typeof data.type !== 'string' || !data.type.startsWith(PREFIX)) return;\n\n switch (data.type.slice(PREFIX.length)) {\n case 'ready':\n // The secret goes only to the checkout origin, never '*'.\n this.iframe.contentWindow?.postMessage(\n { type: `${PREFIX}init`, clientSecret: this.options.clientSecret },\n this.origin,\n );\n return;\n case 'verified':\n clearTimeout(this.readyTimer);\n this.sessionId = String(data.sessionId ?? '');\n this.emit('ready', { sessionId: this.sessionId });\n return;\n case 'resize':\n if (typeof data.height === 'number' && data.height > 0) {\n this.iframe.style.height = `${Math.ceil(data.height)}px`;\n }\n return;\n case 'completed':\n this.emit('completed', {\n sessionId: String(data.sessionId ?? this.sessionId),\n paymentId: typeof data.paymentId === 'string' ? data.paymentId : null,\n });\n return;\n case 'failed':\n this.emit('failed', { sessionId: String(data.sessionId ?? this.sessionId), code: String(data.code ?? 'payment_failed') });\n return;\n case 'expired':\n this.emit('expired', { sessionId: String(data.sessionId ?? this.sessionId) });\n return;\n case 'error':\n clearTimeout(this.readyTimer);\n this.emit('error', { code: String(data.code ?? 'unknown'), message: String(data.message ?? 'Checkout error.') });\n return;\n }\n }\n}\n\n/** Create an embedded checkout for a session. Call `.mount(el)` to show it. */\nexport function checkout(options: CheckoutOptions): Checkout {\n return new Checkout(options);\n}\n\n/** Send the buyer to the hosted checkout page instead of embedding it. */\nexport function redirectToCheckout(options: Pick<CheckoutOptions, 'clientSecret' | 'checkoutOrigin'>): void {\n const slug = parseSecret(options.clientSecret);\n const origin = normalizeOrigin(options.checkoutOrigin ?? DEFAULT_CHECKOUT_ORIGIN);\n window.location.assign(`${origin}/pay/${encodeURIComponent(slug)}`);\n}\n","export {\n Checkout,\n checkout,\n redirectToCheckout,\n DEFAULT_CHECKOUT_ORIGIN,\n type CheckoutEventName,\n type CheckoutEvents,\n type CheckoutOptions,\n} from './checkout';\n\nexport const VERSION = '0.1.0';\n"],"mappings":"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","names":["global_exports","__export","DEFAULT_CHECKOUT_ORIGIN","VERSION","checkout","redirectToCheckout","DEFAULT_CHECKOUT_ORIGIN","PREFIX","SECRET_PATTERN","parseSecret","clientSecret","match","normalizeOrigin","origin","Checkout","options","event","_a","_b","DEFAULT_CHECKOUT_ORIGIN","handler","set","target","container","iframe","payload","error","_c","_d","_e","_f","_g","_h","data","checkout","redirectToCheckout","slug","VERSION"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vexpay/js",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Embed the VEXPay checkout on your website — Pago Móvil C2P and cards, with payment details kept on VEXPay's origin.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"vexpay",
|
|
8
|
+
"venezuela",
|
|
9
|
+
"payments",
|
|
10
|
+
"checkout",
|
|
11
|
+
"pago movil",
|
|
12
|
+
"embedded checkout"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/chuventures/vexpay-js#readme",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/chuventures/vexpay-js.git"
|
|
18
|
+
},
|
|
19
|
+
"type": "module",
|
|
20
|
+
"main": "./dist/index.cjs",
|
|
21
|
+
"module": "./dist/index.js",
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"unpkg": "./dist/vexpay.global.js",
|
|
24
|
+
"jsdelivr": "./dist/vexpay.global.js",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"import": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"default": "./dist/index.js"
|
|
30
|
+
},
|
|
31
|
+
"require": {
|
|
32
|
+
"types": "./dist/index.d.cts",
|
|
33
|
+
"default": "./dist/index.cjs"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"./package.json": "./package.json"
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"dist",
|
|
40
|
+
"README.md",
|
|
41
|
+
"LICENSE"
|
|
42
|
+
],
|
|
43
|
+
"sideEffects": false,
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public",
|
|
46
|
+
"provenance": false
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"happy-dom": "^18",
|
|
50
|
+
"playwright-core": "^1",
|
|
51
|
+
"tsup": "^8",
|
|
52
|
+
"typescript": "^5",
|
|
53
|
+
"vitest": "^3"
|
|
54
|
+
},
|
|
55
|
+
"scripts": {
|
|
56
|
+
"build": "tsup && node scripts/check-size.mjs",
|
|
57
|
+
"typecheck": "tsc --noEmit",
|
|
58
|
+
"test": "vitest run",
|
|
59
|
+
"test:e2e": "node e2e/embed.e2e.mjs"
|
|
60
|
+
}
|
|
61
|
+
}
|