@xpayeg/react 2.4.0 → 3.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/{dist → .types}/index.d.cts +71 -80
- package/{dist → .types}/index.d.mts +71 -80
- package/CHANGELOG.md +36 -30
- package/README.md +139 -137
- package/dist/index.cjs +19 -47
- package/dist/index.mjs +19 -47
- package/package.json +11 -28
package/README.md
CHANGED
|
@@ -14,44 +14,46 @@ This README is a quick-start. The docs site is the authoritative reference.
|
|
|
14
14
|
npm install @xpayeg/sdk @xpayeg/react
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
+
Both ESM (`import`) and CommonJS (`require`) are supported, with matching TypeScript declarations. Your tooling selects the appropriate entrypoint automatically.
|
|
18
|
+
|
|
17
19
|
## Step 1: Create a Checkout Session [Server-side]
|
|
18
20
|
|
|
19
21
|
On your server, create a Checkout Session and return the `clientSecret` to your frontend. The checkout session defines what you're charging for — line items, currency, amounts, and what happens after payment.
|
|
20
22
|
|
|
21
23
|
```javascript
|
|
22
24
|
// Your server (Node.js example with Express)
|
|
23
|
-
app.post(
|
|
24
|
-
const response = await fetch(
|
|
25
|
-
method:
|
|
25
|
+
app.post("/api/create-checkout", async (req, res) => {
|
|
26
|
+
const response = await fetch("https://api.xpay.app/checkout/sessions", {
|
|
27
|
+
method: "POST",
|
|
26
28
|
headers: {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
Authorization: `Bearer ${process.env.XPAY_SECRET_KEY}`,
|
|
30
|
+
"Content-Type": "application/json",
|
|
29
31
|
},
|
|
30
32
|
body: JSON.stringify({
|
|
31
|
-
uiMode:
|
|
33
|
+
uiMode: "custom", // 'custom' for Elements SDK, 'embedded' for drop-in, 'hosted' for redirect
|
|
32
34
|
lineItems: [
|
|
33
35
|
{
|
|
34
36
|
priceData: {
|
|
35
|
-
unitAmount: 50000,
|
|
36
|
-
currency:
|
|
37
|
+
unitAmount: 50000, // Amount in smallest unit (500.00 EGP = 50000 piasters)
|
|
38
|
+
currency: "EGP",
|
|
37
39
|
productData: {
|
|
38
|
-
name:
|
|
39
|
-
description:
|
|
40
|
+
name: "Premium Plan",
|
|
41
|
+
description: "Monthly subscription",
|
|
40
42
|
},
|
|
41
43
|
},
|
|
42
44
|
quantity: 1,
|
|
43
45
|
},
|
|
44
46
|
],
|
|
45
47
|
afterCompletion: {
|
|
46
|
-
type:
|
|
48
|
+
type: "redirect",
|
|
47
49
|
redirect: {
|
|
48
50
|
// {CHECKOUT_SESSION_ID} is automatically replaced with the session ID
|
|
49
|
-
url:
|
|
51
|
+
url: "https://yoursite.com/success?session_id={CHECKOUT_SESSION_ID}",
|
|
50
52
|
},
|
|
51
53
|
},
|
|
52
54
|
// Optional
|
|
53
55
|
customerDetails: { email: req.body.email },
|
|
54
|
-
brandingSettings: { colorMode:
|
|
56
|
+
brandingSettings: { colorMode: "system" },
|
|
55
57
|
}),
|
|
56
58
|
});
|
|
57
59
|
|
|
@@ -202,9 +204,7 @@ function OrderSummary() {
|
|
|
202
204
|
|
|
203
205
|
{/* Live/test mode indicator */}
|
|
204
206
|
{!checkout.livemode && (
|
|
205
|
-
<span className="text-xs bg-yellow-100 text-yellow-800 px-2 py-1 rounded">
|
|
206
|
-
Test Mode
|
|
207
|
-
</span>
|
|
207
|
+
<span className="text-xs bg-yellow-100 text-yellow-800 px-2 py-1 rounded">Test Mode</span>
|
|
208
208
|
)}
|
|
209
209
|
</div>
|
|
210
210
|
);
|
|
@@ -213,22 +213,22 @@ function OrderSummary() {
|
|
|
213
213
|
|
|
214
214
|
**Session fields on `checkout`:**
|
|
215
215
|
|
|
216
|
-
| Field
|
|
217
|
-
|
|
218
|
-
| `checkout.id`
|
|
219
|
-
| `checkout.amountSubtotal` | `number`
|
|
220
|
-
| `checkout.amountTotal`
|
|
221
|
-
| `checkout.currency`
|
|
222
|
-
| `checkout.merchantName`
|
|
223
|
-
| `checkout.livemode`
|
|
224
|
-
| `checkout.expiresAt`
|
|
225
|
-
| `checkout.status`
|
|
226
|
-
| `checkout.canConfirm`
|
|
227
|
-
| `checkout.paymentMethods` | `PaymentMethodInfo[]` | Available payment methods
|
|
228
|
-
| `checkout.lineItems`
|
|
229
|
-
| `checkout.totalDetails`
|
|
230
|
-
| `checkout.fees`
|
|
231
|
-
| `checkout.discounts`
|
|
216
|
+
| Field | Type | Description |
|
|
217
|
+
| ------------------------- | --------------------- | ---------------------------------------------------------------------------------------------- |
|
|
218
|
+
| `checkout.id` | `string` | Session ID |
|
|
219
|
+
| `checkout.amountSubtotal` | `number` | Subtotal before discounts/fees (smallest unit) |
|
|
220
|
+
| `checkout.amountTotal` | `number` | Total amount in smallest currency unit (piasters) |
|
|
221
|
+
| `checkout.currency` | `string` | ISO 4217 currency code |
|
|
222
|
+
| `checkout.merchantName` | `string` | Merchant display name |
|
|
223
|
+
| `checkout.livemode` | `boolean` | Whether this is a live mode session |
|
|
224
|
+
| `checkout.expiresAt` | `string` | Session expiration timestamp |
|
|
225
|
+
| `checkout.status` | `SessionStatus` | Disjoint union: `{type: "open"}` \| `{type: "expired"}` \| `{type: "complete", paymentStatus}` |
|
|
226
|
+
| `checkout.canConfirm` | `boolean` | Whether the session is ready for confirmation |
|
|
227
|
+
| `checkout.paymentMethods` | `PaymentMethodInfo[]` | Available payment methods |
|
|
228
|
+
| `checkout.lineItems` | `LineItem[]` | Line items with product name, quantity, amount |
|
|
229
|
+
| `checkout.totalDetails` | `TotalDetails` | Amounts breakdown (discount, shipping, tax, fees) |
|
|
230
|
+
| `checkout.fees` | `Fees` | Fee breakdown (when feesPassThrough enabled) |
|
|
231
|
+
| `checkout.discounts` | `Discount[]` | Applied discounts |
|
|
232
232
|
|
|
233
233
|
### Updating the Session (Promo Codes, Quantities)
|
|
234
234
|
|
|
@@ -289,13 +289,13 @@ function CheckoutWithPromo() {
|
|
|
289
289
|
))}
|
|
290
290
|
|
|
291
291
|
{/* Total updates reactively */}
|
|
292
|
-
<p>
|
|
292
|
+
<p>
|
|
293
|
+
Total: {checkout.currency} {(checkout.amountTotal / 100).toFixed(2)}
|
|
294
|
+
</p>
|
|
293
295
|
|
|
294
296
|
{/* Fee breakdown (when feesPassThrough enabled) */}
|
|
295
297
|
{checkout.totalDetails?.amountPlatformFee && (
|
|
296
|
-
<p>
|
|
297
|
-
Processing Fee: {(checkout.totalDetails.amountPlatformFee / 100).toFixed(2)}
|
|
298
|
-
</p>
|
|
298
|
+
<p>Processing Fee: {(checkout.totalDetails.amountPlatformFee / 100).toFixed(2)}</p>
|
|
299
299
|
)}
|
|
300
300
|
|
|
301
301
|
<PaymentElement />
|
|
@@ -346,24 +346,23 @@ The error object shape:
|
|
|
346
346
|
|
|
347
347
|
All action methods live on the `checkout` object returned from the `success` state.
|
|
348
348
|
|
|
349
|
-
| Method
|
|
350
|
-
|
|
351
|
-
| `confirm`
|
|
352
|
-
| `applyPromotionCode`
|
|
353
|
-
| `removePromotionCode`
|
|
354
|
-
| `updateLineItemQuantity` | `({lineItem, quantity}) => Promise<ActionResult>` | Update a line item's quantity
|
|
355
|
-
| `submit`
|
|
356
|
-
| `fetchUpdates`
|
|
357
|
-
| `changeAppearance`
|
|
358
|
-
| `on`
|
|
359
|
-
| `getElements`
|
|
349
|
+
| Method | Signature | Description |
|
|
350
|
+
| ------------------------ | ------------------------------------------------- | --------------------------------------------------------------------------------- |
|
|
351
|
+
| `confirm` | `(options?) => Promise<ActionResult>` | Confirm payment. Handles 3DS and redirects. |
|
|
352
|
+
| `applyPromotionCode` | `(code: string) => Promise<ActionResult>` | Apply a promotion code |
|
|
353
|
+
| `removePromotionCode` | `() => Promise<ActionResult>` | Remove the applied promotion code |
|
|
354
|
+
| `updateLineItemQuantity` | `({lineItem, quantity}) => Promise<ActionResult>` | Update a line item's quantity |
|
|
355
|
+
| `submit` | `() => Promise<{error?, selectedPaymentMethod?}>` | Validate all fields before confirming |
|
|
356
|
+
| `fetchUpdates` | `() => Promise<ActionResult>` | Re-fetch the session from the server |
|
|
357
|
+
| `changeAppearance` | `(appearance: Appearance) => void` | Update appearance at runtime |
|
|
358
|
+
| `on` | `("change", handler) => void` | Listen for session changes (rarely needed in React — state updates automatically) |
|
|
359
|
+
| `getElements` | `() => Elements` | Access the underlying Elements instance |
|
|
360
360
|
|
|
361
361
|
**`ActionResult` type:**
|
|
362
362
|
|
|
363
363
|
```typescript
|
|
364
364
|
type ActionResult =
|
|
365
|
-
|
|
366
|
-
| { type: "error"; error: XPayError };
|
|
365
|
+
{ type: "success"; session: CheckoutSession } | { type: "error"; error: XPayError };
|
|
367
366
|
```
|
|
368
367
|
|
|
369
368
|
**`XPayError` type:**
|
|
@@ -422,11 +421,10 @@ After payment, the customer is redirected to your `afterCompletion.redirect.url`
|
|
|
422
421
|
|
|
423
422
|
```javascript
|
|
424
423
|
// Your server — retrieves session using your API key (not from the client SDK)
|
|
425
|
-
app.get(
|
|
426
|
-
const response = await fetch(
|
|
427
|
-
`
|
|
428
|
-
|
|
429
|
-
);
|
|
424
|
+
app.get("/api/order-status", async (req, res) => {
|
|
425
|
+
const response = await fetch(`https://api.xpay.app/checkout/sessions/${req.query.session_id}`, {
|
|
426
|
+
headers: { Authorization: `Bearer ${process.env.XPAY_SECRET_KEY}` },
|
|
427
|
+
});
|
|
430
428
|
const session = await response.json();
|
|
431
429
|
res.json(session);
|
|
432
430
|
});
|
|
@@ -437,7 +435,7 @@ app.get('/api/order-status', async (req, res) => {
|
|
|
437
435
|
```tsx
|
|
438
436
|
function SuccessPage() {
|
|
439
437
|
const [session, setSession] = useState(null);
|
|
440
|
-
const sessionId = new URLSearchParams(window.location.search).get(
|
|
438
|
+
const sessionId = new URLSearchParams(window.location.search).get("session_id");
|
|
441
439
|
|
|
442
440
|
useEffect(() => {
|
|
443
441
|
fetch(`/api/order-status?session_id=${sessionId}`)
|
|
@@ -451,10 +449,14 @@ function SuccessPage() {
|
|
|
451
449
|
<div>
|
|
452
450
|
<h1>Payment {session.paymentStatus === "paid" ? "Confirmed" : "Processing"}</h1>
|
|
453
451
|
|
|
454
|
-
<p>
|
|
452
|
+
<p>
|
|
453
|
+
Order Total: {session.currency} {(session.amountTotal / 100).toFixed(2)}
|
|
454
|
+
</p>
|
|
455
455
|
|
|
456
456
|
{session.lineItems?.map((item) => (
|
|
457
|
-
<p key={item.id}>
|
|
457
|
+
<p key={item.id}>
|
|
458
|
+
{item.price?.product?.name} x {item.quantity}
|
|
459
|
+
</p>
|
|
458
460
|
))}
|
|
459
461
|
|
|
460
462
|
{/* Fee breakdown */}
|
|
@@ -492,21 +494,21 @@ function SuccessPage() {
|
|
|
492
494
|
|
|
493
495
|
**Session fields for display:**
|
|
494
496
|
|
|
495
|
-
| Field
|
|
496
|
-
|
|
497
|
-
| `session.status`
|
|
498
|
-
| `session.paymentStatus`
|
|
499
|
-
| `session.amountSubtotal`
|
|
500
|
-
| `session.amountTotal`
|
|
501
|
-
| `session.currency`
|
|
502
|
-
| `session.lineItems`
|
|
503
|
-
| `session.totalDetails.amountDiscount`
|
|
504
|
-
| `session.totalDetails.amountShipping`
|
|
505
|
-
| `session.totalDetails.amountTax`
|
|
506
|
-
| `session.totalDetails.amountPlatformFee`
|
|
507
|
-
| `session.totalDetails.amountCollectedVat` | `number`
|
|
508
|
-
| `session.customer`
|
|
509
|
-
| `session.merchantName`
|
|
497
|
+
| Field | Type | Description |
|
|
498
|
+
| ----------------------------------------- | ----------------------------------- | ---------------------------------------------- |
|
|
499
|
+
| `session.status` | `'open' \| 'complete' \| 'expired'` | Session status |
|
|
500
|
+
| `session.paymentStatus` | `'unpaid' \| 'paid'` | Payment status |
|
|
501
|
+
| `session.amountSubtotal` | `number` | Subtotal before discounts/fees (smallest unit) |
|
|
502
|
+
| `session.amountTotal` | `number` | Total amount charged (smallest unit) |
|
|
503
|
+
| `session.currency` | `string` | Currency code (e.g., `'EGP'`) |
|
|
504
|
+
| `session.lineItems` | `Array` | Line items with product name, quantity, amount |
|
|
505
|
+
| `session.totalDetails.amountDiscount` | `number` | Discount amount |
|
|
506
|
+
| `session.totalDetails.amountShipping` | `number` | Shipping amount |
|
|
507
|
+
| `session.totalDetails.amountTax` | `number` | Tax amount |
|
|
508
|
+
| `session.totalDetails.amountPlatformFee` | `number` | Platform fee (if feesPassThrough enabled) |
|
|
509
|
+
| `session.totalDetails.amountCollectedVat` | `number` | Collected VAT |
|
|
510
|
+
| `session.customer` | `object` | Customer name, email, phone |
|
|
511
|
+
| `session.merchantName` | `string` | Merchant display name |
|
|
510
512
|
|
|
511
513
|
## Step 5: Handle Webhooks [Server-side]
|
|
512
514
|
|
|
@@ -514,16 +516,16 @@ XPay sends webhook events when payment state changes. Listen for these on your s
|
|
|
514
516
|
|
|
515
517
|
```javascript
|
|
516
518
|
// Your server
|
|
517
|
-
app.post(
|
|
519
|
+
app.post("/webhooks/xpay", (req, res) => {
|
|
518
520
|
const event = req.body;
|
|
519
521
|
|
|
520
522
|
switch (event.type) {
|
|
521
|
-
case
|
|
523
|
+
case "checkout.session.completed":
|
|
522
524
|
// Payment succeeded — fulfill the order
|
|
523
525
|
// Send confirmation email, update database, start shipping
|
|
524
526
|
fulfillOrder(event.data);
|
|
525
527
|
break;
|
|
526
|
-
case
|
|
528
|
+
case "checkout.session.expired":
|
|
527
529
|
// Session expired without payment
|
|
528
530
|
break;
|
|
529
531
|
}
|
|
@@ -543,21 +545,18 @@ The webhook is the **source of truth** for order fulfillment. The client-side `c
|
|
|
543
545
|
Wraps your checkout UI. Provides XPay context to all child components.
|
|
544
546
|
|
|
545
547
|
```tsx
|
|
546
|
-
<XPayProvider
|
|
547
|
-
xpay={xpayPromise}
|
|
548
|
-
options={{ clientSecret, appearance, locale }}
|
|
549
|
-
>
|
|
548
|
+
<XPayProvider xpay={xpayPromise} options={{ clientSecret, appearance, locale }}>
|
|
550
549
|
{children}
|
|
551
550
|
</XPayProvider>
|
|
552
551
|
```
|
|
553
552
|
|
|
554
|
-
| Prop
|
|
555
|
-
|
|
556
|
-
| `xpay`
|
|
557
|
-
| `options`
|
|
558
|
-
| `options.clientSecret` | `string \| Promise<string>`
|
|
559
|
-
| `options.appearance`
|
|
560
|
-
| `options.locale`
|
|
553
|
+
| Prop | Type | Description |
|
|
554
|
+
| ---------------------- | ----------------------------------------------- | -------------------------------------------------------------------- |
|
|
555
|
+
| `xpay` | `XPayInstance \| Promise<XPayInstance> \| null` | XPay instance or promise from `loadXPay()`. Call at module level. |
|
|
556
|
+
| `options` | `{ clientSecret, appearance?, locale? }` | Must include the checkout session's `clientSecret`. |
|
|
557
|
+
| `options.clientSecret` | `string \| Promise<string>` | The session's client secret. Accepts a Promise for deferred loading. |
|
|
558
|
+
| `options.appearance` | `Appearance` | Override the session's branding settings at runtime. |
|
|
559
|
+
| `options.locale` | `"en" \| "ar"` | Locale for the payment form. |
|
|
561
560
|
|
|
562
561
|
### `useCheckout()`
|
|
563
562
|
|
|
@@ -580,37 +579,37 @@ After narrowing to `type: "success"`, the `checkout` object contains all session
|
|
|
580
579
|
|
|
581
580
|
**Session fields** (on `checkout`):
|
|
582
581
|
|
|
583
|
-
| Field
|
|
584
|
-
|
|
585
|
-
| `id`
|
|
586
|
-
| `amountSubtotal` | `number`
|
|
587
|
-
| `amountTotal`
|
|
588
|
-
| `currency`
|
|
589
|
-
| `merchantName`
|
|
590
|
-
| `livemode`
|
|
591
|
-
| `expiresAt`
|
|
592
|
-
| `status`
|
|
593
|
-
| `canConfirm`
|
|
594
|
-
| `paymentMethods` | `PaymentMethodInfo[]` | Available payment methods
|
|
595
|
-
| `lineItems`
|
|
596
|
-
| `totalDetails`
|
|
597
|
-
| `fees`
|
|
598
|
-
| `discounts`
|
|
582
|
+
| Field | Type | Description |
|
|
583
|
+
| ---------------- | --------------------- | ------------------------------------------------------------------------------ |
|
|
584
|
+
| `id` | `string` | Session ID |
|
|
585
|
+
| `amountSubtotal` | `number` | Subtotal before discounts/fees |
|
|
586
|
+
| `amountTotal` | `number` | Total amount (smallest unit) |
|
|
587
|
+
| `currency` | `string` | Currency code |
|
|
588
|
+
| `merchantName` | `string` | Merchant display name |
|
|
589
|
+
| `livemode` | `boolean` | Whether live mode |
|
|
590
|
+
| `expiresAt` | `string` | Session expiration |
|
|
591
|
+
| `status` | `SessionStatus` | `{type: "open"}` \| `{type: "expired"}` \| `{type: "complete", paymentStatus}` |
|
|
592
|
+
| `canConfirm` | `boolean` | Whether the session is ready for confirmation |
|
|
593
|
+
| `paymentMethods` | `PaymentMethodInfo[]` | Available payment methods |
|
|
594
|
+
| `lineItems` | `LineItem[]` | Line items |
|
|
595
|
+
| `totalDetails` | `TotalDetails` | Amounts breakdown |
|
|
596
|
+
| `fees` | `Fees` | Fee breakdown |
|
|
597
|
+
| `discounts` | `Discount[]` | Applied discounts |
|
|
599
598
|
|
|
600
599
|
**Action methods** (on `checkout`):
|
|
601
600
|
|
|
602
|
-
| Method
|
|
603
|
-
|
|
604
|
-
| `confirm`
|
|
605
|
-
| `applyPromotionCode`
|
|
606
|
-
| `removePromotionCode`
|
|
607
|
-
| `updateLineItemQuantity` | `({lineItem, quantity}) => Promise<ActionResult>` | Update line item quantity
|
|
608
|
-
| `submit`
|
|
609
|
-
| `fetchUpdates`
|
|
610
|
-
| `changeAppearance`
|
|
611
|
-
| `on`
|
|
612
|
-
| `on`
|
|
613
|
-
| `getElements`
|
|
601
|
+
| Method | Signature | Description |
|
|
602
|
+
| ------------------------ | ------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
603
|
+
| `confirm` | `(options?) => Promise<ActionResult>` | Confirm payment |
|
|
604
|
+
| `applyPromotionCode` | `(code) => Promise<ActionResult>` | Apply a promo code |
|
|
605
|
+
| `removePromotionCode` | `() => Promise<ActionResult>` | Remove promo code |
|
|
606
|
+
| `updateLineItemQuantity` | `({lineItem, quantity}) => Promise<ActionResult>` | Update line item quantity |
|
|
607
|
+
| `submit` | `() => Promise<{error?, selectedPaymentMethod?}>` | Validate fields |
|
|
608
|
+
| `fetchUpdates` | `() => Promise<ActionResult>` | Re-fetch session |
|
|
609
|
+
| `changeAppearance` | `(appearance) => void` | Update appearance |
|
|
610
|
+
| `on` | `("change", handler) => void` | Listen for session changes |
|
|
611
|
+
| `on` | `("error", handler) => void` | Listen for unsolicited errors (session expired during internal updates, BIN detection failure) |
|
|
612
|
+
| `getElements` | `() => Elements` | Access underlying Elements |
|
|
614
613
|
|
|
615
614
|
### `<PaymentElement>`
|
|
616
615
|
|
|
@@ -618,20 +617,20 @@ Renders the payment method selector and card form.
|
|
|
618
617
|
|
|
619
618
|
```tsx
|
|
620
619
|
<PaymentElement
|
|
621
|
-
options={{ layout:
|
|
620
|
+
options={{ layout: "accordion" }}
|
|
622
621
|
onChange={(e) => console.log(e.complete, e.value.type)}
|
|
623
622
|
/>
|
|
624
623
|
```
|
|
625
624
|
|
|
626
|
-
| Prop
|
|
627
|
-
|
|
628
|
-
| `options`
|
|
629
|
-
| `onChange`
|
|
630
|
-
| `onReady`
|
|
631
|
-
| `onLoaderStart` | `() => void`
|
|
632
|
-
| `onLoadError`
|
|
633
|
-
| `className`
|
|
634
|
-
| `id`
|
|
625
|
+
| Prop | Type | Description |
|
|
626
|
+
| --------------- | --------------------------------------------------------- | --------------------------------------------------------------------- |
|
|
627
|
+
| `options` | `{ layout?, defaultPaymentMethod?, paymentMethodOrder? }` | Configuration |
|
|
628
|
+
| `onChange` | `(event: PaymentElementChangeEvent) => void` | Form state changed |
|
|
629
|
+
| `onReady` | `() => void` | Element initialized (async, fires from `XPAY_SDK_INITIALIZED`) |
|
|
630
|
+
| `onLoaderStart` | `() => void` | Loader animation started (fires synchronously when iframe is created) |
|
|
631
|
+
| `onLoadError` | `(event) => void` | Element failed to load |
|
|
632
|
+
| `className` | `string` | CSS class for the container div |
|
|
633
|
+
| `id` | `string` | ID for the container div |
|
|
635
634
|
|
|
636
635
|
### `<CheckoutButton>`
|
|
637
636
|
|
|
@@ -640,7 +639,7 @@ Opens the drop-in checkout modal on click.
|
|
|
640
639
|
```tsx
|
|
641
640
|
<CheckoutButton
|
|
642
641
|
clientSecret="cs_test_abc_secret_xyz"
|
|
643
|
-
checkoutOptions={{ onComplete: (r) => router.push(
|
|
642
|
+
checkoutOptions={{ onComplete: (r) => router.push("/success") }}
|
|
644
643
|
>
|
|
645
644
|
Pay Now
|
|
646
645
|
</CheckoutButton>
|
|
@@ -663,7 +662,7 @@ A button that opens the full checkout in a modal overlay. No form needed.
|
|
|
663
662
|
<CheckoutButton
|
|
664
663
|
clientSecret={clientSecret}
|
|
665
664
|
checkoutOptions={{
|
|
666
|
-
onComplete: (result) => window.location.href = `/orders/${orderId}
|
|
665
|
+
onComplete: (result) => (window.location.href = `/orders/${orderId}`),
|
|
667
666
|
onClose: () => console.log("Closed"),
|
|
668
667
|
}}
|
|
669
668
|
>
|
|
@@ -710,11 +709,11 @@ await checkout.confirm({
|
|
|
710
709
|
// ^ If successful, the page navigates away. Code below only runs on error.
|
|
711
710
|
```
|
|
712
711
|
|
|
713
|
-
| `redirect`
|
|
714
|
-
|
|
715
|
-
| Not set (default) | `"if_required"` — returns result to your code
|
|
716
|
-
| `"always"`
|
|
717
|
-
| `"if_required"`
|
|
712
|
+
| `redirect` | Behavior |
|
|
713
|
+
| ----------------- | --------------------------------------------------------- |
|
|
714
|
+
| Not set (default) | `"if_required"` — returns result to your code |
|
|
715
|
+
| `"always"` | Redirects to the session's `afterCompletion.redirect.url` |
|
|
716
|
+
| `"if_required"` | Returns result to your code — no redirect |
|
|
718
717
|
|
|
719
718
|
Your server sets that URL when it creates the session. XPay navigates there unchanged, appending nothing.
|
|
720
719
|
|
|
@@ -750,12 +749,7 @@ Override the session's `brandingSettings` at runtime. Uses the same shape.
|
|
|
750
749
|
All components and hooks are fully typed. `@xpayeg/react` re-exports key SDK types for convenience:
|
|
751
750
|
|
|
752
751
|
```tsx
|
|
753
|
-
import type {
|
|
754
|
-
Checkout,
|
|
755
|
-
CheckoutSession,
|
|
756
|
-
CheckoutActions,
|
|
757
|
-
UseCheckoutResult,
|
|
758
|
-
} from "@xpayeg/react";
|
|
752
|
+
import type { Checkout, CheckoutSession, CheckoutActions, UseCheckoutResult } from "@xpayeg/react";
|
|
759
753
|
|
|
760
754
|
// Or import additional types from @xpayeg/sdk directly:
|
|
761
755
|
import type {
|
|
@@ -771,3 +765,11 @@ import type {
|
|
|
771
765
|
CheckoutDiscount,
|
|
772
766
|
} from "@xpayeg/sdk";
|
|
773
767
|
```
|
|
768
|
+
|
|
769
|
+
## Development checks
|
|
770
|
+
|
|
771
|
+
From the monorepo, run `pnpm --filter @xpayeg/react test` for the React DOM
|
|
772
|
+
contracts. These use jsdom and a synthetic Elements boundary to check option
|
|
773
|
+
updates, StrictMode teardown, and current callbacks without subscription churn.
|
|
774
|
+
They do not create checkout sessions or submit payments. Run
|
|
775
|
+
`pnpm --filter @xpayeg/react typecheck` for source, test, and public consumer types.
|
package/dist/index.cjs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
let react = require("react");
|
|
3
3
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
4
|
-
//#region src/context.tsx
|
|
5
4
|
const XPayContext = (0, react.createContext)(null);
|
|
6
5
|
const ElementsContext = (0, react.createContext)(null);
|
|
7
6
|
/** Build a minimal XPayError for SDK-level failures (elements not initialized, etc.) */
|
|
@@ -269,9 +268,9 @@ function useCheckoutActions(ctx) {
|
|
|
269
268
|
changeAppearanceFn: (0, react.useCallback)((appearance) => {
|
|
270
269
|
elements?.changeAppearance(appearance);
|
|
271
270
|
}, [elements]),
|
|
272
|
-
onChangeFn: (0, react.useCallback)((
|
|
271
|
+
onChangeFn: (0, react.useCallback)((event, handler) => {
|
|
273
272
|
elements?.on(event, handler);
|
|
274
|
-
}
|
|
273
|
+
}, [elements]),
|
|
275
274
|
getElementsFn: (0, react.useCallback)(() => {
|
|
276
275
|
if (!elements) throw new Error("Elements not initialized");
|
|
277
276
|
return elements;
|
|
@@ -296,8 +295,6 @@ function useXPay() {
|
|
|
296
295
|
function useElements() {
|
|
297
296
|
return (0, react.useContext)(ElementsContext);
|
|
298
297
|
}
|
|
299
|
-
//#endregion
|
|
300
|
-
//#region src/hooks.ts
|
|
301
298
|
/**
|
|
302
299
|
* Convenience hook for payment confirmation.
|
|
303
300
|
*
|
|
@@ -323,8 +320,6 @@ function useConfirmPayment() {
|
|
|
323
320
|
isConfirming: false
|
|
324
321
|
};
|
|
325
322
|
}
|
|
326
|
-
//#endregion
|
|
327
|
-
//#region src/utils/useAttachEvent.ts
|
|
328
323
|
/**
|
|
329
324
|
* Attach an event listener to an element without causing listener churn on re-renders.
|
|
330
325
|
*
|
|
@@ -352,21 +347,6 @@ function useAttachEvent(element, event, cb) {
|
|
|
352
347
|
element
|
|
353
348
|
]);
|
|
354
349
|
}
|
|
355
|
-
//#endregion
|
|
356
|
-
//#region src/utils/usePrevious.ts
|
|
357
|
-
/**
|
|
358
|
-
* Returns the previous value of a variable.
|
|
359
|
-
* Useful for detecting prop changes between renders.
|
|
360
|
-
*/
|
|
361
|
-
function usePrevious(value) {
|
|
362
|
-
const ref = (0, react.useRef)(void 0);
|
|
363
|
-
(0, react.useEffect)(() => {
|
|
364
|
-
ref.current = value;
|
|
365
|
-
}, [value]);
|
|
366
|
-
return ref.current;
|
|
367
|
-
}
|
|
368
|
-
//#endregion
|
|
369
|
-
//#region src/utils/extractAllowedOptionsUpdates.ts
|
|
370
350
|
/**
|
|
371
351
|
* Extract only the changed, mutable options from a new options object.
|
|
372
352
|
*
|
|
@@ -408,19 +388,15 @@ function isEqual(a, b) {
|
|
|
408
388
|
}
|
|
409
389
|
return false;
|
|
410
390
|
}
|
|
411
|
-
//#endregion
|
|
412
|
-
//#region src/utils/isServer.ts
|
|
413
391
|
/** True when running in a server environment (SSR/RSC) */
|
|
414
392
|
const isServer = typeof window === "undefined";
|
|
415
|
-
//#endregion
|
|
416
|
-
//#region src/PaymentElement.tsx
|
|
417
393
|
const IMMUTABLE_OPTS = [];
|
|
418
394
|
const PaymentElementClient = ({ options, onReady, onChange, onLoaderStart, onLoadError, className, id }) => {
|
|
419
395
|
const elements = useElements();
|
|
420
396
|
const containerRef = (0, react.useRef)(null);
|
|
421
397
|
const elementRef = (0, react.useRef)(null);
|
|
422
398
|
const [element, setElement] = (0, react.useState)(null);
|
|
423
|
-
const
|
|
399
|
+
const previousOptions = (0, react.useRef)(void 0);
|
|
424
400
|
(0, react.useLayoutEffect)(() => {
|
|
425
401
|
if (elementRef.current !== null || !elements || !containerRef.current) return;
|
|
426
402
|
const el = elements.create("payment", options);
|
|
@@ -440,14 +416,12 @@ const PaymentElementClient = ({ options, onReady, onChange, onLoaderStart, onLoa
|
|
|
440
416
|
};
|
|
441
417
|
}, []);
|
|
442
418
|
(0, react.useEffect)(() => {
|
|
419
|
+
const prevOptions = previousOptions.current;
|
|
420
|
+
previousOptions.current = options;
|
|
443
421
|
if (!element || !options) return;
|
|
444
422
|
const updates = extractAllowedOptionsUpdates(options, prevOptions, IMMUTABLE_OPTS);
|
|
445
423
|
if (updates && "update" in element) element.update(updates);
|
|
446
|
-
}, [
|
|
447
|
-
options,
|
|
448
|
-
prevOptions,
|
|
449
|
-
element
|
|
450
|
-
]);
|
|
424
|
+
}, [options, element]);
|
|
451
425
|
useAttachEvent(element, "ready", onReady);
|
|
452
426
|
useAttachEvent(element, "change", onChange);
|
|
453
427
|
useAttachEvent(element, "loaderstart", onLoaderStart);
|
|
@@ -482,8 +456,6 @@ const PaymentElementServer = ({ className, id }) => {
|
|
|
482
456
|
* ```
|
|
483
457
|
*/
|
|
484
458
|
const PaymentElement = isServer ? PaymentElementServer : PaymentElementClient;
|
|
485
|
-
//#endregion
|
|
486
|
-
//#region src/CheckoutButton.tsx
|
|
487
459
|
/**
|
|
488
460
|
* Button that opens the drop-in checkout modal on click.
|
|
489
461
|
*
|
|
@@ -506,26 +478,26 @@ const PaymentElement = isServer ? PaymentElementServer : PaymentElementClient;
|
|
|
506
478
|
*/
|
|
507
479
|
const CheckoutButton = ({ clientSecret, children = "Pay", checkoutOptions, className, disabled }) => {
|
|
508
480
|
const xpay = useXPay();
|
|
481
|
+
const handleClick = (0, react.useCallback)(() => {
|
|
482
|
+
if (!xpay) return;
|
|
483
|
+
xpay.checkout({
|
|
484
|
+
clientSecret,
|
|
485
|
+
mode: "modal",
|
|
486
|
+
...checkoutOptions
|
|
487
|
+
}).open();
|
|
488
|
+
}, [
|
|
489
|
+
xpay,
|
|
490
|
+
clientSecret,
|
|
491
|
+
checkoutOptions
|
|
492
|
+
]);
|
|
509
493
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
510
494
|
type: "button",
|
|
511
|
-
onClick:
|
|
512
|
-
if (!xpay) return;
|
|
513
|
-
xpay.checkout({
|
|
514
|
-
clientSecret,
|
|
515
|
-
mode: "modal",
|
|
516
|
-
...checkoutOptions
|
|
517
|
-
}).open();
|
|
518
|
-
}, [
|
|
519
|
-
xpay,
|
|
520
|
-
clientSecret,
|
|
521
|
-
checkoutOptions
|
|
522
|
-
]),
|
|
495
|
+
onClick: handleClick,
|
|
523
496
|
disabled: disabled || !xpay,
|
|
524
497
|
className,
|
|
525
498
|
children
|
|
526
499
|
});
|
|
527
500
|
};
|
|
528
|
-
//#endregion
|
|
529
501
|
exports.CheckoutButton = CheckoutButton;
|
|
530
502
|
exports.PaymentElement = PaymentElement;
|
|
531
503
|
exports.XPayProvider = XPayProvider;
|