التوثيق

@xpayeg/react

الـ React SDK. XPayProvider، والـ hooks، والمكوّنات المبنية فوق @xpayeg/sdk.

الـ React SDK بتاع XPay غلاف رفيع حوالين @xpayeg/sdk بيكشف provider، وhooks، ومكوّن للـ payment element. الـ provider بينشئ نسخة Elements من clientSecret، والـ hooks بيكشفوا حالة الجلسة الحيّة ودوال الإجراءات، والمكوّنات بتركّب واجهة الدفع.

pnpm add @xpayeg/sdk @xpayeg/react
# Both packages are required. @xpayeg/react peer-depends on @xpayeg/sdk, react ^18 || ^19, react-dom ^18 || ^19.

الصفحة دي مرجع لكل تصدير. للشروحات، شوف Drop-in وElements تحت Integrate.

شوفه شغّال: متجر مثال Next.js

متجر شغّال مبني بالـ SDK ده: الدفع المستضاف، وdrop-in، وinline، وElements، زائد مستقبِل webhooks متحقّق من التوقيع. اعمل clone، وحطّ مفاتيح الاختبار، وشغّل.

البداية السريعة

النمط المنصوح بيه هو loadXPay على مستوى الموديول (أو في ملف محمّل صغيّر)، وبعدين تنشئ جلسة دفع على السيرفر بتاعك، وتمسك الـ clientSecret اللي رجع في الحالة، وتعرض <XPayProvider> بمجرد ما يبقى عندك. وبعدها useCheckout() بيقرا الجلسة الحيّة جوّه أي مكوّن ابن.

lib/xpay.ts
"use client";

import { loadXPay } from "@xpayeg/sdk";

// Module-level: load once, share across all components.
export const xpayPromise = loadXPay(process.env.NEXT_PUBLIC_XPAY_PUBLISHABLE_KEY!);
app/checkout/page.tsx
"use client";

import { useState } from "react";
import { xpayPromise } from "@/lib/xpay";
import { PaymentElement, useCheckout, XPayProvider } from "@xpayeg/react";

export default function CheckoutPage() {
  const [clientSecret, setClientSecret] = useState<string | null>(null);
  const [creating, setCreating] = useState(false);

  const startCheckout = async () => {
    setCreating(true);
    const res = await fetch("/api/create-checkout", { method: "POST" });
    const { clientSecret } = await res.json();
    setClientSecret(clientSecret);
    setCreating(false);
  };

  if (!clientSecret) {
    return (
      <button onClick={startCheckout} disabled={creating}>
        {creating ? "Starting…" : "Pay now"}
      </button>
    );
  }

  return (
    <XPayProvider xpay={xpayPromise} options={{ clientSecret }}>
      <CheckoutForm />
    </XPayProvider>
  );
}

function CheckoutForm() {
  const state = useCheckout();

  if (state.type === "loading") return <p>Loading…</p>;
  if (state.type === "error") return <p>{state.error.message}</p>;

  const { checkout } = state;
  if (checkout.status.type === "expired") return <p>This checkout has expired.</p>;
  if (checkout.status.type === "complete") return <p>Payment complete. Thank you.</p>;

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    const result = await checkout.confirm({
      customerDetails: { email: "customer@example.com" },
    });
    if (result.type === "error") {
      alert(result.error.message);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <PaymentElement />
      <button type="submit" disabled={!checkout.canConfirm}>
        Pay {checkout.currency} {(checkout.amountTotal / 100).toFixed(2)}
      </button>
    </form>
  );
}

الـ clientSecret بيعيش في الحالة، فـ XPayProvider بيتركّب مرة واحدة بس و/api/create-checkout بيتنادى مرة واحدة بالظبط لكل عملية دفع. لو بتنشئ الجلسة بدري في مسارك (ضغطة على صفحة منتج، أو server component، أو route param)، مرّر النص اللي رجع بأي طريقة تحبها: props، أو search params، أو context. الـ provider محتاج النص بس.

useCheckout() بيتحدّث تفاعليًا بعد كل كود خصم، أو تغيير كمية، أو إعادة حساب رسوم. مفيش حاجة لـ onChange callback في React؛ المكوّن بيعيد العرض بآخر بيانات checkout من نفسه.

التصديرات بنظرة سريعة

