@xpayeg/react 2.3.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/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('/api/create-checkout', async (req, res) => {
24
- const response = await fetch('https://api.xpay.app/checkout/sessions', {
25
- method: 'POST',
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
- 'Authorization': `Bearer ${process.env.XPAY_SECRET_KEY}`,
28
- 'Content-Type': 'application/json',
29
+ Authorization: `Bearer ${process.env.XPAY_SECRET_KEY}`,
30
+ "Content-Type": "application/json",
29
31
  },
30
32
  body: JSON.stringify({
31
- uiMode: 'custom', // 'custom' for Elements SDK, 'embedded' for drop-in, 'hosted' for redirect
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, // Amount in smallest unit (500.00 EGP = 50000 piasters)
36
- currency: 'EGP',
37
+ unitAmount: 50000, // Amount in smallest unit (500.00 EGP = 50000 piasters)
38
+ currency: "EGP",
37
39
  productData: {
38
- name: 'Premium Plan',
39
- description: 'Monthly subscription',
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: 'redirect',
48
+ type: "redirect",
47
49
  redirect: {
48
50
  // {CHECKOUT_SESSION_ID} is automatically replaced with the session ID
49
- url: 'https://yoursite.com/success?session_id={CHECKOUT_SESSION_ID}',
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: 'system' },
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 | 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 |
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>Total: {checkout.currency} {(checkout.amountTotal / 100).toFixed(2)}</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 | 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 |
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
- | { type: "success"; session: CheckoutSession }
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('/api/order-status', async (req, res) => {
426
- const response = await fetch(
427
- `https://api.xpay.app/checkout/sessions/${req.query.session_id}`,
428
- { headers: { 'Authorization': `Bearer ${process.env.XPAY_SECRET_KEY}` } },
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('session_id');
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>Order Total: {session.currency} {(session.amountTotal / 100).toFixed(2)}</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}>{item.price?.product?.name} x {item.quantity}</p>
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 | Type | Description |
496
- |-------|------|-------------|
497
- | `session.status` | `'open' \| 'complete' \| 'expired'` | Session status |
498
- | `session.paymentStatus` | `'unpaid' \| 'paid'` | Payment status |
499
- | `session.amountSubtotal` | `number` | Subtotal before discounts/fees (smallest unit) |
500
- | `session.amountTotal` | `number` | Total amount charged (smallest unit) |
501
- | `session.currency` | `string` | Currency code (e.g., `'EGP'`) |
502
- | `session.lineItems` | `Array` | Line items with product name, quantity, amount |
503
- | `session.totalDetails.amountDiscount` | `number` | Discount amount |
504
- | `session.totalDetails.amountShipping` | `number` | Shipping amount |
505
- | `session.totalDetails.amountTax` | `number` | Tax amount |
506
- | `session.totalDetails.amountPlatformFee` | `number` | Platform fee (if feesPassThrough enabled) |
507
- | `session.totalDetails.amountCollectedVat` | `number` | Collected VAT |
508
- | `session.customer` | `object` | Customer name, email, phone |
509
- | `session.merchantName` | `string` | Merchant display name |
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('/webhooks/xpay', (req, res) => {
519
+ app.post("/webhooks/xpay", (req, res) => {
518
520
  const event = req.body;
519
521
 
520
522
  switch (event.type) {
521
- case 'checkout.session.completed':
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 'checkout.session.expired':
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 | Type | Description |
555
- |------|------|-------------|
556
- | `xpay` | `XPayInstance \| Promise<XPayInstance> \| null` | XPay instance or promise from `loadXPay()`. Call at module level. |
557
- | `options` | `{ clientSecret, appearance?, locale? }` | Must include the checkout session's `clientSecret`. |
558
- | `options.clientSecret` | `string \| Promise<string>` | The session's client secret. Accepts a Promise for deferred loading. |
559
- | `options.appearance` | `Appearance` | Override the session's branding settings at runtime. |
560
- | `options.locale` | `"en" \| "ar"` | Locale for the payment form. |
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 | Type | Description |
584
- |-------|------|-------------|
585
- | `id` | `string` | Session ID |
586
- | `amountSubtotal` | `number` | Subtotal before discounts/fees |
587
- | `amountTotal` | `number` | Total amount (smallest unit) |
588
- | `currency` | `string` | Currency code |
589
- | `merchantName` | `string` | Merchant display name |
590
- | `livemode` | `boolean` | Whether live mode |
591
- | `expiresAt` | `string` | Session expiration |
592
- | `status` | `SessionStatus` | `{type: "open"}` \| `{type: "expired"}` \| `{type: "complete", paymentStatus}` |
593
- | `canConfirm` | `boolean` | Whether the session is ready for confirmation |
594
- | `paymentMethods` | `PaymentMethodInfo[]` | Available payment methods |
595
- | `lineItems` | `LineItem[]` | Line items |
596
- | `totalDetails` | `TotalDetails` | Amounts breakdown |
597
- | `fees` | `Fees` | Fee breakdown |
598
- | `discounts` | `Discount[]` | Applied 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 | Signature | Description |
603
- |--------|-----------|-------------|
604
- | `confirm` | `(options?) => Promise<ActionResult>` | Confirm payment |
605
- | `applyPromotionCode` | `(code) => Promise<ActionResult>` | Apply a promo code |
606
- | `removePromotionCode` | `() => Promise<ActionResult>` | Remove promo code |
607
- | `updateLineItemQuantity` | `({lineItem, quantity}) => Promise<ActionResult>` | Update line item quantity |
608
- | `submit` | `() => Promise<{error?, selectedPaymentMethod?}>` | Validate fields |
609
- | `fetchUpdates` | `() => Promise<ActionResult>` | Re-fetch session |
610
- | `changeAppearance` | `(appearance) => void` | Update appearance |
611
- | `on` | `("change", handler) => void` | Listen for session changes |
612
- | `on` | `("error", handler) => void` | Listen for unsolicited errors (session expired during internal updates, BIN detection failure) |
613
- | `getElements` | `() => Elements` | Access underlying Elements |
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: 'accordion' }}
620
+ options={{ layout: "accordion" }}
622
621
  onChange={(e) => console.log(e.complete, e.value.type)}
623
622
  />
624
623
  ```
625
624
 
626
- | Prop | Type | Description |
627
- |------|------|-------------|
628
- | `options` | `{ layout?, defaultPaymentMethod?, paymentMethodOrder? }` | Configuration |
629
- | `onChange` | `(event: PaymentElementChangeEvent) => void` | Form state changed |
630
- | `onReady` | `() => void` | Element initialized (async, fires from `XPAY_SDK_INITIALIZED`) |
631
- | `onLoaderStart` | `() => void` | Loader animation started (fires synchronously when iframe is created) |
632
- | `onLoadError` | `(event) => void` | Element failed to load |
633
- | `className` | `string` | CSS class for the container div |
634
- | `id` | `string` | ID for the container div |
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('/success') }}
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` | Behavior |
714
- |---|---|
715
- | Not set (default) | `"if_required"` — returns result to your code |
716
- | `"always"` | Redirects to the session's `afterCompletion.redirect.url` |
717
- | `"if_required"` | Returns result to your code — no redirect |
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)(((event, handler) => {
271
+ onChangeFn: (0, react.useCallback)((event, handler) => {
273
272
  elements?.on(event, handler);
274
- }), [elements]),
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 prevOptions = usePrevious(options);
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: (0, react.useCallback)(() => {
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;