@xpayeg/react 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/README.md ADDED
@@ -0,0 +1,824 @@
1
+ # @xpayeg/react
2
+
3
+ React components and hooks for XPay payments. A thin wrapper around the [XPay JS SDK](https://www.npmjs.com/package/@xpayeg/sdk) that provides React-friendly APIs for embedding payment forms.
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
+ ```bash
14
+ npm install @xpayeg/sdk @xpayeg/react
15
+ ```
16
+
17
+ ## Step 1: Create a Checkout Session [Server-side]
18
+
19
+ 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
+
21
+ ```javascript
22
+ // 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',
26
+ headers: {
27
+ 'Authorization': `Bearer ${process.env.XPAY_SECRET_KEY}`,
28
+ 'Content-Type': 'application/json',
29
+ },
30
+ body: JSON.stringify({
31
+ uiMode: 'custom', // 'custom' for Elements SDK, 'embedded' for drop-in, 'hosted' for redirect
32
+ lineItems: [
33
+ {
34
+ priceData: {
35
+ unitAmount: 50000, // Amount in smallest unit (500.00 EGP = 50000 piasters)
36
+ currency: 'EGP',
37
+ productData: {
38
+ name: 'Premium Plan',
39
+ description: 'Monthly subscription',
40
+ },
41
+ },
42
+ quantity: 1,
43
+ },
44
+ ],
45
+ afterCompletion: {
46
+ type: 'redirect',
47
+ redirect: {
48
+ // {CHECKOUT_SESSION_ID} is automatically replaced with the session ID
49
+ url: 'https://yoursite.com/success?session_id={CHECKOUT_SESSION_ID}',
50
+ },
51
+ },
52
+ // Optional
53
+ customerDetails: { email: req.body.email },
54
+ brandingSettings: { colorMode: 'system' },
55
+ }),
56
+ });
57
+
58
+ const session = await response.json();
59
+ res.json({ clientSecret: session.clientSecret });
60
+ });
61
+ ```
62
+
63
+ ## Step 2: Set Up the Frontend [Client-side]
64
+
65
+ Load XPay at module level (outside any component) and wrap your checkout UI with `XPayProvider`.
66
+
67
+ The `clientSecret` option accepts either a string or a `Promise<string>`, so you can pass the fetch directly without managing loading state yourself.
68
+
69
+ ```tsx
70
+ import { loadXPay } from "@xpayeg/sdk";
71
+ import { XPayProvider, PaymentElement, useCheckout } from "@xpayeg/react";
72
+
73
+ // Load once at module level — not inside a component
74
+ const xpayPromise = loadXPay("pk_test_xxx");
75
+
76
+ // Fetch clientSecret as a Promise — no useState/useEffect needed
77
+ const fetchClientSecret = fetch("/api/create-checkout", { method: "POST" })
78
+ .then((r) => r.json())
79
+ .then((d) => d.clientSecret as string);
80
+
81
+ function App() {
82
+ return (
83
+ <XPayProvider xpay={xpayPromise} options={{ clientSecret: fetchClientSecret }}>
84
+ <CheckoutForm />
85
+ </XPayProvider>
86
+ );
87
+ }
88
+ ```
89
+
90
+ You can also pass `clientSecret` as a plain string if you already have it:
91
+
92
+ ```tsx
93
+ <XPayProvider xpay={xpayPromise} options={{ clientSecret: "cs_test_abc_secret_xyz" }}>
94
+ <CheckoutForm />
95
+ </XPayProvider>
96
+ ```
97
+
98
+ ## Step 3: Build the Checkout Form
99
+
100
+ `useCheckout()` returns a disjoint union — handle each state explicitly before accessing session data or methods.
101
+
102
+ ```tsx
103
+ function CheckoutForm() {
104
+ const checkoutState = useCheckout();
105
+
106
+ if (checkoutState.type === "loading") {
107
+ return <div className="animate-pulse h-48 bg-gray-100 rounded-lg" />;
108
+ }
109
+
110
+ if (checkoutState.type === "error") {
111
+ return <div className="text-red-500">Failed to load: {checkoutState.error.message}</div>;
112
+ }
113
+
114
+ const { currency, amountTotal, lineItems, confirm, applyPromotionCode } = checkoutState.checkout;
115
+
116
+ const handleSubmit = async (e: React.FormEvent) => {
117
+ e.preventDefault();
118
+
119
+ const result = await confirm({
120
+ customerDetails: { email: "customer@example.com", name: "John Doe" },
121
+ // Default: redirect: "if_required" — returns result to your code
122
+ // Use redirect: "always" to redirect to afterCompletion.redirect.url
123
+ });
124
+
125
+ // This code only runs if redirect: "if_required" or if redirect fails
126
+ if (result.type === "error") {
127
+ console.error(result.error.message);
128
+ }
129
+ };
130
+
131
+ return (
132
+ <form onSubmit={handleSubmit}>
133
+ <p className="text-lg font-semibold">
134
+ Total: {currency} {(amountTotal / 100).toFixed(2)}
135
+ </p>
136
+
137
+ <PaymentElement />
138
+
139
+ <button type="submit">
140
+ Pay {currency} {(amountTotal / 100).toFixed(2)}
141
+ </button>
142
+ </form>
143
+ );
144
+ }
145
+ ```
146
+
147
+ All payment actions (3DS challenges, Valu/PayTabs modals) are handled automatically inside `confirm()`. The promise resolves only when the full payment flow completes.
148
+
149
+ ### Handling Session Status
150
+
151
+ After narrowing to the `success` state, check the session status to handle expired or completed sessions:
152
+
153
+ ```tsx
154
+ function CheckoutPage() {
155
+ const checkoutState = useCheckout();
156
+
157
+ if (checkoutState.type === "loading") return <Loading />;
158
+ if (checkoutState.type === "error") return <Error message={checkoutState.error.message} />;
159
+
160
+ const { checkout } = checkoutState;
161
+
162
+ if (checkout.status.type === "expired") {
163
+ return <div>This checkout session has expired. Please start a new order.</div>;
164
+ }
165
+
166
+ if (checkout.status.type === "complete") {
167
+ return <div>Payment complete. Thank you!</div>;
168
+ }
169
+
170
+ // status.type === "open" — render the payment form
171
+ return <CheckoutForm />;
172
+ }
173
+ ```
174
+
175
+ ### Displaying Session Data
176
+
177
+ The `checkout` object on the success state contains all session data. Read `amountTotal`, `currency`, `paymentMethods`, and `merchantName` directly — no separate `sessionData` accessor needed.
178
+
179
+ ```tsx
180
+ function OrderSummary() {
181
+ const checkoutState = useCheckout();
182
+
183
+ if (checkoutState.type === "loading") return <div>Loading...</div>;
184
+ if (checkoutState.type === "error") return <div>Error: {checkoutState.error.message}</div>;
185
+
186
+ const { checkout } = checkoutState;
187
+
188
+ return (
189
+ <div>
190
+ {/* Merchant name */}
191
+ <h2>{checkout.merchantName}</h2>
192
+
193
+ {/* Amount and currency from session */}
194
+ <p className="text-2xl font-bold">
195
+ {checkout.currency} {(checkout.amountTotal / 100).toFixed(2)}
196
+ </p>
197
+
198
+ {/* Available payment methods */}
199
+ <p className="text-sm text-gray-500">
200
+ Pay with: {checkout.paymentMethods.map((pm) => pm.displayName).join(", ")}
201
+ </p>
202
+
203
+ {/* Live/test mode indicator */}
204
+ {!checkout.livemode && (
205
+ <span className="text-xs bg-yellow-100 text-yellow-800 px-2 py-1 rounded">
206
+ Test Mode
207
+ </span>
208
+ )}
209
+ </div>
210
+ );
211
+ }
212
+ ```
213
+
214
+ **Session fields on `checkout`:**
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 |
232
+
233
+ ### Updating the Session (Promo Codes, Quantities)
234
+
235
+ Action methods return `Promise<ActionResult>` — a tagged union of `{type: "success", session}` or `{type: "error", error}`. The hook's state updates reactively after each action, so `checkout.amountTotal`, `checkout.lineItems`, and `checkout.totalDetails` always reflect the latest values.
236
+
237
+ ```tsx
238
+ function CheckoutWithPromo() {
239
+ const checkoutState = useCheckout();
240
+ const [promoCode, setPromoCode] = useState("");
241
+ const [promoError, setPromoError] = useState("");
242
+
243
+ if (checkoutState.type !== "success") return <div>Loading...</div>;
244
+
245
+ const { checkout } = checkoutState;
246
+
247
+ const handleApplyPromo = async () => {
248
+ setPromoError("");
249
+ const result = await checkout.applyPromotionCode(promoCode);
250
+ if (result.type === "error") {
251
+ setPromoError(result.error.message);
252
+ }
253
+ };
254
+
255
+ return (
256
+ <div>
257
+ {/* Promo code input */}
258
+ <input
259
+ value={promoCode}
260
+ onChange={(e) => setPromoCode(e.target.value)}
261
+ placeholder="Promo code"
262
+ />
263
+ <button onClick={handleApplyPromo}>Apply</button>
264
+ <button onClick={() => checkout.removePromotionCode()}>Remove</button>
265
+ {promoError && <p className="text-red-500 text-sm">{promoError}</p>}
266
+
267
+ {/* Line items with quantity controls */}
268
+ {checkout.lineItems?.map((item) => (
269
+ <div key={item.id}>
270
+ <span>{item.description}</span>
271
+ <button
272
+ onClick={() =>
273
+ checkout.updateLineItemQuantity({ lineItem: item.id, quantity: item.quantity + 1 })
274
+ }
275
+ >
276
+ +
277
+ </button>
278
+ <button
279
+ onClick={() =>
280
+ checkout.updateLineItemQuantity({
281
+ lineItem: item.id,
282
+ quantity: Math.max(1, item.quantity - 1),
283
+ })
284
+ }
285
+ >
286
+ -
287
+ </button>
288
+ </div>
289
+ ))}
290
+
291
+ {/* Total updates reactively */}
292
+ <p>Total: {checkout.currency} {(checkout.amountTotal / 100).toFixed(2)}</p>
293
+
294
+ {/* Fee breakdown (when feesPassThrough enabled) */}
295
+ {checkout.totalDetails?.amountPlatformFee && (
296
+ <p>
297
+ Processing Fee: {(checkout.totalDetails.amountPlatformFee / 100).toFixed(2)}
298
+ </p>
299
+ )}
300
+
301
+ <PaymentElement />
302
+ <button onClick={() => checkout.confirm()}>Pay</button>
303
+ </div>
304
+ );
305
+ }
306
+ ```
307
+
308
+ **Reactive state:** `useCheckout()` updates automatically whenever the session changes — after applying a promo code, changing quantities, or any server-side update. There is no need for a separate `onChange` callback in React. The component re-renders with the latest `checkout` data.
309
+
310
+ ### Error Event
311
+
312
+ Use `checkout.on("error", handler)` to listen for unsolicited errors -- errors that occur outside of a merchant-initiated action. Examples: session expired during fee recalculation when switching payment methods, BIN detection failure.
313
+
314
+ ```tsx
315
+ function CheckoutForm({ checkout }: { checkout: Checkout }) {
316
+ const [error, setError] = useState("");
317
+
318
+ useEffect(() => {
319
+ checkout.on("error", (err) => {
320
+ console.error("[XPay SDK] Unsolicited error:", err.code, err.message);
321
+ setError(err.message);
322
+ });
323
+ }, [checkout]);
324
+
325
+ return (
326
+ <div>
327
+ {error && <p className="text-red-500">{error}</p>}
328
+ <PaymentElement />
329
+ </div>
330
+ );
331
+ }
332
+ ```
333
+
334
+ The error object shape:
335
+
336
+ ```typescript
337
+ {
338
+ type: "invalid_request_error"; // error category
339
+ code: "checkout_session_expired"; // machine-readable code
340
+ message: "This checkout session has expired"; // human-readable message
341
+ docUrl?: "https://docs.xpay.app/api/errors#checkout_session_expired";
342
+ }
343
+ ```
344
+
345
+ ### Action Methods
346
+
347
+ All action methods live on the `checkout` object returned from the `success` state.
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 |
360
+
361
+ **`ActionResult` type:**
362
+
363
+ ```typescript
364
+ type ActionResult =
365
+ | { type: "success"; session: CheckoutSession }
366
+ | { type: "error"; error: XPayError };
367
+ ```
368
+
369
+ **`XPayError` type:**
370
+
371
+ ```typescript
372
+ interface XPayError {
373
+ type: string;
374
+ message: string;
375
+ code: string | null;
376
+ declineCode?: string | null;
377
+ }
378
+ ```
379
+
380
+ ### `submit()` and `fetchUpdates()`
381
+
382
+ Use `submit()` to validate all payment fields before calling `confirm()`. This is useful when you want to validate the form as a separate step (for example, when showing a confirmation dialog).
383
+
384
+ ```tsx
385
+ const handleSubmit = async () => {
386
+ // Step 1: validate fields
387
+ const { error, selectedPaymentMethod } = await checkout.submit();
388
+ if (error) {
389
+ showError(error.message);
390
+ return;
391
+ }
392
+
393
+ // Step 2: show confirmation dialog
394
+ const confirmed = await showConfirmDialog(selectedPaymentMethod);
395
+ if (!confirmed) return;
396
+
397
+ // Step 3: confirm payment
398
+ const result = await checkout.confirm();
399
+ if (result.type === "error") {
400
+ showError(result.error.message);
401
+ }
402
+ };
403
+ ```
404
+
405
+ Use `fetchUpdates()` to re-fetch the session from the server. This is useful when you know the session has been updated server-side (for example, after adding a shipping address that changes the total).
406
+
407
+ ```tsx
408
+ const handleAddressChange = async () => {
409
+ await saveAddressToServer(address);
410
+ const result = await checkout.fetchUpdates();
411
+ if (result.type === "success") {
412
+ // checkout state is now up to date
413
+ }
414
+ };
415
+ ```
416
+
417
+ ## Step 4: Show a Success Page [Server-side + Client-side]
418
+
419
+ After payment, the customer is redirected to your `afterCompletion.redirect.url`. Retrieve the session from **your server** (using your API key) to display order details.
420
+
421
+ **Server endpoint:**
422
+
423
+ ```javascript
424
+ // 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
+ );
430
+ const session = await response.json();
431
+ res.json(session);
432
+ });
433
+ ```
434
+
435
+ **React success page:**
436
+
437
+ ```tsx
438
+ function SuccessPage() {
439
+ const [session, setSession] = useState(null);
440
+ const sessionId = new URLSearchParams(window.location.search).get('session_id');
441
+
442
+ useEffect(() => {
443
+ fetch(`/api/order-status?session_id=${sessionId}`)
444
+ .then((r) => r.json())
445
+ .then(setSession);
446
+ }, [sessionId]);
447
+
448
+ if (!session) return <div>Loading order details...</div>;
449
+
450
+ return (
451
+ <div>
452
+ <h1>Payment {session.paymentStatus === "paid" ? "Confirmed" : "Processing"}</h1>
453
+
454
+ <p>Order Total: {session.currency} {(session.amountTotal / 100).toFixed(2)}</p>
455
+
456
+ {session.lineItems?.map((item) => (
457
+ <p key={item.id}>{item.price?.product?.name} x {item.quantity}</p>
458
+ ))}
459
+
460
+ {/* Fee breakdown */}
461
+ {session.totalDetails && (
462
+ <dl>
463
+ <dt>Subtotal</dt>
464
+ <dd>{(session.amountSubtotal / 100).toFixed(2)}</dd>
465
+
466
+ {session.totalDetails.amountDiscount !== 0 && (
467
+ <>
468
+ <dt>Discount</dt>
469
+ <dd>-{(session.totalDetails.amountDiscount / 100).toFixed(2)}</dd>
470
+ </>
471
+ )}
472
+
473
+ {session.totalDetails.amountPlatformFee && (
474
+ <>
475
+ <dt>Processing Fee</dt>
476
+ <dd>{(session.totalDetails.amountPlatformFee / 100).toFixed(2)}</dd>
477
+ </>
478
+ )}
479
+
480
+ {session.totalDetails.amountCollectedVat && (
481
+ <>
482
+ <dt>VAT</dt>
483
+ <dd>{(session.totalDetails.amountCollectedVat / 100).toFixed(2)}</dd>
484
+ </>
485
+ )}
486
+ </dl>
487
+ )}
488
+ </div>
489
+ );
490
+ }
491
+ ```
492
+
493
+ **Session fields for display:**
494
+
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 |
510
+
511
+ ## Step 5: Handle Webhooks [Server-side]
512
+
513
+ XPay sends webhook events when payment state changes. Listen for these on your server — don't rely solely on the client-side result.
514
+
515
+ ```javascript
516
+ // Your server
517
+ app.post('/webhooks/xpay', (req, res) => {
518
+ const event = req.body;
519
+
520
+ switch (event.type) {
521
+ case 'checkout.session.completed':
522
+ // Payment succeeded — fulfill the order
523
+ // Send confirmation email, update database, start shipping
524
+ fulfillOrder(event.data);
525
+ break;
526
+ case 'checkout.session.expired':
527
+ // Session expired without payment
528
+ break;
529
+ }
530
+
531
+ res.json({ received: true });
532
+ });
533
+ ```
534
+
535
+ The webhook is the **source of truth** for order fulfillment. The client-side `confirm()` result is for UX only (showing success/error to the customer). A customer could close their browser after paying but before seeing the success page — the webhook still fires.
536
+
537
+ ---
538
+
539
+ ## API Reference
540
+
541
+ ### `<XPayProvider>`
542
+
543
+ Wraps your checkout UI. Provides XPay context to all child components.
544
+
545
+ ```tsx
546
+ <XPayProvider
547
+ xpay={xpayPromise}
548
+ options={{ clientSecret, appearance, locale }}
549
+ >
550
+ {children}
551
+ </XPayProvider>
552
+ ```
553
+
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. |
561
+
562
+ ### `useCheckout()`
563
+
564
+ The primary hook. Returns a disjoint union — narrow the `type` before accessing data.
565
+
566
+ ```tsx
567
+ const checkoutState = useCheckout();
568
+ ```
569
+
570
+ **Return type: `UseCheckoutResult`**
571
+
572
+ ```typescript
573
+ type UseCheckoutResult =
574
+ | { type: "loading" }
575
+ | { type: "error"; error: { message: string } }
576
+ | { type: "success"; checkout: Checkout };
577
+ ```
578
+
579
+ After narrowing to `type: "success"`, the `checkout` object contains all session fields and action methods merged together.
580
+
581
+ **Session fields** (on `checkout`):
582
+
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 |
599
+
600
+ **Action methods** (on `checkout`):
601
+
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 |
614
+
615
+ ### `<PaymentElement>`
616
+
617
+ Renders the payment method selector and card form.
618
+
619
+ ```tsx
620
+ <PaymentElement
621
+ options={{ layout: 'accordion' }}
622
+ onChange={(e) => console.log(e.complete, e.value.type)}
623
+ />
624
+ ```
625
+
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 |
635
+
636
+ ### `<CardElement>`
637
+
638
+ Card form only (no payment method selector).
639
+
640
+ ```tsx
641
+ <CardElement onChange={(e) => console.log(e.complete, e.value.brand)} />
642
+ ```
643
+
644
+ ### `<CheckoutButton>`
645
+
646
+ Opens the drop-in checkout modal on click.
647
+
648
+ ```tsx
649
+ <CheckoutButton
650
+ clientSecret="cs_test_abc_secret_xyz"
651
+ checkoutOptions={{ onComplete: (r) => router.push('/success') }}
652
+ >
653
+ Pay Now
654
+ </CheckoutButton>
655
+ ```
656
+
657
+ ### `useXPay()` / `useElements()` / `useConfirmPayment()`
658
+
659
+ Lower-level hooks for advanced use cases.
660
+
661
+ ---
662
+
663
+ ## Integration Patterns
664
+
665
+ ### Drop-in Checkout (Simplest)
666
+
667
+ A button that opens the full checkout in a modal overlay. No form needed.
668
+
669
+ ```tsx
670
+ <XPayProvider xpay={xpayPromise}>
671
+ <CheckoutButton
672
+ clientSecret={clientSecret}
673
+ checkoutOptions={{
674
+ onComplete: (result) => window.location.href = `/orders/${orderId}`,
675
+ onClose: () => console.log("Closed"),
676
+ }}
677
+ >
678
+ Subscribe Now
679
+ </CheckoutButton>
680
+ </XPayProvider>
681
+ ```
682
+
683
+ ### Card Element with Custom Payment Method Selector
684
+
685
+ Build your own selector. Mount card form only when "Card" is selected.
686
+
687
+ ```tsx
688
+ function CustomCheckout() {
689
+ const checkoutState = useCheckout();
690
+ const [method, setMethod] = useState("card");
691
+
692
+ if (checkoutState.type !== "success") return <div>Loading...</div>;
693
+
694
+ const { checkout } = checkoutState;
695
+
696
+ return (
697
+ <div>
698
+ <div className="flex gap-2">
699
+ {checkout.paymentMethods.map((pm) => (
700
+ <button
701
+ key={pm.type}
702
+ onClick={() => setMethod(pm.type)}
703
+ className={method === pm.type ? "border-blue-500 border-2" : "border-gray-200 border"}
704
+ >
705
+ {pm.displayName}
706
+ </button>
707
+ ))}
708
+ </div>
709
+
710
+ {method === "card" && <CardElement />}
711
+ {method !== "card" && <p>You will complete payment in a secure form.</p>}
712
+
713
+ <button onClick={() => checkout.confirm({ paymentMethod: method })}>
714
+ {method === "card" ? "Pay" : `Continue with ${method}`}
715
+ </button>
716
+ </div>
717
+ );
718
+ }
719
+ ```
720
+
721
+ ### Dark Mode Toggle
722
+
723
+ Sync XPay appearance with your site's theme at runtime using `changeAppearance()`.
724
+
725
+ ```tsx
726
+ function ThemeAwareCheckout() {
727
+ const checkoutState = useCheckout();
728
+ const { theme } = useTheme(); // your app's theme hook
729
+
730
+ useEffect(() => {
731
+ if (checkoutState.type === "success") {
732
+ checkoutState.checkout.changeAppearance({ colorMode: theme as "light" | "dark" });
733
+ }
734
+ }, [theme, checkoutState]);
735
+
736
+ return <PaymentElement />;
737
+ }
738
+ ```
739
+
740
+ ### Redirect Behavior
741
+
742
+ By default, `confirm()` returns the result to your code (`redirect: "if_required"`). Use `redirect: "always"` to redirect to the server's `afterCompletion.redirect.url` after success.
743
+
744
+ ```tsx
745
+ // Default: returns result to your code (no redirect)
746
+ const result = await checkout.confirm({ customerDetails: { email, name } });
747
+ if (result.type === "success") {
748
+ router.push("/thank-you");
749
+ }
750
+
751
+ // Redirect after success instead:
752
+ await checkout.confirm({
753
+ customerDetails: { email, name },
754
+ redirect: "always",
755
+ });
756
+ // ^ If successful, the page navigates away. Code below only runs on error.
757
+
758
+ // Override the redirect URL from the client:
759
+ await checkout.confirm({
760
+ customerDetails: { email, name },
761
+ redirect: "always",
762
+ returnUrl: "https://mysite.com/custom-success",
763
+ });
764
+ ```
765
+
766
+ | `redirect` | Behavior |
767
+ |---|---|
768
+ | Not set (default) | `"if_required"` — returns result to your code |
769
+ | `"always"` | Redirects to `returnUrl` (client) → server's `afterCompletion.redirect.url` |
770
+ | `"if_required"` | Returns result to your code — no redirect |
771
+
772
+ ---
773
+
774
+ ## Appearance
775
+
776
+ Override the session's `brandingSettings` at runtime. Uses the same shape.
777
+
778
+ ```tsx
779
+ <XPayProvider
780
+ xpay={xpayPromise}
781
+ options={{
782
+ clientSecret,
783
+ appearance: {
784
+ colorMode: "dark",
785
+ borderStyle: "rounded",
786
+ inputStyle: "outlined",
787
+ inputSize: "large",
788
+ spacing: "normal",
789
+ formLayout: "compact",
790
+ colors: { primary: "#0066FF", background: "#1a1a1a" },
791
+ fontFamily: "Inter, sans-serif",
792
+ },
793
+ }}
794
+ >
795
+ ```
796
+
797
+ ---
798
+
799
+ ## TypeScript
800
+
801
+ All components and hooks are fully typed. `@xpayeg/react` re-exports key SDK types for convenience:
802
+
803
+ ```tsx
804
+ import type {
805
+ Checkout,
806
+ CheckoutSession,
807
+ CheckoutActions,
808
+ UseCheckoutResult,
809
+ } from "@xpayeg/react";
810
+
811
+ // Or import additional types from @xpayeg/sdk directly:
812
+ import type {
813
+ ActionResult,
814
+ XPayError,
815
+ PaymentMethodInfo,
816
+ CustomerDetails,
817
+ Appearance,
818
+ SessionStatus,
819
+ CheckoutLineItem,
820
+ CheckoutTotalDetails,
821
+ CheckoutFees,
822
+ CheckoutDiscount,
823
+ } from "@xpayeg/sdk";
824
+ ```