التصديرالنوعيعني إيه
XPayProvidercomponentبيغلّف واجهة الدفع بتاعتك وبيوفّر السياق لكل المكوّنات الأبناء.
useCheckouthookبيرجّع حالة الـ disjoint union وكائن الجلسة-زائد-الإجراءات المدموج.
useXPayhookبيرجّع الـ XPayInstance الأساسي (أو null).
useElementshookبيرجّع نسخة الـ Elements الأساسية (أو null).
useConfirmPaymenthookhook راحة بيكشف confirmPayment من الـ checkout النشط.
PaymentElementcomponentبيركّب مختار وسيلة الدفع مع فورم البطاقة.
CheckoutButtoncomponentزرار جاهز بيفتح الـ drop-in checkout modal عند الضغط.
CheckouttypeCheckoutSession & CheckoutActions. الشكل المدموج اللي useCheckout بيرجّعه عند النجاح.
UseCheckoutResulttypeالـ disjoint union اللي بيرجّعه useCheckout.
PaymentElementProps, CheckoutButtonPropstypeأنواع props المكوّنات.

الـ package بيعيد تصدير كام نوع أساسي من @xpayeg/sdk للراحة: CheckoutSession، وCheckoutActions، وPaymentMethodInfo، وAppearance، وActionResult، وXPayError، وConfirmPaymentOptions، وPaymentElementChangeEvent، وElementsOptions. استورد الأنواع الإضافية مباشرة من @xpayeg/sdk لما تحتاجها.

<XPayProvider>

بيغلّف الجزء من تطبيقك اللي بيشغّل الدفع. بينشئ نسخة Elements من options.clientSecret وبيوفّر تلات سياقات React: الـ XPayInstance، والـ Elements، وحالة الـ checkout الواعية بحالة التحميل.

<XPayProvider xpay={xpayPromise} options={{ clientSecret, appearance, locale }}>
  {children}
</XPayProvider>
الـ Propالنوعالوصف
xpayXPayInstance | Promise<XPayInstance | null> | nullالنسخة من loadXPay(). مرّر الـ Promise؛ والـ provider بيستناه على العميل.
options.clientSecretstring | Promise<string>الـ client secret بتاع جلسة الدفع. مطلوب لإنشاء نسخة Elements وتحميل الجلسة.
options.appearanceAppearanceاختياري. تجاوزات العلامة التجارية المدموجة مع العلامة من ناحية السيرفر للجلسة.
options.locale"en" | "ar"اختياري. الافتراضي "en".
childrenReactNodeالشجرة الفرعية اللي بتستدعي useCheckout، وuseXPay، وuseElements، وuseConfirmPayment.

الـ provider بيتعامل مع الـ SSR بسلاسة: loadXPay بيرجّع null على السيرفر، والـ provider بيستنى العميل علشان يحمّل الـ SDK قبل ما ينشئ نسخة الـ Elements.

useCheckout()

الـ hook الأساسي. بيرجّع disjoint union؛ ضيّق حسب state.type قبل ما تقرا بيانات الجلسة أو تستدعي الإجراءات.

type UseCheckoutResult =
  | { type: "loading" }
  | { type: "error"; error: { message: string } }
  | { type: "success"; checkout: Checkout };

type Checkout = CheckoutSession & CheckoutActions;
const state = useCheckout();
if (state.type === "loading") return <Spinner />;
if (state.type === "error") return <p>{state.error.message}</p>;
const { checkout } = state;

بعد التضييق لـ success، الـ checkout بيحمل كل حقل جلسة زائد كل دالة إجراء.

حقول الجلسة

حقول بيانات الجلسة على الـ checkout جاية مباشرة من CheckoutSession. شوف مرجع @xpayeg/sdk للمخطط الكامل. أكتر الحقول استخدامًا:

الحقلالنوعالوصف
idstringمعرّف الجلسة.
amountTotalnumberالإجمالي بالوحدات الصغرى.
amountSubtotalnumberالمجموع الفرعي بالوحدات الصغرى.
currencystringكود العملة.
merchantNamestringاسم نشاطك التجاري.
livemodebooleanالحساب الفعلي مقابل وضع الاختبار.
statusSessionStatus{type:"open"} | {type:"expired"} | {type:"complete", paymentStatus}
canConfirmbooleanإذا كانت الجلسة جاهزة للتأكيد.
paymentMethodsPaymentMethodInfo[]الوسائل المتاحة على الجلسة دي.
lineItemsCheckoutLineItem[]البنود.
totalDetailsCheckoutTotalDetailsتفصيل المجموع الفرعي، والضريبة، والشحن، والخصم، والرسوم.
discountsCheckoutDiscount[]أكواد الخصم المطبَّقة.

