@vexpay/js 0.1.1-rc.0 → 0.3.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,6 +1,6 @@
1
1
  # VEXPay.js
2
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.
3
+ Embed [VEXPay](https://vexpay.app)'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
4
 
5
5
  - Zero dependencies, under 10 kB gzipped
6
6
  - ESM, CommonJS and a `<script>` build (`window.VexPay`)
@@ -88,14 +88,24 @@ redirectToCheckout({ clientSecret });
88
88
 
89
89
  Sends the buyer to the hosted checkout page. No `allowedOrigins` needed.
90
90
 
91
+ ## USDT
92
+
93
+ When USDT is enabled on your VEXPay account, sessions can offer it (`methods: ['usdt']`, or the default of every method you accept). The buyer picks a network and sends USDT to the address shown; the embedded checkout keeps polling and emits `completed` once the transfer is confirmed, which can take a few minutes. Confirm on your server before fulfilling, and don't fulfil when the `payment.completed` webhook has `underpaid: true`.
94
+
95
+ ## Testing card payments
96
+
97
+ Checkouts created with a test API key show a "Tarjetas de prueba" list in the card form. Picking a card fills the form, and each card returns a fixed outcome (approval, a bank decline, a chargeback) without charging anything. Full list: [test cards](https://docs.vexpay.app/vpos#test-card).
98
+
91
99
  ## Options
92
100
 
93
101
  | Option | Default | |
94
102
  |---|---|---|
95
103
  | `clientSecret` | — | From `POST /v1/checkout/sessions`. Required. |
96
- | `checkoutOrigin` | `https://pay.vexwallet.co` | Where the hosted checkout lives. |
104
+ | `checkoutOrigin` | `https://pay.vexpay.app` | Where the hosted checkout lives. |
97
105
  | `readyTimeoutMs` | `15000` | How long to wait for the checkout to load before emitting `error` with `origin_not_allowed`. |
98
106
 
107
+ Before 0.3.0 the default `checkoutOrigin` was `https://pay.vexwallet.co`. That host keeps serving embedded checkouts, so passing `checkoutOrigin: 'https://pay.vexwallet.co'` explicitly still works.
108
+
99
109
  ## Troubleshooting
100
110
 
101
111
  - **`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:*`.
package/dist/index.cjs CHANGED
@@ -29,7 +29,7 @@ __export(index_exports, {
29
29
  module.exports = __toCommonJS(index_exports);
30
30
 
31
31
  // src/checkout.ts
32
- var DEFAULT_CHECKOUT_ORIGIN = "https://pay.vexwallet.co";
32
+ var DEFAULT_CHECKOUT_ORIGIN = "https://pay.vexpay.app";
33
33
  var DEFAULT_READY_TIMEOUT_MS = 15e3;
34
34
  var PREFIX = "vexpay:";
35
35
  var SECRET_PATTERN = /^([a-z0-9-]+)_secret_[A-Za-z0-9_-]{20,}$/;
@@ -80,7 +80,7 @@ var Checkout = class {
80
80
  iframe.allow = "payment";
81
81
  iframe.setAttribute(
82
82
  "sandbox",
83
- "allow-scripts allow-forms allow-same-origin allow-popups allow-top-navigation-by-user-activation"
83
+ "allow-scripts allow-forms allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-top-navigation-by-user-activation"
84
84
  );
85
85
  iframe.style.cssText = "width:100%;min-height:480px;border:0;display:block;background:transparent;";
86
86
  this.iframe = iframe;
@@ -166,5 +166,5 @@ function redirectToCheckout(options) {
166
166
  }
167
167
 
168
168
  // src/index.ts
169
- var VERSION = "0.1.1-rc.0";
169
+ var VERSION = "0.3.0";
170
170
  //# sourceMappingURL=index.cjs.map
@@ -1 +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.1-rc.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":[]}
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.3.0';\n","export const DEFAULT_CHECKOUT_ORIGIN = 'https://pay.vexpay.app';\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.vexpay.app`. */\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 // allow-popups-to-escape-sandbox: USDT opens the processor's payment page in a new\n // window, which must run as a normal page (not inherit this iframe's sandbox).\n iframe.setAttribute(\n 'sandbox',\n 'allow-scripts allow-forms allow-same-origin allow-popups allow-popups-to-escape-sandbox 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 CHANGED
@@ -1,8 +1,8 @@
1
- declare const DEFAULT_CHECKOUT_ORIGIN = "https://pay.vexwallet.co";
1
+ declare const DEFAULT_CHECKOUT_ORIGIN = "https://pay.vexpay.app";
2
2
  interface CheckoutOptions {
3
3
  /** `clientSecret` from `POST /v1/checkout/sessions` (created on your server). */
4
4
  clientSecret: string;
5
- /** Where the hosted checkout lives. Default `https://pay.vexwallet.co`. */
5
+ /** Where the hosted checkout lives. Default `https://pay.vexpay.app`. */
6
6
  checkoutOrigin?: string;
7
7
  /** How long to wait for the checkout to load before emitting `origin_not_allowed`. Default 15 s. */
8
8
  readyTimeoutMs?: number;
@@ -64,6 +64,6 @@ declare function checkout(options: CheckoutOptions): Checkout;
64
64
  /** Send the buyer to the hosted checkout page instead of embedding it. */
65
65
  declare function redirectToCheckout(options: Pick<CheckoutOptions, 'clientSecret' | 'checkoutOrigin'>): void;
66
66
 
67
- declare const VERSION = "0.1.1-rc.0";
67
+ declare const VERSION = "0.3.0";
68
68
 
69
69
  export { Checkout, type CheckoutEventName, type CheckoutEvents, type CheckoutOptions, DEFAULT_CHECKOUT_ORIGIN, VERSION, checkout, redirectToCheckout };
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- declare const DEFAULT_CHECKOUT_ORIGIN = "https://pay.vexwallet.co";
1
+ declare const DEFAULT_CHECKOUT_ORIGIN = "https://pay.vexpay.app";
2
2
  interface CheckoutOptions {
3
3
  /** `clientSecret` from `POST /v1/checkout/sessions` (created on your server). */
4
4
  clientSecret: string;
5
- /** Where the hosted checkout lives. Default `https://pay.vexwallet.co`. */
5
+ /** Where the hosted checkout lives. Default `https://pay.vexpay.app`. */
6
6
  checkoutOrigin?: string;
7
7
  /** How long to wait for the checkout to load before emitting `origin_not_allowed`. Default 15 s. */
8
8
  readyTimeoutMs?: number;
@@ -64,6 +64,6 @@ declare function checkout(options: CheckoutOptions): Checkout;
64
64
  /** Send the buyer to the hosted checkout page instead of embedding it. */
65
65
  declare function redirectToCheckout(options: Pick<CheckoutOptions, 'clientSecret' | 'checkoutOrigin'>): void;
66
66
 
67
- declare const VERSION = "0.1.1-rc.0";
67
+ declare const VERSION = "0.3.0";
68
68
 
69
69
  export { Checkout, type CheckoutEventName, type CheckoutEvents, type CheckoutOptions, DEFAULT_CHECKOUT_ORIGIN, VERSION, checkout, redirectToCheckout };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/checkout.ts
2
- var DEFAULT_CHECKOUT_ORIGIN = "https://pay.vexwallet.co";
2
+ var DEFAULT_CHECKOUT_ORIGIN = "https://pay.vexpay.app";
3
3
  var DEFAULT_READY_TIMEOUT_MS = 15e3;
4
4
  var PREFIX = "vexpay:";
5
5
  var SECRET_PATTERN = /^([a-z0-9-]+)_secret_[A-Za-z0-9_-]{20,}$/;
@@ -50,7 +50,7 @@ var Checkout = class {
50
50
  iframe.allow = "payment";
51
51
  iframe.setAttribute(
52
52
  "sandbox",
53
- "allow-scripts allow-forms allow-same-origin allow-popups allow-top-navigation-by-user-activation"
53
+ "allow-scripts allow-forms allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-top-navigation-by-user-activation"
54
54
  );
55
55
  iframe.style.cssText = "width:100%;min-height:480px;border:0;display:block;background:transparent;";
56
56
  this.iframe = iframe;
@@ -136,7 +136,7 @@ function redirectToCheckout(options) {
136
136
  }
137
137
 
138
138
  // src/index.ts
139
- var VERSION = "0.1.1-rc.0";
139
+ var VERSION = "0.3.0";
140
140
  export {
141
141
  Checkout,
142
142
  DEFAULT_CHECKOUT_ORIGIN,
package/dist/index.js.map CHANGED
@@ -1 +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.1-rc.0';\n"],"mappings":";AAAO,IAAM,0BAA0B;AACvC,IAAM,2BAA2B;AACjC,IAAM,SAAS;AACf,IAAM,iBAAiB;AA2BvB,SAAS,YAAY,cAA+B;AAClD,QAAM,QAAQ,OAAO,iBAAiB,WAAW,eAAe,KAAK,YAAY,IAAI;AACrF,MAAI,CAAC,OAAO;AACV,UAAM,IAAI,MAAM,sGAAsG;AAAA,EACxH;AACA,SAAO,MAAM,CAAC;AAChB;AAEA,SAAS,gBAAgB,QAAwB;AAC/C,SAAO,IAAI,IAAI,MAAM,EAAE;AACzB;AAMO,IAAM,WAAN,MAAe;AAAA,EAUpB,YAA6B,SAA0B;AAA1B;AAN7B,SAAiB,WAAW,oBAAI,IAAsD;AACtF,SAAQ,SAAmC;AAC3C,SAAQ,YAAY;AAEpB,SAAiB,YAAY,CAAC,UAAwB,KAAK,cAAc,KAAK;AAtDhF;AAyDI,SAAK,OAAO,YAAY,QAAQ,YAAY;AAC5C,SAAK,SAAS,iBAAgB,aAAQ,mBAAR,YAA0B,uBAAuB;AAC/E,SAAK,kBAAiB,aAAQ,mBAAR,YAA0B;AAAA,EAClD;AAAA;AAAA,EAGA,GAAgC,OAAU,SAAiC;AACzE,QAAI,MAAM,KAAK,SAAS,IAAI,KAAK;AACjC,QAAI,CAAC,IAAK,MAAK,SAAS,IAAI,OAAQ,MAAM,oBAAI,IAAI,CAAE;AACpD,QAAI,IAAI,OAAmC;AAC3C,WAAO,MAAM,KAAK,IAAI,OAAO,OAAO;AAAA,EACtC;AAAA,EAEA,IAAiC,OAAU,SAA2B;AAtExE;AAuEI,eAAK,SAAS,IAAI,KAAK,MAAvB,mBAA0B,OAAO;AAAA,EACnC;AAAA;AAAA,EAGA,MAAM,QAAoC;AACxC,QAAI,KAAK,OAAQ,MAAK,QAAQ;AAC9B,UAAM,YAAY,OAAO,WAAW,WAAW,SAAS,cAA2B,MAAM,IAAI;AAC7F,QAAI,CAAC,WAAW;AACd,WAAK,KAAK,SAAS,EAAE,MAAM,0BAA0B,SAAS,8BAA8B,OAAO,MAAM,CAAC,IAAI,CAAC;AAC/G,aAAO;AAAA,IACT;AAEA,UAAM,SAAS,SAAS,cAAc,QAAQ;AAC9C,WAAO,MAAM,GAAG,KAAK,MAAM,QAAQ,mBAAmB,KAAK,IAAI,CAAC;AAChE,WAAO,QAAQ;AACf,WAAO,QAAQ;AACf,WAAO;AAAA,MACL;AAAA,MACA;AAAA,IACF;AACA,WAAO,MAAM,UAAU;AACvB,SAAK,SAAS;AAEd,WAAO,iBAAiB,WAAW,KAAK,SAAS;AACjD,cAAU,YAAY,MAAM;AAG5B,SAAK,aAAa,WAAW,MAAM;AACjC,WAAK,KAAK,SAAS;AAAA,QACjB,MAAM;AAAA,QACN,SAAS,0CAA0C,OAAO,SAAS,MAAM;AAAA,MAC3E,CAAC;AAAA,IACH,GAAG,KAAK,cAAc;AACtB,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,UAAgB;AA5GlB;AA6GI,iBAAa,KAAK,UAAU;AAC5B,WAAO,oBAAoB,WAAW,KAAK,SAAS;AACpD,eAAK,WAAL,mBAAa;AACb,SAAK,SAAS;AAAA,EAChB;AAAA,EAEQ,KAAkC,OAAU,SAAkC;AAnHxF;AAoHI,eAAW,YAAW,UAAK,SAAS,IAAI,KAAK,MAAvB,YAA4B,CAAC,GAAG;AACpD,UAAI;AACF,QAAC,QAAuB,OAAO;AAAA,MACjC,SAAS,OAAO;AACd,mBAAW,MAAM;AACf,gBAAM;AAAA,QACR,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,cAAc,OAA2B;AA/HnD;AAiII,QAAI,CAAC,KAAK,UAAU,MAAM,WAAW,KAAK,OAAO,iBAAiB,MAAM,WAAW,KAAK,OAAQ;AAChG,UAAM,OAAO,MAAM;AACnB,QAAI,CAAC,QAAQ,OAAO,KAAK,SAAS,YAAY,CAAC,KAAK,KAAK,WAAW,MAAM,EAAG;AAE7E,YAAQ,KAAK,KAAK,MAAM,OAAO,MAAM,GAAG;AAAA,MACtC,KAAK;AAEH,mBAAK,OAAO,kBAAZ,mBAA2B;AAAA,UACzB,EAAE,MAAM,GAAG,MAAM,QAAQ,cAAc,KAAK,QAAQ,aAAa;AAAA,UACjE,KAAK;AAAA;AAEP;AAAA,MACF,KAAK;AACH,qBAAa,KAAK,UAAU;AAC5B,aAAK,YAAY,QAAO,UAAK,cAAL,YAAkB,EAAE;AAC5C,aAAK,KAAK,SAAS,EAAE,WAAW,KAAK,UAAU,CAAC;AAChD;AAAA,MACF,KAAK;AACH,YAAI,OAAO,KAAK,WAAW,YAAY,KAAK,SAAS,GAAG;AACtD,eAAK,OAAO,MAAM,SAAS,GAAG,KAAK,KAAK,KAAK,MAAM,CAAC;AAAA,QACtD;AACA;AAAA,MACF,KAAK;AACH,aAAK,KAAK,aAAa;AAAA,UACrB,WAAW,QAAO,UAAK,cAAL,YAAkB,KAAK,SAAS;AAAA,UAClD,WAAW,OAAO,KAAK,cAAc,WAAW,KAAK,YAAY;AAAA,QACnE,CAAC;AACD;AAAA,MACF,KAAK;AACH,aAAK,KAAK,UAAU,EAAE,WAAW,QAAO,UAAK,cAAL,YAAkB,KAAK,SAAS,GAAG,MAAM,QAAO,UAAK,SAAL,YAAa,gBAAgB,EAAE,CAAC;AACxH;AAAA,MACF,KAAK;AACH,aAAK,KAAK,WAAW,EAAE,WAAW,QAAO,UAAK,cAAL,YAAkB,KAAK,SAAS,EAAE,CAAC;AAC5E;AAAA,MACF,KAAK;AACH,qBAAa,KAAK,UAAU;AAC5B,aAAK,KAAK,SAAS,EAAE,MAAM,QAAO,UAAK,SAAL,YAAa,SAAS,GAAG,SAAS,QAAO,UAAK,YAAL,YAAgB,iBAAiB,EAAE,CAAC;AAC/G;AAAA,IACJ;AAAA,EACF;AACF;AAGO,SAAS,SAAS,SAAoC;AAC3D,SAAO,IAAI,SAAS,OAAO;AAC7B;AAGO,SAAS,mBAAmB,SAAyE;AAjL5G;AAkLE,QAAM,OAAO,YAAY,QAAQ,YAAY;AAC7C,QAAM,SAAS,iBAAgB,aAAQ,mBAAR,YAA0B,uBAAuB;AAChF,SAAO,SAAS,OAAO,GAAG,MAAM,QAAQ,mBAAmB,IAAI,CAAC,EAAE;AACpE;;;AC3KO,IAAM,UAAU;","names":[]}
1
+ {"version":3,"sources":["../src/checkout.ts","../src/index.ts"],"sourcesContent":["export const DEFAULT_CHECKOUT_ORIGIN = 'https://pay.vexpay.app';\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.vexpay.app`. */\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 // allow-popups-to-escape-sandbox: USDT opens the processor's payment page in a new\n // window, which must run as a normal page (not inherit this iframe's sandbox).\n iframe.setAttribute(\n 'sandbox',\n 'allow-scripts allow-forms allow-same-origin allow-popups allow-popups-to-escape-sandbox 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.3.0';\n"],"mappings":"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","names":[]}
@@ -1,2 +1,2 @@
1
- "use strict";var VexPay=(()=>{var c=Object.defineProperty;var k=Object.getOwnPropertyDescriptor;var T=Object.getOwnPropertyNames;var I=Object.prototype.hasOwnProperty;var w=(s,e)=>{for(var t in e)c(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&&c(s,n,{get:()=>e[n],enumerable:!(i=k(e,n))||i.enumerable});return s};var _=s=>C(c({},"__esModule",{value:!0}),s);var S={};w(S,{DEFAULT_CHECKOUT_ORIGIN:()=>r,VERSION:()=>v,checkout:()=>h,redirectToCheckout:()=>u});var r="https://pay.vexwallet.co";var d="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:r),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(o){setTimeout(()=>{throw o})}}handleMessage(e){var i,n,o,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(d)))switch(t.type.slice(d.length)){case"ready":(i=this.iframe.contentWindow)==null||i.postMessage({type:`${d}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((o=t.sessionId)!=null?o: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:r);window.location.assign(`${t}/pay/${encodeURIComponent(e)}`)}var v="0.1.1-rc.0";return _(S);})();
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.vexpay.app";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-popups-to-escape-sandbox 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,p,g,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((p=t.sessionId)!=null?p:this.sessionId)});return;case"error":clearTimeout(this.readyTimer),this.emit("error",{code:String((g=t.code)!=null?g:"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.3.0";return _(S);})();
2
2
  //# sourceMappingURL=vexpay.global.js.map
@@ -1 +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.1-rc.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"]}
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.vexpay.app';\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.vexpay.app`. */\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 // allow-popups-to-escape-sandbox: USDT opens the processor's payment page in a new\n // window, which must run as a normal page (not inherit this iframe's sandbox).\n iframe.setAttribute(\n 'sandbox',\n 'allow-scripts allow-forms allow-same-origin allow-popups allow-popups-to-escape-sandbox 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.3.0';\n"],"mappings":"0bAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,6BAAAE,EAAA,YAAAC,EAAA,aAAAC,EAAA,uBAAAC,ICAO,IAAMC,EAA0B,yBAEvC,IAAMC,EAAS,UACTC,EAAiB,2CA2BvB,SAASC,EAAYC,EAA+B,CAClD,IAAMC,EAAQ,OAAOD,GAAiB,SAAWF,EAAe,KAAKE,CAAY,EAAI,KACrF,GAAI,CAACC,EACH,MAAM,IAAI,MAAM,sGAAsG,EAExH,OAAOA,EAAM,CAAC,CAChB,CAEA,SAASC,EAAgBC,EAAwB,CAC/C,OAAO,IAAI,IAAIA,CAAM,EAAE,MACzB,CAMO,IAAMC,EAAN,KAAe,CAUpB,YAA6BC,EAA0B,CAA1B,aAAAA,EAN7B,KAAiB,SAAW,IAAI,IAChC,KAAQ,OAAmC,KAC3C,KAAQ,UAAY,GAEpB,KAAiB,UAAaC,GAAwB,KAAK,cAAcA,CAAK,EAtDhF,IAAAC,EAAAC,EAyDI,KAAK,KAAOT,EAAYM,EAAQ,YAAY,EAC5C,KAAK,OAASH,GAAgBK,EAAAF,EAAQ,iBAAR,KAAAE,EAA0BE,CAAuB,EAC/E,KAAK,gBAAiBD,EAAAH,EAAQ,iBAAR,KAAAG,EAA0B,IAClD,CAGA,GAAgCF,EAAUI,EAAiC,CACzE,IAAIC,EAAM,KAAK,SAAS,IAAIL,CAAK,EACjC,OAAKK,GAAK,KAAK,SAAS,IAAIL,EAAQK,EAAM,IAAI,GAAM,EACpDA,EAAI,IAAID,CAAmC,EACpC,IAAM,KAAK,IAAIJ,EAAOI,CAAO,CACtC,CAEA,IAAiCJ,EAAUI,EAA2B,CAtExE,IAAAH,GAuEIA,EAAA,KAAK,SAAS,IAAID,CAAK,IAAvB,MAAAC,EAA0B,OAAOG,EACnC,CAGA,MAAME,EAAoC,CACpC,KAAK,QAAQ,KAAK,QAAQ,EAC9B,IAAMC,EAAY,OAAOD,GAAW,SAAW,SAAS,cAA2BA,CAAM,EAAIA,EAC7F,GAAI,CAACC,EACH,YAAK,KAAK,QAAS,CAAE,KAAM,yBAA0B,QAAS,8BAA8B,OAAOD,CAAM,CAAC,GAAI,CAAC,EACxG,KAGT,IAAME,EAAS,SAAS,cAAc,QAAQ,EAC9C,OAAAA,EAAO,IAAM,GAAG,KAAK,MAAM,QAAQ,mBAAmB,KAAK,IAAI,CAAC,WAChEA,EAAO,MAAQ,kBACfA,EAAO,MAAQ,UAGfA,EAAO,aACL,UACA,iIACF,EACAA,EAAO,MAAM,QAAU,6EACvB,KAAK,OAASA,EAEd,OAAO,iBAAiB,UAAW,KAAK,SAAS,EACjDD,EAAU,YAAYC,CAAM,EAG5B,KAAK,WAAa,WAAW,IAAM,CACjC,KAAK,KAAK,QAAS,CACjB,KAAM,qBACN,QAAS,0CAA0C,OAAO,SAAS,MAAM,mCAC3E,CAAC,CACH,EAAG,KAAK,cAAc,EACf,IACT,CAGA,SAAgB,CA9GlB,IAAAP,EA+GI,aAAa,KAAK,UAAU,EAC5B,OAAO,oBAAoB,UAAW,KAAK,SAAS,GACpDA,EAAA,KAAK,SAAL,MAAAA,EAAa,SACb,KAAK,OAAS,IAChB,CAEQ,KAAkCD,EAAUS,EAAkC,CArHxF,IAAAR,EAsHI,QAAWG,KAAWH,EAAA,KAAK,SAAS,IAAID,CAAK,IAAvB,KAAAC,EAA4B,CAAC,EACjD,GAAI,CACDG,EAAuBK,CAAO,CACjC,OAASC,EAAO,CACd,WAAW,IAAM,CACf,MAAMA,CACR,CAAC,CACH,CAEJ,CAEQ,cAAcV,EAA2B,CAjInD,IAAAC,EAAAC,EAAAS,EAAAC,EAAAC,EAAAC,EAAAC,EAAAC,EAmII,GAAI,CAAC,KAAK,QAAUhB,EAAM,SAAW,KAAK,OAAO,eAAiBA,EAAM,SAAW,KAAK,OAAQ,OAChG,IAAMiB,EAAOjB,EAAM,KACnB,GAAI,GAACiB,GAAQ,OAAOA,EAAK,MAAS,UAAY,CAACA,EAAK,KAAK,WAAW1B,CAAM,GAE1E,OAAQ0B,EAAK,KAAK,MAAM1B,EAAO,MAAM,EAAG,CACtC,IAAK,SAEHU,EAAA,KAAK,OAAO,gBAAZ,MAAAA,EAA2B,YACzB,CAAE,KAAM,GAAGV,CAAM,OAAQ,aAAc,KAAK,QAAQ,YAAa,EACjE,KAAK,QAEP,OACF,IAAK,WACH,aAAa,KAAK,UAAU,EAC5B,KAAK,UAAY,QAAOW,EAAAe,EAAK,YAAL,KAAAf,EAAkB,EAAE,EAC5C,KAAK,KAAK,QAAS,CAAE,UAAW,KAAK,SAAU,CAAC,EAChD,OACF,IAAK,SACC,OAAOe,EAAK,QAAW,UAAYA,EAAK,OAAS,IACnD,KAAK,OAAO,MAAM,OAAS,GAAG,KAAK,KAAKA,EAAK,MAAM,CAAC,MAEtD,OACF,IAAK,YACH,KAAK,KAAK,YAAa,CACrB,UAAW,QAAON,EAAAM,EAAK,YAAL,KAAAN,EAAkB,KAAK,SAAS,EAClD,UAAW,OAAOM,EAAK,WAAc,SAAWA,EAAK,UAAY,IACnE,CAAC,EACD,OACF,IAAK,SACH,KAAK,KAAK,SAAU,CAAE,UAAW,QAAOL,EAAAK,EAAK,YAAL,KAAAL,EAAkB,KAAK,SAAS,EAAG,KAAM,QAAOC,EAAAI,EAAK,OAAL,KAAAJ,EAAa,gBAAgB,CAAE,CAAC,EACxH,OACF,IAAK,UACH,KAAK,KAAK,UAAW,CAAE,UAAW,QAAOC,EAAAG,EAAK,YAAL,KAAAH,EAAkB,KAAK,SAAS,CAAE,CAAC,EAC5E,OACF,IAAK,QACH,aAAa,KAAK,UAAU,EAC5B,KAAK,KAAK,QAAS,CAAE,KAAM,QAAOC,EAAAE,EAAK,OAAL,KAAAF,EAAa,SAAS,EAAG,QAAS,QAAOC,EAAAC,EAAK,UAAL,KAAAD,EAAgB,iBAAiB,CAAE,CAAC,EAC/G,MACJ,CACF,CACF,EAGO,SAASE,EAASnB,EAAoC,CAC3D,OAAO,IAAID,EAASC,CAAO,CAC7B,CAGO,SAASoB,EAAmBpB,EAAyE,CAnL5G,IAAAE,EAoLE,IAAMmB,EAAO3B,EAAYM,EAAQ,YAAY,EACvCF,EAASD,GAAgBK,EAAAF,EAAQ,iBAAR,KAAAE,EAA0BE,CAAuB,EAChF,OAAO,SAAS,OAAO,GAAGN,CAAM,QAAQ,mBAAmBuB,CAAI,CAAC,EAAE,CACpE,CC7KO,IAAMC,EAAU","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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vexpay/js",
3
- "version": "0.1.1-rc.0",
3
+ "version": "0.3.0",
4
4
  "description": "Embed the VEXPay checkout on your website \u2014 Pago M\u00f3vil C2P and cards, with payment details kept on VEXPay's origin.",
5
5
  "license": "MIT",
6
6
  "keywords": [