@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 +14 -0
- package/LICENSE +21 -0
- package/README.md +529 -0
- package/dist/index.cjs +99 -0
- package/dist/index.d.cts +3874 -0
- package/dist/index.d.mts +3874 -0
- package/dist/index.mjs +98 -0
- package/package.json +66 -0
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;
|