دوال الإجراءات

دوال الإجراءات جاية من CheckoutActions. كلها بترجّع Promise<ActionResult> (tagged union من success أو error) إلا حيث ما يُذكر.

الدالةالوصف
confirm(options?: Omit<ConfirmPaymentOptions, "elements">)بتأكّد الدفعة. بتتولّى تحدّيات 3DS وتحويلات BNPL داخليًا.
applyPromotionCode(code: string)بتطبّق كود خصم؛ والجلسة بتعيد العرض بالإجمالي الجديد.
removePromotionCode()بتشيل الكود المطبَّق.
updateLineItemQuantity({ lineItem, quantity })بتحدّث كمية بند.
submit()بتتحقّق من الحقول. بترجّع { error?, selectedPaymentMethod? }.
fetchUpdates()بتعيد جلب الجلسة من السيرفر.
changeAppearance(appearance: Appearance)بتحدّث العلامة التجارية في وقت التشغيل؛ بترجّع void.
on(event, handler)تستنى "change"، أو "error"، أو حدث مخصّص. بترجّع void.
getElements()بترجّع نسخة الـ Elements الأساسية للوصول منخفض المستوى.

الـ hook بيستنى أحداث الـ change والـ loaderror الأساسية بنفسه، فلما أكواد الخصم بتتطبّق، أو البنود بتتحدّث، أو الرسوم بتتعاد حسابها، المكوّن بيعيد العرض بآخر بيانات من غير ما تربط effect.

أكواد الخصم والكميات (مثال)

function LineItems() {
  const state = useCheckout();
  if (state.type !== "success") return null;
  const { checkout } = state;

  return (
    <ul>
      {checkout.lineItems?.map((item) => (
        <li key={item.id}>
          <span>{item.description}</span>
          <button
            onClick={() =>
              checkout.updateLineItemQuantity({
                lineItem: item.id,
                quantity: item.quantity + 1,
              })
            }
          >
            +
          </button>
          <button
            onClick={() =>
              checkout.updateLineItemQuantity({
                lineItem: item.id,
                quantity: Math.max(1, item.quantity - 1),
              })
            }
          >
            -
          </button>
        </li>
      ))}
    </ul>
  );
}

function PromoInput() {
  const state = useCheckout();
  const [code, setCode] = useState("");
  const [error, setError] = useState("");

  if (state.type !== "success") return null;
  const { checkout } = state;

  const apply = async () => {
    setError("");
    const result = await checkout.applyPromotionCode(code);
    if (result.type === "error") setError(result.error.message);
  };

  return (
    <div>
      <input value={code} onChange={(e) => setCode(e.target.value)} />
      <button onClick={apply}>Apply</button>
      <button onClick={() => checkout.removePromotionCode()}>Remove</button>
      {error && <p style={{ color: "red" }}>{error}</p>}
    </div>
  );
}

استنى الأخطاء غير المطلوبة

أحداث الـ change بتشغّل حالة React تلقائيًا. حدث الـ error بيغطي الأخطاء اللي بتطلق بره أي إجراء تاجر (مثلًا جلسة انتهت أثناء إعادة حساب رسوم، أو فشل في اكتشاف الـ BIN):

useEffect(() => {
  if (state.type !== "success") return;
  state.checkout.on("error", (err) => {
    console.error("[XPay] unsolicited error:", err.code, err.message);
  });
}, [state]);

useXPay() وuseElements()

hooks منخفضة المستوى بترجّع النسخ الأساسية. مفيدة لما تحتاج تستدعي دوال مش مكشوفة من خلال useCheckout (مثلًا تركيب element إضافي برمجيًا).

function useXPay(): XPayInstance | null;
function useElements(): Elements | null;

الاتنين بيرجّعوا null أثناء تحميل الـ SDK أو قبل ما يتوفّر clientSecret. والاتنين لازم يتنادوا جوّه <XPayProvider>.

useConfirmPayment()

