@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/dist/index.cjs ADDED
@@ -0,0 +1,576 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let react = require("react");
3
+ let react_jsx_runtime = require("react/jsx-runtime");
4
+ //#region src/context.tsx
5
+ const XPayContext = (0, react.createContext)(null);
6
+ const ElementsContext = (0, react.createContext)(null);
7
+ /** Build a minimal XPayError for SDK-level failures (elements not initialized, etc.) */
8
+ function notInitializedError() {
9
+ return {
10
+ type: "api_error",
11
+ code: null,
12
+ message: "Elements not initialized",
13
+ param: null,
14
+ docUrl: null,
15
+ declineCode: null,
16
+ adviceCode: null,
17
+ chargeId: null,
18
+ paymentMethodId: null,
19
+ paymentMethodType: null,
20
+ paymentMethod: null
21
+ };
22
+ }
23
+ const CheckoutContext = (0, react.createContext)(null);
24
+ /**
25
+ * Provides XPay context to all child components.
26
+ *
27
+ * Accepts either a resolved XPay instance or a Promise (from `loadXPay()`).
28
+ * When `options` (with `clientSecret`) is provided, creates an Elements instance
29
+ * and fetches session data. Use `useCheckout()` in child components to access
30
+ * session data and action methods.
31
+ *
32
+ * @example
33
+ * ```tsx
34
+ * import { loadXPay } from "@xpayeg/sdk";
35
+ * import { XPayProvider, useCheckout, PaymentElement } from "@xpayeg/react";
36
+ *
37
+ * const xpayPromise = loadXPay("pk_test_xxx");
38
+ *
39
+ * function App() {
40
+ * return (
41
+ * <XPayProvider xpay={xpayPromise} options={{ clientSecret }}>
42
+ * <CheckoutForm />
43
+ * </XPayProvider>
44
+ * );
45
+ * }
46
+ * ```
47
+ */
48
+ function XPayProvider({ xpay: xpayProp, options, children }) {
49
+ const [resolvedXPay, setResolvedXPay] = (0, react.useState)(xpayProp && !(xpayProp instanceof Promise) ? xpayProp : null);
50
+ const [xpayError, setXpayError] = (0, react.useState)(null);
51
+ (0, react.useEffect)(() => {
52
+ if (!xpayProp) return;
53
+ if (!(xpayProp instanceof Promise)) {
54
+ setResolvedXPay(xpayProp);
55
+ return;
56
+ }
57
+ let cancelled = false;
58
+ xpayProp.then((instance) => {
59
+ if (!cancelled) setResolvedXPay(instance);
60
+ }).catch((err) => {
61
+ if (!cancelled) setXpayError(err instanceof Error ? err.message : "Failed to load XPay");
62
+ });
63
+ return () => {
64
+ cancelled = true;
65
+ };
66
+ }, [xpayProp]);
67
+ const elements = (0, react.useMemo)(() => {
68
+ if (!resolvedXPay || !options?.clientSecret) return null;
69
+ return resolvedXPay.elements(options);
70
+ }, [resolvedXPay, options?.clientSecret]);
71
+ const [session, setSession] = (0, react.useState)(null);
72
+ const [sessionError, setSessionError] = (0, react.useState)(null);
73
+ (0, react.useEffect)(() => {
74
+ if (!elements) return;
75
+ const onReady = (data) => {
76
+ setSession(data.session);
77
+ };
78
+ const onChange = (data) => {
79
+ const newSession = data;
80
+ if (newSession) setSession(newSession);
81
+ };
82
+ const onError = (data) => {
83
+ const d = data;
84
+ setSessionError(d.message || d.error || "Failed to load session");
85
+ };
86
+ elements.on("ready", onReady);
87
+ elements.on("change", onChange);
88
+ elements.on("loaderror", onError);
89
+ }, [elements]);
90
+ const checkoutState = (0, react.useMemo)(() => {
91
+ const errorMsg = xpayError || sessionError;
92
+ if (errorMsg) return {
93
+ type: "error",
94
+ error: { message: errorMsg }
95
+ };
96
+ if (!resolvedXPay || !elements || !session) return { type: "loading" };
97
+ return {
98
+ type: "success",
99
+ session,
100
+ elements,
101
+ xpay: resolvedXPay
102
+ };
103
+ }, [
104
+ resolvedXPay,
105
+ elements,
106
+ session,
107
+ xpayError,
108
+ sessionError
109
+ ]);
110
+ const xpayValue = (0, react.useMemo)(() => resolvedXPay, [resolvedXPay]);
111
+ const elementsValue = (0, react.useMemo)(() => elements, [elements]);
112
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(XPayContext.Provider, {
113
+ value: xpayValue,
114
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ElementsContext.Provider, {
115
+ value: elementsValue,
116
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckoutContext.Provider, {
117
+ value: checkoutState,
118
+ children
119
+ })
120
+ })
121
+ });
122
+ }
123
+ /**
124
+ * Access the checkout state — a tagged union of `loading`, `error`, or `success`.
125
+ *
126
+ * On success, returns a `checkout` object (type `Checkout`) that merges
127
+ * session data with action methods (confirm, promo codes, quantities, etc.).
128
+ *
129
+ * @returns `UseCheckoutResult` — narrow the type by checking `result.type`
130
+ * @throws Error if used outside of `<XPayProvider>`
131
+ *
132
+ * @example
133
+ * ```tsx
134
+ * function CheckoutForm() {
135
+ * const state = useCheckout();
136
+ *
137
+ * if (state.type === "loading") return <Skeleton />;
138
+ * if (state.type === "error") return <p>{state.error.message}</p>;
139
+ *
140
+ * const { checkout } = state;
141
+ * return <button onClick={() => checkout.confirm()}>
142
+ * Pay {checkout.currency} {checkout.amountTotal}
143
+ * </button>;
144
+ * }
145
+ * ```
146
+ */
147
+ function useCheckout() {
148
+ const ctx = (0, react.useContext)(CheckoutContext);
149
+ if (!ctx) throw new Error("Could not find XPay context. Wrap the part of your app that calls useCheckout() in an <XPayProvider> provider.");
150
+ const { confirmFn, applyPromoFn, removePromoFn, updateLineItemQtyFn, submitFn, fetchUpdatesFn, changeAppearanceFn, onChangeFn, getElementsFn } = useCheckoutActions(ctx);
151
+ return (0, react.useMemo)(() => {
152
+ if (ctx.type === "loading") return { type: "loading" };
153
+ if (ctx.type === "error") return {
154
+ type: "error",
155
+ error: ctx.error
156
+ };
157
+ return {
158
+ type: "success",
159
+ checkout: {
160
+ ...ctx.session,
161
+ confirm: confirmFn,
162
+ applyPromotionCode: applyPromoFn,
163
+ removePromotionCode: removePromoFn,
164
+ updateLineItemQuantity: updateLineItemQtyFn,
165
+ submit: submitFn,
166
+ fetchUpdates: fetchUpdatesFn,
167
+ changeAppearance: changeAppearanceFn,
168
+ on: onChangeFn,
169
+ getElements: getElementsFn
170
+ }
171
+ };
172
+ }, [
173
+ ctx,
174
+ confirmFn,
175
+ applyPromoFn,
176
+ removePromoFn,
177
+ updateLineItemQtyFn,
178
+ submitFn,
179
+ fetchUpdatesFn,
180
+ changeAppearanceFn,
181
+ onChangeFn,
182
+ getElementsFn
183
+ ]);
184
+ }
185
+ /** Build stable action callbacks from internal state */
186
+ function useCheckoutActions(ctx) {
187
+ const elements = ctx.type === "success" ? ctx.elements : null;
188
+ const xpay = ctx.type === "success" ? ctx.xpay : null;
189
+ return {
190
+ confirmFn: (0, react.useCallback)(async (opts) => {
191
+ if (!xpay || !elements) return {
192
+ type: "error",
193
+ error: {
194
+ type: "invalid_request_error",
195
+ code: null,
196
+ declineCode: null,
197
+ adviceCode: null,
198
+ message: "XPay not initialized",
199
+ docUrl: null,
200
+ chargeId: null,
201
+ paymentMethodId: null,
202
+ paymentMethodType: null,
203
+ paymentMethod: null
204
+ }
205
+ };
206
+ return xpay.confirmPayment({
207
+ ...opts,
208
+ elements
209
+ });
210
+ }, [xpay, elements]),
211
+ applyPromoFn: (0, react.useCallback)(async (code) => {
212
+ if (!elements) return {
213
+ type: "error",
214
+ error: notInitializedError()
215
+ };
216
+ return elements.applyPromotionCode(code);
217
+ }, [elements]),
218
+ removePromoFn: (0, react.useCallback)(async () => {
219
+ if (!elements) return {
220
+ type: "error",
221
+ error: notInitializedError()
222
+ };
223
+ return elements.removePromotionCode();
224
+ }, [elements]),
225
+ updateLineItemQtyFn: (0, react.useCallback)(async (args) => {
226
+ if (!elements) return {
227
+ type: "error",
228
+ error: notInitializedError()
229
+ };
230
+ return elements.updateLineItemQuantity(args);
231
+ }, [elements]),
232
+ submitFn: (0, react.useCallback)(async () => {
233
+ if (!elements) return { error: notInitializedError() };
234
+ return elements.submit();
235
+ }, [elements]),
236
+ fetchUpdatesFn: (0, react.useCallback)(async () => {
237
+ if (!elements) return {
238
+ type: "error",
239
+ error: notInitializedError()
240
+ };
241
+ return elements.fetchUpdates();
242
+ }, [elements]),
243
+ changeAppearanceFn: (0, react.useCallback)((appearance) => {
244
+ elements?.changeAppearance(appearance);
245
+ }, [elements]),
246
+ onChangeFn: (0, react.useCallback)(((event, handler) => {
247
+ elements?.on(event, handler);
248
+ }), [elements]),
249
+ getElementsFn: (0, react.useCallback)(() => {
250
+ if (!elements) throw new Error("Elements not initialized");
251
+ return elements;
252
+ }, [elements])
253
+ };
254
+ }
255
+ /**
256
+ * Access the raw XPay SDK instance. Returns `null` while the SDK is loading.
257
+ *
258
+ * Most merchants should use `useCheckout()` instead. Use `useXPay()` only when
259
+ * you need direct access to `xpay.confirmPayment()` or `xpay.checkout()`.
260
+ */
261
+ function useXPay() {
262
+ return (0, react.useContext)(XPayContext);
263
+ }
264
+ /**
265
+ * Access the Elements instance. Returns `null` while the session is loading.
266
+ *
267
+ * Use this for direct element management. In most cases, use `useCheckout()` instead
268
+ * which provides both session data and action methods.
269
+ */
270
+ function useElements() {
271
+ return (0, react.useContext)(ElementsContext);
272
+ }
273
+ //#endregion
274
+ //#region src/hooks.ts
275
+ /**
276
+ * Convenience hook for payment confirmation.
277
+ *
278
+ * @example
279
+ * ```tsx
280
+ * const { confirmPayment, isConfirming } = useConfirmPayment();
281
+ * ```
282
+ */
283
+ function useConfirmPayment() {
284
+ const result = useCheckout();
285
+ if (result.type !== "success") return {
286
+ confirmPayment: async () => ({
287
+ type: "error",
288
+ error: {
289
+ message: "Checkout not ready",
290
+ code: null
291
+ }
292
+ }),
293
+ isConfirming: false
294
+ };
295
+ return {
296
+ confirmPayment: result.checkout.confirm,
297
+ isConfirming: false
298
+ };
299
+ }
300
+ //#endregion
301
+ //#region src/utils/useAttachEvent.ts
302
+ /**
303
+ * Attach an event listener to an element without causing listener churn on re-renders.
304
+ *
305
+ * Stores the callback in a ref so the actual listener remains stable.
306
+ * Inspired by Stripe's useAttachEvent pattern.
307
+ */
308
+ function useAttachEvent(element, event, cb) {
309
+ const cbDefined = !!cb;
310
+ const cbRef = (0, react.useRef)(cb);
311
+ (0, react.useEffect)(() => {
312
+ cbRef.current = cb;
313
+ }, [cb]);
314
+ (0, react.useEffect)(() => {
315
+ if (!cbDefined || !element) return;
316
+ const decoratedCb = (...args) => {
317
+ if (cbRef.current) cbRef.current(...args);
318
+ };
319
+ element.on(event, decoratedCb);
320
+ return () => {
321
+ element.off(event, decoratedCb);
322
+ };
323
+ }, [
324
+ cbDefined,
325
+ event,
326
+ element
327
+ ]);
328
+ }
329
+ //#endregion
330
+ //#region src/utils/usePrevious.ts
331
+ /**
332
+ * Returns the previous value of a variable.
333
+ * Useful for detecting prop changes between renders.
334
+ */
335
+ function usePrevious(value) {
336
+ const ref = (0, react.useRef)(void 0);
337
+ (0, react.useEffect)(() => {
338
+ ref.current = value;
339
+ }, [value]);
340
+ return ref.current;
341
+ }
342
+ //#endregion
343
+ //#region src/utils/extractAllowedOptionsUpdates.ts
344
+ /**
345
+ * Extract only the changed, mutable options from a new options object.
346
+ *
347
+ * Returns null if nothing changed. Warns if immutable keys are modified.
348
+ * Inspired by Stripe's extractAllowedOptionsUpdates pattern.
349
+ */
350
+ function extractAllowedOptionsUpdates(options, prevOptions, immutableKeys) {
351
+ if (!options) return null;
352
+ let updates = null;
353
+ for (const key of Object.keys(options)) {
354
+ const isUpdated = !prevOptions || !isEqual(options[key], prevOptions[key]);
355
+ if (immutableKeys.includes(key)) {
356
+ if (isUpdated && prevOptions) console.warn(`Unsupported prop change: options.${key} is not a mutable property.`);
357
+ continue;
358
+ }
359
+ if (!isUpdated) continue;
360
+ updates = {
361
+ ...updates || {},
362
+ [key]: options[key]
363
+ };
364
+ }
365
+ return updates;
366
+ }
367
+ function isEqual(a, b) {
368
+ if (a === b) return true;
369
+ if (a == null || b == null) return false;
370
+ if (typeof a !== typeof b) return false;
371
+ if (Array.isArray(a) && Array.isArray(b)) {
372
+ if (a.length !== b.length) return false;
373
+ return a.every((v, i) => isEqual(v, b[i]));
374
+ }
375
+ if (typeof a === "object" && typeof b === "object") {
376
+ const aObj = a;
377
+ const bObj = b;
378
+ const aKeys = Object.keys(aObj);
379
+ const bKeys = Object.keys(bObj);
380
+ if (aKeys.length !== bKeys.length) return false;
381
+ return aKeys.every((key) => isEqual(aObj[key], bObj[key]));
382
+ }
383
+ return false;
384
+ }
385
+ //#endregion
386
+ //#region src/utils/isServer.ts
387
+ /** True when running in a server environment (SSR/RSC) */
388
+ const isServer = typeof window === "undefined";
389
+ //#endregion
390
+ //#region src/PaymentElement.tsx
391
+ const IMMUTABLE_OPTS$1 = [];
392
+ const PaymentElementClient = ({ options, onReady, onChange, onLoaderStart, onLoadError, className, id }) => {
393
+ const elements = useElements();
394
+ const containerRef = (0, react.useRef)(null);
395
+ const elementRef = (0, react.useRef)(null);
396
+ const [element, setElement] = (0, react.useState)(null);
397
+ const prevOptions = usePrevious(options);
398
+ (0, react.useLayoutEffect)(() => {
399
+ if (elementRef.current !== null || !elements || !containerRef.current) return;
400
+ const el = elements.create("payment", options);
401
+ elementRef.current = el;
402
+ setElement(el);
403
+ el.mount(containerRef.current);
404
+ }, [elements]);
405
+ (0, react.useLayoutEffect)(() => {
406
+ return () => {
407
+ if (elementRef.current) {
408
+ try {
409
+ elementRef.current.destroy();
410
+ } catch {}
411
+ elementRef.current = null;
412
+ }
413
+ setElement(null);
414
+ };
415
+ }, []);
416
+ (0, react.useEffect)(() => {
417
+ if (!element || !options) return;
418
+ const updates = extractAllowedOptionsUpdates(options, prevOptions, IMMUTABLE_OPTS$1);
419
+ if (updates && "update" in element) element.update(updates);
420
+ }, [
421
+ options,
422
+ prevOptions,
423
+ element
424
+ ]);
425
+ useAttachEvent(element, "ready", onReady);
426
+ useAttachEvent(element, "change", onChange);
427
+ useAttachEvent(element, "loaderstart", onLoaderStart);
428
+ useAttachEvent(element, "loaderror", onLoadError);
429
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
430
+ ref: containerRef,
431
+ className,
432
+ id
433
+ });
434
+ };
435
+ /** SSR placeholder — renders an empty div for hydration */
436
+ const PaymentElementServer = ({ className, id }) => {
437
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
438
+ className,
439
+ id
440
+ });
441
+ };
442
+ /**
443
+ * Renders the XPay Payment Element — a full payment method selector with card form.
444
+ *
445
+ * Must be used inside `<XPayProvider>` with an `options` prop containing `clientSecret`.
446
+ * Handles mount/unmount lifecycle, StrictMode, and SSR automatically.
447
+ *
448
+ * @example
449
+ * ```tsx
450
+ * <XPayProvider xpay={xpayPromise} options={{ clientSecret }}>
451
+ * <PaymentElement
452
+ * onChange={(e) => setPaymentReady(e.complete)}
453
+ * onLoadError={(e) => console.error(e.message)}
454
+ * />
455
+ * </XPayProvider>
456
+ * ```
457
+ */
458
+ const PaymentElement = isServer ? PaymentElementServer : PaymentElementClient;
459
+ //#endregion
460
+ //#region src/CardElement.tsx
461
+ const IMMUTABLE_OPTS = [];
462
+ const CardElementClient = ({ options, onReady, onChange, onLoaderStart, onLoadError, className, id }) => {
463
+ const elements = useElements();
464
+ const containerRef = (0, react.useRef)(null);
465
+ const elementRef = (0, react.useRef)(null);
466
+ const [element, setElement] = (0, react.useState)(null);
467
+ const prevOptions = usePrevious(options);
468
+ (0, react.useLayoutEffect)(() => {
469
+ if (elementRef.current !== null || !elements || !containerRef.current) return;
470
+ const el = elements.create("card", options);
471
+ elementRef.current = el;
472
+ setElement(el);
473
+ el.mount(containerRef.current);
474
+ }, [elements]);
475
+ (0, react.useLayoutEffect)(() => {
476
+ return () => {
477
+ if (elementRef.current) {
478
+ try {
479
+ elementRef.current.destroy();
480
+ } catch {}
481
+ elementRef.current = null;
482
+ }
483
+ setElement(null);
484
+ };
485
+ }, []);
486
+ (0, react.useEffect)(() => {
487
+ if (!element || !options) return;
488
+ const updates = extractAllowedOptionsUpdates(options, prevOptions, IMMUTABLE_OPTS);
489
+ if (updates && "update" in element) element.update(updates);
490
+ }, [
491
+ options,
492
+ prevOptions,
493
+ element
494
+ ]);
495
+ useAttachEvent(element, "ready", onReady);
496
+ useAttachEvent(element, "change", onChange);
497
+ useAttachEvent(element, "loaderstart", onLoaderStart);
498
+ useAttachEvent(element, "loaderror", onLoadError);
499
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
500
+ ref: containerRef,
501
+ className,
502
+ id
503
+ });
504
+ };
505
+ const CardElementServer = ({ className, id }) => {
506
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
507
+ className,
508
+ id
509
+ });
510
+ };
511
+ /**
512
+ * Renders the XPay Card Element — a card-only form (number, expiry, CVV).
513
+ *
514
+ * Use this when you handle payment method selection yourself.
515
+ * Must be used inside `<XPayProvider>` with an `options` prop containing `clientSecret`.
516
+ *
517
+ * @example
518
+ * ```tsx
519
+ * <CardElement
520
+ * onChange={(e) => setCardReady(e.complete)}
521
+ * />
522
+ * ```
523
+ */
524
+ const CardElement = isServer ? CardElementServer : CardElementClient;
525
+ //#endregion
526
+ //#region src/CheckoutButton.tsx
527
+ /**
528
+ * Button that opens the drop-in checkout modal on click.
529
+ *
530
+ * Must be inside XPayProvider.
531
+ *
532
+ * @example
533
+ * ```tsx
534
+ * <XPayProvider xpay={xpay}>
535
+ * <CheckoutButton
536
+ * clientSecret="cs_test_abc_secret_xyz"
537
+ * checkoutOptions={{
538
+ * onComplete: (result) => router.push('/success'),
539
+ * onClose: () => console.log('Closed'),
540
+ * }}
541
+ * >
542
+ * Pay Now
543
+ * </CheckoutButton>
544
+ * </XPayProvider>
545
+ * ```
546
+ */
547
+ const CheckoutButton = ({ clientSecret, children = "Pay", checkoutOptions, className, disabled }) => {
548
+ const xpay = useXPay();
549
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
550
+ type: "button",
551
+ onClick: (0, react.useCallback)(() => {
552
+ if (!xpay) return;
553
+ xpay.checkout({
554
+ clientSecret,
555
+ mode: "modal",
556
+ ...checkoutOptions
557
+ }).open();
558
+ }, [
559
+ xpay,
560
+ clientSecret,
561
+ checkoutOptions
562
+ ]),
563
+ disabled: disabled || !xpay,
564
+ className,
565
+ children
566
+ });
567
+ };
568
+ //#endregion
569
+ exports.CardElement = CardElement;
570
+ exports.CheckoutButton = CheckoutButton;
571
+ exports.PaymentElement = PaymentElement;
572
+ exports.XPayProvider = XPayProvider;
573
+ exports.useCheckout = useCheckout;
574
+ exports.useConfirmPayment = useConfirmPayment;
575
+ exports.useElements = useElements;
576
+ exports.useXPay = useXPay;