@xpayeg/sdk 1.0.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/CHANGELOG.md ADDED
@@ -0,0 +1,14 @@
1
+ # @xpayeg/sdk
2
+
3
+ ## 1.0.0
4
+ ### Major Changes
5
+
6
+
7
+
8
+ - [#100](https://github.com/xpayeg/xpay/pull/100) [`76481af`](https://github.com/xpayeg/xpay/commit/76481af7da2fab85f3557666315374c09cc5dddf) Thanks [@Elmosh](https://github.com/Elmosh)! - Initial 1.0.0 release.
9
+
10
+ `@xpayeg/sdk` ships the `loadXPay()` loader plus the full public TypeScript surface for embedding XPay payments — `Elements`, `PaymentElement`, `CardElement`, drop-in `checkout()`, `initCheckout()`, action methods (`confirm`, `applyPromotionCode`, `removePromotionCode`, `updateLineItemQuantity`, `submit`, `fetchUpdates`, `changeAppearance`), and the tagged-union `ActionResult` / `XPayError` shapes.
11
+
12
+ `@xpayeg/react` ships the React bindings: `XPayProvider`, `useCheckout`, `useXPay`, `useElements`, `useConfirmPayment`, `<PaymentElement>`, `<CardElement>`, and `<CheckoutButton>` — all SSR-safe, with stable event listeners and smart option diffing.
13
+
14
+ The runtime is served from `https://checkout.xpay.app/v1/sdk.js` and auto-loaded by `loadXPay()`. Both packages publish with npm provenance.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) XPay
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,529 @@
1
+ # @xpayeg/sdk
2
+
3
+ XPay JavaScript SDK -- loader and TypeScript types for embedding XPay payments on any website.
4
+
5
+ ## Documentation
6
+
7
+ Full guides, API reference, and live examples: **<https://docs.xpay.app>**
8
+
9
+ This README is a quick-start. The docs site is the authoritative reference.
10
+
11
+ ## Installation
12
+
13
+ **Via npm (recommended for bundler projects):**
14
+
15
+ ```bash
16
+ npm install @xpayeg/sdk
17
+ ```
18
+
19
+ **Via CDN (for non-bundler setups):**
20
+
21
+ ```html
22
+ <script src="https://checkout.xpay.app/v1/sdk.js"></script>
23
+ ```
24
+
25
+ ## Step 1: Create a Checkout Session [Server-side]
26
+
27
+ On your server, create a Checkout Session and return the `clientSecret` to your frontend.
28
+
29
+ ```javascript
30
+ // Your server (Node.js example)
31
+ app.post('/api/create-checkout', async (req, res) => {
32
+ const response = await fetch('https://api.xpay.app/checkout/sessions', {
33
+ method: 'POST',
34
+ headers: {
35
+ 'Authorization': `Bearer ${process.env.XPAY_SECRET_KEY}`,
36
+ 'Content-Type': 'application/json',
37
+ },
38
+ body: JSON.stringify({
39
+ uiMode: 'custom', // 'custom' for Elements SDK, 'embedded' for drop-in modal, 'hosted' for redirect
40
+ lineItems: [
41
+ {
42
+ priceData: {
43
+ unitAmount: 50000, // 500.00 EGP in piasters
44
+ currency: 'EGP',
45
+ productData: { name: 'Premium Plan' },
46
+ },
47
+ quantity: 1,
48
+ },
49
+ ],
50
+ afterCompletion: {
51
+ type: 'redirect',
52
+ redirect: {
53
+ // {CHECKOUT_SESSION_ID} is automatically replaced with the actual session ID
54
+ url: 'https://yoursite.com/success?session_id={CHECKOUT_SESSION_ID}',
55
+ },
56
+ },
57
+ }),
58
+ });
59
+
60
+ const session = await response.json();
61
+ res.json({ clientSecret: session.clientSecret });
62
+ });
63
+ ```
64
+
65
+ ## Step 2: Initialize Checkout [Client-side]
66
+
67
+ ### `initCheckout` (Recommended)
68
+
69
+ Returns a single object with session fields and action methods merged together.
70
+
71
+ ```javascript
72
+ import { loadXPay } from '@xpayeg/sdk';
73
+
74
+ const xpay = await loadXPay('pk_test_xxx');
75
+
76
+ // Fetch client secret from your server
77
+ const { clientSecret } = await fetch('/api/create-checkout', { method: 'POST' }).then(r => r.json());
78
+
79
+ // Initialize checkout -- returns session data + action methods in one object
80
+ const checkout = await xpay.initCheckout({ clientSecret });
81
+
82
+ // Destructure session fields and actions together
83
+ const {
84
+ status, currency, amountTotal, amountSubtotal, canConfirm, paymentMethods,
85
+ confirm, applyPromotionCode, removePromotionCode, updateLineItemQuantity,
86
+ } = checkout;
87
+
88
+ // Display in your UI
89
+ document.getElementById('total').textContent =
90
+ `${currency} ${(amountTotal / 100).toFixed(2)}`;
91
+ document.getElementById('methods').textContent =
92
+ `Pay with: ${paymentMethods.map(pm => pm.displayName).join(', ')}`;
93
+ ```
94
+
95
+ `clientSecret` accepts `Promise<string> | string`, so you can pass the fetch directly:
96
+
97
+ ```javascript
98
+ const checkout = await xpay.initCheckout({
99
+ clientSecret: fetch('/api/create-checkout', { method: 'POST' })
100
+ .then(r => r.json())
101
+ .then(data => data.clientSecret),
102
+ });
103
+ ```
104
+
105
+ **Session fields on the checkout object:**
106
+
107
+ | Field | Type | Example | Description |
108
+ |-------|------|---------|-------------|
109
+ | `id` | `string` | `"cs_test_abc"` | Session ID |
110
+ | `status` | `SessionStatus` | `{ type: "open" }` | Structured status (see below) |
111
+ | `canConfirm` | `boolean` | `true` | Whether the session is ready for confirmation |
112
+ | `amountTotal` | `number` | `52450` | Final total in smallest currency unit |
113
+ | `amountSubtotal` | `number` | `50000` | Subtotal before fees/discounts |
114
+ | `currency` | `string` | `"EGP"` | ISO 4217 currency code |
115
+ | `merchantName` | `string` | `"My Store"` | Merchant display name |
116
+ | `livemode` | `boolean` | `false` | Whether live or test mode |
117
+ | `paymentMethods` | `PaymentMethodInfo[]` | `[{ type, displayName, category }]` | Available payment methods |
118
+ | `lineItems` | `LineItemDto[]` | | Line items with prices and quantities |
119
+ | `totalDetails` | `TotalDetailsResponseDto` | | Breakdown of fees, discounts, VAT |
120
+ | `discounts` | `DiscountResponseDto[]` | | Applied discounts |
121
+
122
+ ### Session Status
123
+
124
+ `status` is a structured object, not a plain string:
125
+
126
+ ```javascript
127
+ // Check session status
128
+ if (checkout.status.type === 'open') {
129
+ // Session is active, show payment form
130
+ }
131
+
132
+ if (checkout.status.type === 'expired') {
133
+ // Session expired, show message or create a new one
134
+ showMessage('This checkout session has expired.');
135
+ }
136
+
137
+ if (checkout.status.type === 'complete') {
138
+ // Payment finished -- check paymentStatus for details
139
+ console.log(checkout.status.paymentStatus); // "paid"
140
+ showMessage('Payment successful!');
141
+ }
142
+ ```
143
+
144
+ ### Alternative: `elements`
145
+
146
+ For event-driven initialization and lower-level control.
147
+
148
+ ```javascript
149
+ const xpay = await loadXPay('pk_test_xxx');
150
+ const elements = xpay.elements({ clientSecret });
151
+
152
+ elements.on('ready', ({ session }) => {
153
+ console.log(session.status); // { type: "open" }
154
+ console.log(session.canConfirm); // true
155
+ console.log(session.amountTotal); // 52450
156
+ console.log(session.paymentMethods); // [{ type: 'card', ... }]
157
+ });
158
+
159
+ elements.on('loaderror', (event) => {
160
+ // Only fires for actual failures (invalid secret, network error, etc.)
161
+ // event: { type: "invalid_request_error" | "api_error" | "network_error", message, code?, param?, docUrl? }
162
+ console.error('Failed to load:', event.message);
163
+ });
164
+
165
+ elements.on('error', (error) => {
166
+ // Fires for unsolicited errors not triggered by a merchant action.
167
+ // Examples: session expired during fee recalculation when switching payment methods, BIN detection failure.
168
+ console.log(error.type); // "invalid_request_error"
169
+ console.log(error.code); // "checkout_session_expired"
170
+ console.log(error.message); // "This checkout session has expired"
171
+ console.log(error.docUrl); // "https://docs.xpay.app/api/errors#checkout_session_expired"
172
+ });
173
+ ```
174
+
175
+ ## Step 3: Mount the Payment Form
176
+
177
+ ### Payment Element (handles method selection + card form)
178
+
179
+ ```javascript
180
+ const checkout = await xpay.initCheckout({ clientSecret });
181
+ const elements = checkout.getElements();
182
+
183
+ const paymentElement = elements.create('payment');
184
+ paymentElement.mount('#payment-element');
185
+
186
+ paymentElement.on('change', (event) => {
187
+ submitButton.disabled = !event.complete;
188
+ console.log(event.value.type); // 'card', 'valu', etc.
189
+ });
190
+ ```
191
+
192
+ ### Card Element (card form only -- you handle method selection)
193
+
194
+ ```javascript
195
+ const cardElement = elements.create('card');
196
+ cardElement.mount('#card-form');
197
+
198
+ cardElement.on('change', (event) => {
199
+ console.log(event.complete, event.value.brand);
200
+ });
201
+ ```
202
+
203
+ ## Step 4: Confirm the Payment
204
+
205
+ `confirm()` returns a tagged union -- check `type` to determine the outcome.
206
+
207
+ By default, the result is **returned to your code** (`redirect: "if_required"`). Use `redirect: "always"` to redirect to the `afterCompletion.redirect.url` you set on the server.
208
+
209
+ ```javascript
210
+ // Default behavior: returns result to your code (no redirect)
211
+ const result = await checkout.confirm({
212
+ customerDetails: {
213
+ email: 'customer@example.com',
214
+ name: 'Ahmed Hassan',
215
+ phone: '+201234567890',
216
+ },
217
+ });
218
+
219
+ if (result.type === 'error') {
220
+ errorDiv.textContent = result.error.message;
221
+ } else {
222
+ console.log(result.session.status); // { type: "complete", paymentStatus: "paid" }
223
+ window.location.href = '/thank-you';
224
+ }
225
+ ```
226
+
227
+ To redirect after success instead:
228
+
229
+ ```javascript
230
+ await checkout.confirm({
231
+ customerDetails: {
232
+ email: 'customer@example.com',
233
+ name: 'Ahmed Hassan',
234
+ },
235
+ redirect: 'always', // Redirect to afterCompletion.redirect.url
236
+ });
237
+ // ^ If successful, the page navigates away. Code below only runs on error.
238
+ ```
239
+
240
+ **Redirect behavior:**
241
+
242
+ | `redirect` | Behavior |
243
+ |---|---|
244
+ | Not set (default) | `"if_required"` — returns result to your code |
245
+ | `"always"` | Redirects to `returnUrl` (client override) → server's `afterCompletion.redirect.url` |
246
+ | `"if_required"` | Returns result to your code — no redirect |
247
+
248
+ You can override the server's redirect URL from the client:
249
+
250
+ ```javascript
251
+ await checkout.confirm({
252
+ customerDetails: { email, name },
253
+ redirect: 'always',
254
+ returnUrl: 'https://mysite.com/custom-success', // Overrides server URL
255
+ });
256
+ ```
257
+
258
+ ### Pre-validation with `submit()`
259
+
260
+ Call `submit()` before `confirm()` to validate all fields and get the selected payment method:
261
+
262
+ ```javascript
263
+ const { error, selectedPaymentMethod } = await elements.submit();
264
+ if (error) {
265
+ errorDiv.textContent = error.message;
266
+ return;
267
+ }
268
+
269
+ // Fields are valid, proceed to confirm
270
+ const result = await checkout.confirm({ paymentMethod: selectedPaymentMethod });
271
+ ```
272
+
273
+ ## Step 4b: Update Session (Promo Codes, Quantities)
274
+
275
+ Action methods return `Promise<{ type: "success", session } | { type: "error", error }>`.
276
+
277
+ ```javascript
278
+ // Apply promotion code
279
+ const result = await checkout.applyPromotionCode('SAVE20');
280
+ if (result.type === 'error') {
281
+ errorDiv.textContent = result.error.message; // e.g., "Invalid promotion code"
282
+ } else {
283
+ console.log('New total:', result.session.amountTotal);
284
+ }
285
+
286
+ // Remove promotion code
287
+ await checkout.removePromotionCode();
288
+
289
+ // Update line item quantity (object param, not positional)
290
+ await checkout.updateLineItemQuantity({ lineItem: 'li_abc123', quantity: 3 });
291
+ ```
292
+
293
+ ### Listening for Session Changes
294
+
295
+ Every update (promo codes, quantity changes, payment method selection, BIN detection) triggers a `change` event with the full updated `CheckoutSession`:
296
+
297
+ ```javascript
298
+ // initCheckout -- use on('change', ...)
299
+ checkout.on('change', (session) => {
300
+ totalDiv.textContent = `Total: ${session.currency} ${(session.amountTotal / 100).toFixed(2)}`;
301
+
302
+ // Amounts breakdown via totalDetails
303
+ console.log(session.totalDetails?.amountPlatformFee); // Processing fee
304
+ console.log(session.totalDetails?.amountCollectedVat); // VAT
305
+ console.log(session.totalDetails?.amountDiscount); // Discount amount
306
+ console.log(session.totalDetails?.amountShipping); // Shipping
307
+ console.log(session.totalDetails?.amountTax); // Tax
308
+
309
+ console.log(session.discounts); // Applied discounts
310
+ console.log(session.lineItems); // Updated line items
311
+ });
312
+ ```
313
+
314
+ With the classic `elements` API:
315
+
316
+ ```javascript
317
+ elements.on('change', (session) => {
318
+ console.log(session.amountTotal);
319
+ console.log(session.totalDetails?.amountPlatformFee);
320
+ });
321
+ ```
322
+
323
+ ### Re-sync from Server
324
+
325
+ Force a fresh session fetch if needed:
326
+
327
+ ```javascript
328
+ const result = await elements.fetchUpdates();
329
+ if (result.type === 'success') {
330
+ console.log('Session refreshed:', result.session.amountTotal);
331
+ }
332
+ ```
333
+
334
+ ## Step 5: Show a Success Page
335
+
336
+ Retrieve the session from **your server** (using your API key) to display order details.
337
+
338
+ **Server endpoint:**
339
+
340
+ ```javascript
341
+ // Your server -- retrieves session using your API key
342
+ app.get('/api/order-status', async (req, res) => {
343
+ const response = await fetch(
344
+ `https://api.xpay.app/checkout/sessions/${req.query.session_id}`,
345
+ { headers: { 'Authorization': `Bearer ${process.env.XPAY_SECRET_KEY}` } },
346
+ );
347
+ const session = await response.json();
348
+ res.json(session);
349
+ });
350
+ ```
351
+
352
+ **Client-side success page:**
353
+
354
+ ```javascript
355
+ const sessionId = new URLSearchParams(window.location.search).get('session_id');
356
+ const session = await fetch(`/api/order-status?session_id=${sessionId}`).then(r => r.json());
357
+
358
+ document.getElementById('status').textContent =
359
+ session.paymentStatus === 'paid' ? 'Payment Confirmed' : 'Processing...';
360
+ document.getElementById('total').textContent =
361
+ `${session.currency} ${(session.amountTotal / 100).toFixed(2)}`;
362
+
363
+ // Amounts breakdown
364
+ // session.totalDetails.amountDiscount -- 0
365
+ // session.totalDetails.amountPlatformFee -- 1750 (Processing Fee, if fees pass-through)
366
+ // session.totalDetails.amountCollectedVat -- 700 (VAT)
367
+ // session.totalDetails.amountShipping -- 0
368
+ // session.totalDetails.amountTax -- 0
369
+
370
+ // Line items
371
+ session.lineItems?.forEach(item => {
372
+ console.log(item.price.product.name, 'x', item.quantity, '=', item.amountTotal);
373
+ });
374
+ ```
375
+
376
+ ## Step 6: Handle Webhooks [Server-side]
377
+
378
+ Listen for webhook events on your server. This is the source of truth for order fulfillment.
379
+
380
+ ```javascript
381
+ app.post('/webhooks/xpay', (req, res) => {
382
+ const event = req.body;
383
+
384
+ if (event.type === 'checkout.session.completed') {
385
+ fulfillOrder(event.data); // Ship product, send email, update DB
386
+ }
387
+
388
+ res.json({ received: true });
389
+ });
390
+ ```
391
+
392
+ ## Drop-in Checkout (Modal)
393
+
394
+ The simplest integration -- opens the full checkout in a modal overlay. No form needed.
395
+
396
+ ```html
397
+ <button id="checkout-button">Pay Now</button>
398
+
399
+ <script src="https://checkout.xpay.app/v1/sdk.js"></script>
400
+ <script>
401
+ document.getElementById('checkout-button').addEventListener('click', async () => {
402
+ const xpay = XPay('pk_test_xxx');
403
+
404
+ const checkout = xpay.checkout({
405
+ clientSecret: 'cs_test_abc_secret_xyz',
406
+ mode: 'modal',
407
+ onComplete: (result) => {
408
+ window.location.href = '/success';
409
+ },
410
+ onClose: () => {
411
+ console.log('Customer closed checkout');
412
+ },
413
+ });
414
+
415
+ checkout.open();
416
+ });
417
+ </script>
418
+ ```
419
+
420
+ ## React Integration
421
+
422
+ The `@xpayeg/react` package provides a `useCheckout()` hook that wraps `initCheckout` with loading/error states:
423
+
424
+ ```javascript
425
+ import { useCheckout } from '@xpayeg/react';
426
+
427
+ function CheckoutPage() {
428
+ const checkoutState = useCheckout();
429
+
430
+ if (checkoutState.type === 'loading') {
431
+ return <Spinner />;
432
+ }
433
+
434
+ if (checkoutState.type === 'error') {
435
+ return <div>Error: {checkoutState.error.message}</div>;
436
+ }
437
+
438
+ const { currency, lineItems, amountTotal, confirm } = checkoutState.checkout;
439
+
440
+ return (
441
+ <div>
442
+ <h2>Total: {currency} {(amountTotal / 100).toFixed(2)}</h2>
443
+ {lineItems?.map(item => (
444
+ <div key={item.id}>{item.price.product.name} x {item.quantity}</div>
445
+ ))}
446
+ <button onClick={() => confirm({ customerDetails: { email } })}>Pay</button>
447
+ </div>
448
+ );
449
+ }
450
+ ```
451
+
452
+ ## Appearance
453
+
454
+ Override the session's `brandingSettings` at runtime:
455
+
456
+ ```javascript
457
+ const checkout = await xpay.initCheckout({
458
+ clientSecret,
459
+ appearance: {
460
+ colorMode: 'dark',
461
+ borderStyle: 'pill',
462
+ inputStyle: 'filled',
463
+ colors: { primary: '#FF6B35' },
464
+ },
465
+ });
466
+
467
+ // Update later with changeAppearance()
468
+ checkout.changeAppearance({ colorMode: 'light' });
469
+ ```
470
+
471
+ With the classic API:
472
+
473
+ ```javascript
474
+ const elements = xpay.elements({ clientSecret, appearance: { colorMode: 'dark' } });
475
+ elements.changeAppearance({ colorMode: 'light' });
476
+ ```
477
+
478
+ ## API Reference
479
+
480
+ ### `loadXPay(publishableKey?)`
481
+
482
+ Loads the XPay SDK from CDN. Returns a Promise. Call at module level, not inside components.
483
+
484
+ ```javascript
485
+ import { loadXPay } from '@xpayeg/sdk';
486
+ const xpay = await loadXPay('pk_test_xxx');
487
+ ```
488
+
489
+ ### `xpay.initCheckout(options)`
490
+
491
+ Initialize a checkout session. Resolves with a merged object containing session fields and action methods.
492
+
493
+ ```javascript
494
+ const checkout = await xpay.initCheckout({
495
+ clientSecret: 'cs_test_abc_secret_xyz', // string | Promise<string>
496
+ appearance: { colorMode: 'dark' },
497
+ locale: 'ar',
498
+ });
499
+
500
+ // Session fields: status, canConfirm, amountTotal, currency, paymentMethods, lineItems, totalDetails, ...
501
+ // Action methods: confirm(), applyPromotionCode(), removePromotionCode(), updateLineItemQuantity(),
502
+ // submit(), fetchUpdates(), changeAppearance(), on(), getElements()
503
+ ```
504
+
505
+ ### `xpay.elements(options)`
506
+
507
+ Create an Elements instance for mounting payment elements.
508
+
509
+ ### `xpay.checkout(options)`
510
+
511
+ Create a drop-in checkout instance (modal or inline).
512
+
513
+ ### `xpay.confirmPayment(options)`
514
+
515
+ Confirm and submit a payment (classic API).
516
+
517
+ ## TypeScript
518
+
519
+ Full TypeScript support with all types exported:
520
+
521
+ ```typescript
522
+ import type {
523
+ XPayInstance, Elements, PaymentMethodInfo,
524
+ CheckoutSession, SessionStatus, CustomerDetails, Appearance,
525
+ InitCheckoutResult, CheckoutActions,
526
+ ActionResult, XPayError,
527
+ CheckoutLineItem, CheckoutTotalDetails, CheckoutFees, CheckoutDiscount,
528
+ } from '@xpayeg/sdk';
529
+ ```
package/dist/index.cjs ADDED
@@ -0,0 +1,99 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/index.ts
3
+ const DEFAULT_SDK_URL = "https://checkout.xpay.app/v1/sdk.js";
4
+ let loadPromise = null;
5
+ /**
6
+ * Load the XPay SDK from CDN.
7
+ *
8
+ * Returns a promise that resolves to an XPayInstance with full TypeScript support.
9
+ * The SDK script is loaded once and cached — subsequent calls return the same instance.
10
+ *
11
+ * @param publishableKey - Your publishable API key (pk_test_... or pk_live_...)
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * import { loadXPay } from '@xpayeg/sdk';
16
+ *
17
+ * const xpay = await loadXPay('pk_test_xxx');
18
+ * const elements = xpay.elements({ clientSecret: 'cs_test_abc_secret_xyz' });
19
+ * ```
20
+ */
21
+ async function loadXPay(publishableKey) {
22
+ if (typeof window === "undefined") return null;
23
+ return (await ensureLoaded())(publishableKey);
24
+ }
25
+ function ensureLoaded() {
26
+ if (window.XPay) return Promise.resolve(window.XPay);
27
+ if (loadPromise) return loadPromise;
28
+ loadPromise = new Promise((resolve, reject) => {
29
+ const existing = findExistingScript();
30
+ if (existing) {
31
+ if (window.XPay) {
32
+ resolve(window.XPay);
33
+ return;
34
+ }
35
+ const onLoad = () => {
36
+ cleanup();
37
+ if (window.XPay) resolve(window.XPay);
38
+ else reject(/* @__PURE__ */ new Error("XPay SDK loaded but window.XPay is not available"));
39
+ };
40
+ const onError = () => {
41
+ cleanup();
42
+ loadPromise = null;
43
+ reject(/* @__PURE__ */ new Error("Failed to load XPay SDK"));
44
+ };
45
+ const cleanup = () => {
46
+ existing.removeEventListener("load", onLoad);
47
+ existing.removeEventListener("error", onError);
48
+ };
49
+ existing.addEventListener("load", onLoad);
50
+ existing.addEventListener("error", onError);
51
+ return;
52
+ }
53
+ const script = document.createElement("script");
54
+ script.src = DEFAULT_SDK_URL;
55
+ script.async = true;
56
+ const onLoad = () => {
57
+ cleanup();
58
+ if (window.XPay) resolve(window.XPay);
59
+ else {
60
+ loadPromise = null;
61
+ reject(/* @__PURE__ */ new Error("XPay SDK loaded but window.XPay is not available"));
62
+ }
63
+ };
64
+ const onError = () => {
65
+ cleanup();
66
+ loadPromise = null;
67
+ reject(/* @__PURE__ */ new Error(`Failed to load XPay SDK from ${DEFAULT_SDK_URL}`));
68
+ };
69
+ const cleanup = () => {
70
+ script.removeEventListener("load", onLoad);
71
+ script.removeEventListener("error", onError);
72
+ };
73
+ script.addEventListener("load", onLoad);
74
+ script.addEventListener("error", onError);
75
+ document.head.appendChild(script);
76
+ });
77
+ return loadPromise;
78
+ }
79
+ /** Find an existing XPay SDK script tag in the document — must match the
80
+ * versioned `/v1/sdk.js` path AND be served from an xpay-controlled origin
81
+ * so a random third-party `/v1/sdk.js` on the page is never mistaken for ours. */
82
+ function findExistingScript() {
83
+ const exact = document.querySelector(`script[src="${DEFAULT_SDK_URL}"]`);
84
+ if (exact) return exact;
85
+ let expectedOrigin = "";
86
+ try {
87
+ expectedOrigin = new URL(DEFAULT_SDK_URL).origin;
88
+ } catch {}
89
+ const scripts = document.querySelectorAll("script[src*=\"/v1/sdk.js\"]");
90
+ for (const script of scripts) try {
91
+ const url = new URL(script.src);
92
+ const pathOk = url.pathname.endsWith("/v1/sdk.js");
93
+ const originOk = url.origin === expectedOrigin || url.hostname.endsWith(".xpay.app");
94
+ if (pathOk && originOk) return script;
95
+ } catch {}
96
+ return null;
97
+ }
98
+ //#endregion
99
+ exports.loadXPay = loadXPay;