hook راحة حوالين useCheckout. بيرجّع نفس دالة الـ confirm زائد علَم isConfirming. مفيد لما عايز API صغيّر لمكوّن طرفي محتاج بس يقدّم:

const { confirmPayment, isConfirming } = useConfirmPayment();

لو الـ checkout لسه مش جاهز، confirmPayment() بيرجّع { type: "error", error: { message: "Checkout not ready" } } فورًا بدل ما يرمي خطأ.

<PaymentElement />

بيعرض مختار وسيلة الدفع وفورم البطاقة. لازم يكون جوّه <XPayProvider> بجلسة.

<PaymentElement
  onChange={(e) => setReady(e.complete)}
  onReady={() => console.log("element ready")}
/>
الـ Propالنوعالوصف
onReady() => voidاختياري. بيطلق لما iframe الـ element يتهيّأ.
onChange(event: PaymentElementChangeEvent) => voidاختياري. بيطلق عند اختيار وسيلة الدفع وتغيّر الحقول.
onLoaderStart() => voidاختياري. بيطلق بشكل متزامن لما الـ iframe يتعمل.
onLoadError(event: ElementsLoadErrorEvent) => voidاختياري. بيطلق لما الـ element يفشل في التحميل.
classNamestringاختياري. CSS class للحاوية <div>.
idstringاختياري. ID للحاوية <div>.

المكوّن بيعرض <div> واحد وبيركّب الـ PaymentElement الأساسي (من @xpayeg/sdk) فيه. عند التفكيك، الـ element بيتفكّك تلقائيًا.

<CheckoutButton />

زرار جاهز بيفتح الـ drop-in checkout modal عند الضغط. بيغلّف xpay.checkout({ mode: "modal" }) علشان ما تضطرش تربطه بنفسك.

<CheckoutButton
  clientSecret="cs_test_..."
  checkoutOptions={{
    onComplete: (result) => router.push(`/orders/${result.paymentIntentId}`),
    onClose: () => console.log("Customer closed checkout"),
  }}
>
  Pay Now
</CheckoutButton>
الـ Propالنوعالوصف
clientSecretstringمطلوب. الـ client secret بتاع جلسة الدفع.
childrenReactNodeلافتة الزرار. الافتراضي "Pay".
checkoutOptionsOmit<CheckoutOptions, "clientSecret" | "mode">اختياري. الـ callbacks (onComplete، وonClose، وonReady، وonConfirmed، وonError)، وappearance، وlocale.
classNamestringاختياري. CSS class للزرار.
disabledbooleanاختياري. بيعطّل الزرار.

الزرار بيتعطّل تلقائيًا كمان لما الـ SDK لسه مش متحمّل (useXPay() بيرجّع null). لازم يكون جوّه <XPayProvider>.

أنماط شائعة

مظهر واعٍ بالثيم

زامن وضع لون XPay مع ثيم تطبيقك باستخدام changeAppearance في وقت التشغيل:

function ThemeAwareCheckout() {
  const state = useCheckout();
  const { theme } = useTheme();

  useEffect(() => {
    if (state.type === "success") {
      state.checkout.changeAppearance({ colorMode: theme as "light" | "dark" });
    }
  }, [theme, state]);

  return <PaymentElement />;
}

تحقّق مسبق قبل التأكيد

submit() بيتحقّق من كل الحقول قبل confirm(). مفيد لما عايز تحكم خطوة التأكيد ورا خطوة واجهة تانية (نافذة تأكيد، أو خانة شروط، إلخ):

const handlePay = async () => {
  const { error, selectedPaymentMethod } = await checkout.submit();
  if (error) return setError(error.message);

  const ok = await showConfirmDialog(selectedPaymentMethod);
  if (!ok) return;

  const result = await checkout.confirm();
  if (result.type === "error") setError(result.error.message);
};

التحويل بعد النجاح

افتراضيًا confirm() بيرجّع النتيجة لكودك. مرّر redirect: "always" علشان توجّه العميل لـ afterCompletion.redirect.url بتاعك بعد النجاح:

await checkout.confirm({
  customerDetails: { email },
  redirect: "always",
});
// On success, the page navigates away. Any code below only runs on error.

تقدر تتجاوز الـ returnUrl من ناحية السيرفر من العميل بتمريره صراحةً: redirect: "always", returnUrl: "https://example.com/custom".

رايح فين بعد كده

في الصفحة دي