Drop-in
سيرفر زائد كام سطر واجهة. افتح دفع XPay كنافذة منبثقة أو iframe داخلي على نطاقك. العميل عمره ما بيغادر موقعك.
Drop-in بيفتح دفع XPay الكامل جوّه iframe على نطاقك. السيرفر بتاعك بينشئ جلسة دفع، والواجهة بتاعتك بتستدعي xpay.checkout({ ... })، والعميل بيدفع من غير ما ينتقل بعيد أبدًا. بتكتب حوالي عشر سطور كود واجهة؛ والـ iframe بيتولّى وسائل الدفع، وفورم البطاقة، والـ 3D Secure، والوسائل المحلية زي ڤاليو وفوري.
اختار النمط ده لما تكون عايز العميل يفضل على نطاقك بس مش عايز تبني فورم الدفع بنفسك. لو محتاج واجهة مخصّصة، استخدم Elements. لو مش عايز أي كود واجهة، استخدم الدفع المستضاف أو رابط دفع.
أمثلة الكود تحت بتغطي الاتنين Vanilla JavaScript (أي إطار، أو HTML عادي) وReact. الـ SDK الاتنين بيشتركوا في نفس الـ API؛ اختار التاب اللي يناسب الستاك بتاعك. أطر العمل غير React (Vue، Svelte، Solid، Angular، Lit) بتستخدم مسار الـ vanilla.
Drop-in عبارة عن غلاف SDK رفيع حوالين نفس جلسة الدفع اللي بيستخدمها كل نمط تاني. لمجموعة الحقول
الكاملة اللي تقدر تظبطها على POST /checkout/sessions (البنود، وتحصيل بيانات العميل، والعلامة
التجارية، ووسائل الدفع، والرسوم، والبيانات الوصفية، إلخ)، شوف مرجع جلسة
الدفع.
إزاي بيشتغل
- السيرفر بتاعك بيستدعي
POST /checkout/sessionsبـuiMode: "embedded"والبنود. XPay بيرجّع جلسة فيهاclientSecretوpaymentMethodTypes. - السيرفر بتاعك بيشحن الـ
clientSecretللواجهة (عادةً جوّه عرض صفحة HTML أو رد fetch). - الواجهة بتاعتك بتحمّل الـ SDK بمفتاحك القابل للنشر وبتستدعي
xpay.checkout({ clientSecret, mode: "modal" }). استدعاءcheckout.open()بيعرض الـ iframe. - العميل بيختار وسيلة، ويملا الفورم، ويكمّل الـ 3D Secure لو لازم، ويدفع. كل ده من غير ما ينتقل بعيد.
- الـ SDK بيطلق الـ
onCompletecallback بتاعك بمعرّف الـ Payment Intent. والسيرفر بتاعك بيستلمcheckout.session.completedبشكل مستقل علشان ينفّذ الطلب.
التفاعل كله بيحصل جوّه iframe بيشاور على https://checkout.xpay.app. بيانات البطاقة عمرها ما بتدخل الـ DOM بتاعك.
نفّذه
1. خُد مفاتيح API للاختبار
محتاج المفتاحين لـ Drop-in:
sk_test_*للسيرفر بتاعك. بيُستخدم في إنشاء جلسة الدفع.pk_test_*للواجهة بتاعتك. بيُستخدم في تحميل الـ SDK. آمن إنك تشحنه للمتصفّح.
في لوحة التحكم، روح المطورين ← مفاتيح API وانسخ الاتنين. المفاتيح الفعلية (sk_live_* / pk_live_*) بتشتغل بنفس الطريقة بمجرد ما حسابك يتعتمد.
2. أنشئ جلسة دفع على السيرفر بتاعك
شرطين خاصين بـ Drop-in:
uiMode: "embedded"بيقول لـ XPay إن الجلسة دي لـ iframe.- رد الجلسة فيه
clientSecretبيحصر الـ SDK على الجلسة دي بالظبط. هتشحنه للواجهة.
curl -X POST https://api.xpay.app/checkout/sessions \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{
"uiMode": "embedded",
"afterCompletion": {
"type": "redirect",
"redirect": { "url": "https://yourshop.example/order/{CHECKOUT_SESSION_ID}" }
},
"lineItems": [
{
"priceData": {
"currency": "EGP",
"unitAmount": 149900,
"productData": { "name": "Test product" }
},
"quantity": 1
}
]
}'const res = await fetch("https://api.xpay.app/checkout/sessions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.XPAY_SECRET_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
uiMode: "embedded",
afterCompletion: {
type: "redirect",
redirect: { url: "https://yourshop.example/order/{CHECKOUT_SESSION_ID}" },
},
lineItems: [
{
priceData: {
currency: "EGP",
unitAmount: 149900,
productData: { name: "Test product" },
},
quantity: 1,
},
],
}),
});
const session = await res.json();
// Send `session.clientSecret` to your frontendimport os, requests
res = requests.post(
"https://api.xpay.app/checkout/sessions",
headers={
"Authorization": f"Bearer {os.environ['XPAY_SECRET_KEY']}",
"Content-Type": "application/json",
},
json={
"uiMode": "embedded",
"afterCompletion": {
"type": "redirect",
"redirect": {"url": "https://yourshop.example/order/{CHECKOUT_SESSION_ID}"},
},
"lineItems": [
{
"priceData": {
"currency": "EGP",
"unitAmount": 149900,
"productData": {"name": "Test product"},
},
"quantity": 1,
}
],
},
timeout=10,
)
session = res.json()
# Send `session["clientSecret"]` to your frontendالرد فيه نفس شكل جلسة الدفع اللي بيرجّعه الدفع المستضاف. الحقول اللي بتشحنها للواجهة:
{
"id": "cs_test_AbC123...",
"clientSecret": "cs_test_AbC123..._secret_xyz789",
"amountTotal": 149900,
"currency": "EGP"
}كام قاعدة:
cancelUrlبيترفض معuiMode: "embedded". Drop-in بيطلّع حالات الفشل جوّه الـ iframe مع إعادة محاولة. مفيش مسار "وَدّي العميل لـ URL فشل".clientSecretلجلسة واحدة. بيحصر الـ SDK على الجلسة دي بالظبط ومينفعش يتعاد استخدامه. ما تخزّنوش بعد عمر الجلسة.afterCompletion.redirect.urlلسه ليه معنى. لما الدفعة تنجح، الـ SDK بيمرّر الـ URL ده لـonCompletecallback بتاعك كـredirectUrl. كودك هو اللي بيقرّر يتنقّل ولا لأ. لو استخدمتhosted_confirmation، الـredirectUrlبيبقىundefined: الـ iframe بيعرض صفحة النجاح لحظة وبيقفل النافذة المنبثقة تلقائيًا.
3. حمّل الـ SDK على الواجهة بتاعتك
لو بتستخدم bundler (Vite، أو webpack، إلخ)، ثبّت المحمّل من npm، وبعدين استورد loadXPay واعمله await:
npm install @xpayeg/sdkimport { loadXPay } from "@xpayeg/sdk";
const xpay = await loadXPay("pk_test_..."); // your publishable key
const checkout = xpay.checkout({
clientSecret: "cs_test_..._secret_xyz789", // from your server
mode: "modal",
onComplete: (result) => {
// result.paymentIntentId is the pi_*
// result.redirectUrl is your afterCompletion.redirect.url (if set)
if (result.redirectUrl) {
window.location.href = result.redirectUrl;
}
},
onClose: () => {
console.log("Customer closed the modal");
},
});
document.getElementById("pay-button")?.addEventListener("click", () => {
checkout.open();
});على HTML عادي من غير build step، ما تستوردش. حطّ الـ CDN script tag واستدعي الـ factory العام XPay اللي الـ runtime بيعرضه. نفس الـ API، من غير await:
<script src="https://checkout.xpay.app/v1/sdk.js"></script>
<script>
// الـ factory العام، مش loadXPay. `loadXPay` موجود بس في الـ package بتاع npm.
const xpay = XPay("pk_test_...");
const checkout = xpay.checkout({
clientSecret: "cs_test_..._secret_xyz789", // from your server
mode: "modal",
onComplete: (result) => {
if (result.redirectUrl) window.location.href = result.redirectUrl;
},
});
document.getElementById("pay-button")?.addEventListener("click", () => {
checkout.open();
});
</script>ثبّت الـ SDK packages الاتنين:
npm install @xpayeg/sdk @xpayeg/reactimport { CheckoutButton, XPayProvider } from "@xpayeg/react";
import { loadXPay } from "@xpayeg/sdk";
// Load once at module level (not inside a component)
const xpayPromise = loadXPay("pk_test_...");
export function PayPage({ clientSecret }: { clientSecret: string }) {
return (
<XPayProvider xpay={xpayPromise}>
<CheckoutButton
clientSecret={clientSecret}
checkoutOptions={{
onComplete: (result) => {
if (result.redirectUrl) window.location.href = result.redirectUrl;
},
}}
>
Pay now
</CheckoutButton>
</XPayProvider>
);
}<XPayProvider> بيقبل يا إما XPayInstance أو Promise<XPayInstance>، فتمرير xpayPromise مباشرة هو النمط الأصح. ما تستدعيش loadXPay() جوّه مكوّن. حمّله مرة واحدة لكل صفحة على مستوى الموديول.
الـ SDK بيتحمّل من https://checkout.xpay.app/v1/sdk.js مرة واحدة لكل صفحة؛ واستدعاءات loadXPay() اللي بعدها بترجّع نفس النسخة المخزّنة.
4. تعامل مع النتيجة
xpay.checkout(...) بيرجّع CheckoutInstance تقدر تشترك فيه عن طريق الـ callbacks في الـ constructor أو .on(event, handler). خمس أحداث بتطلق على مدى عمر عملية دفع واحدة.
| الحدث | إمتى | الحمولة |
|---|---|---|
ready | الجلسة اتحمّلت والـ iframe جاهز للعرض. | الـ Checkout SessionAPI الكاملة. |
confirmed | العميل ضغط ادفع. مفيد لو عايز تعطّل زرار الإغلاق أو تعرض حالة "بنعالج". | لا شيء. |
complete | الدفعة نجحت. الـ iframe بيقفل النافذة المنبثقة تلقائيًا بعد 300 مللي ثانية. | { status: "succeeded", paymentIntentId, chargeId?, redirectUrl? }. |
close | النافذة المنبثقة اتقفلت. بتطلق للإغلاق اليدوي، أو بعد complete، أو لو checkout.close() اتنادى. | لا شيء. |
error | الجلسة فشلت في التحميل أو الـ SDK واجه خطأ قاتل. ما بتطلقش للأخطاء اللي العميل ممكن يتعافى منها زي رفض البطاقة. | { message, code? }. |
حدث الـ complete مجاملة لتجربة المستخدم. لازم السيرفر بتاعك يفضل يستنى webhook checkout.session.completed علشان يعلّم الطلب مدفوع. الـ iframe ممكن يتقفل بدري، والشبكة ممكن تفصل، والـ onComplete handler بتاعك ممكن يرمي خطأ. الـ webhook هو مصدر الحقيقة.
في React، نفس الـ callbacks بتركب على الـ checkoutOptions prop بتاع <CheckoutButton />: onComplete، onClose، onReady، onConfirmed، onError. بتتطابق واحد لواحد مع الأحداث اللي فوق.
Modal مقابل inline
mode بيتحكم في إن الـ iframe يغطّي الصفحة ولا يقعد جوّه حاوية.
mode: modal
تغطية بملء الشاشة بـ iframe في النص. بتستدعي checkout.open() علشان تعرضه. اضغط بره، أو دوس
Escape، أو استدعي checkout.close() علشان تخفيه. الأنسب لزراير "ادفع دلوقتي"، وصفحات ما بعد
السلة، وأي حاجة مدفوعة بالأحداث.
mode: inline
iframe متركّب جوّه حاوية بتوفّرها أنت. مفيش فتح/قفل: بيبقى موجود بمجرد ما تنشئ النسخة. مرّر
container كـ CSS selector أو HTMLElement. الأنسب لصفحات "الدفع" المضمّنة اللي الفورم فيها هو
الصفحة.
// Inline mode
const checkout = xpay.checkout({
clientSecret,
mode: "inline",
container: "#checkout-container", // or document.getElementById(...)
onComplete: (result) => {
/* ... */
},
});
// On unmount or before re-creating, release the iframe
checkout.destroy();الـ iframe المضمّن بيعيد ضبط حجمه تلقائيًا على حسب ارتفاع محتواه. مش محتاج تظبط ارتفاع ثابت في الـ CSS بتاعك.
استدعي checkout.destroy() دايمًا لما تفكّك المكوّن اللي بيملك الدفع المضمّن. من غيرها، الـ iframe بيفضل في الـ DOM، واستدعاء xpay.checkout({ mode: "inline" }) جديد هيركّب واحد تاني. وضع الـ modal بيتولّى تنظيفه بنفسه عند الإغلاق، لكن لسه تقدر تستدعي destroy() علشان تحرّر المستمعين بدري.
<CheckoutButton /> بيشتغل modal بس. للنمط المضمّن في React، انزل للـ JS SDK عن طريق useXPay() واستدعي xpay.checkout({ mode: "inline" }) بنفسك. وافتكر تستدعي destroy() في useEffect للتنظيف.
تخصيص المظهر واللغة
appearance بيقبل نفس شكل brandingSettings بتاع الجلسة. قيم الـ SDK في وقت التشغيل بتكسب؛ وbrandingSettings بتاع الجلسة والقيم الافتراضية للتاجر بتوفّر البدائل الاحتياطية. لقائمة الحقول الكاملة شوف الإعدادات المتقدمة → العلامة التجارية.
const checkout = xpay.checkout({
clientSecret,
mode: "modal",
locale: "ar",
appearance: {
colorMode: "dark",
borderStyle: "rounded",
colors: { primary: "#635bff" },
},
});<CheckoutButton
clientSecret={clientSecret}
checkoutOptions={{
locale: "ar",
appearance: {
colorMode: "dark",
borderStyle: "rounded",
colors: { primary: "#635bff" },
},
onComplete: (result) => {
/* ... */
},
}}
>
Pay now
</CheckoutButton>مفيد لما نفس الجلسة محتاجة تبان مختلفة عبر سطوح مختلفة (مثلًا تطابق مفتاح الوضع الداكن في صفحتك من غير ما تحفظه على الجلسة).
أكّد بـ webhook
الـ onComplete callback بتاع Drop-in سريع ومريح، لكنه مش موثوق. جلسة الدفع اللي السيرفر بتاعك بينشئها هي نفس الكائن اللي بينتجه كل نمط تاني، فمعالِج webhook checkout.session.completed اللي كنت هتكتبه للدفع المستضاف بيشتغل من غير تغيير مع Drop-in.
الحد الأدنى اللي معالِجك محتاج يعمله:
- تحقّق من ترويسة
XPay-Signature. - فُكّ محتوى الـ JSON.
- لو نوع الحدث
checkout.session.completedوحالة الجلسةstatusهيcomplete، نفّذ الطلب.
لشرح المعالِج الكامل (طريقة التحقق من التوقيع، وجدول حقول الحمولة، وسلوك إعادة المحاولة)، شوف الدفع المستضاف → أكّد بـ webhook. نفس الكود بينطبق هنا.
اختبره
في وضع الاختبار، استخدم بطاقة النجاح 5123 4500 0000 0008 بتاريخ انتهاء 01/39 علشان تشغّل المسار الناجح جوّه الـ iframe. فورم البطاقة، وتحدّي الـ 3D Secure، وحالة النجاح بعد الدفع، كلهم بيشتغلوا جوّه الـ iframe؛ مش محتاج تبدّل تابات ولا صفحات. لقائمة بطاقات الاختبار الكاملة ومصفوفة تاريخ الانتهاء للنتيجة، شوف وضع الاختبار وبطاقات الاختبار.
علشان تجرّب معالِج الـ webhook بتاعك محليًا قبل النشر، شوف تطوير الـ webhook محليًا.
قائمة التحقق قبل الإنتاج
قبل ما تحوّل للحساب الفعلي:
- بدّل المفاتيح. استبدل
sk_test_*بـsk_live_*على السيرفر وpk_test_*بـpk_live_*على الواجهة. رابط الـ API مش بيتغيّر. - اظبط نقطة نهاية webhook فعلية. وضع الاختبار والحساب الفعلي ليهم نقاط نهاية webhook منفصلة. كل واحدة بياخد السرّ بتاع التوقيع
whsec_*الخاص بيها. - تحقّق من التواقيع على الـ webhook، مش على
onComplete. عامل حدث الـcompleteكتلميح لتجربة المستخدم، مش كتصريح. - ما تكشفش المفتاح السرّي للمتصفّح. مفاتيح
sk_*للسيرفر بس. الـ SDK بياخد مفتاح قابل للنشر (pk_*) بالتصميم. - امنع تكرار تسليمات الـ webhook على
event.id. XPay بيعيد محاولة التسليمات عند الردود المش 2xx أو انتهاء المهلة، فنفسcheckout.session.completedلنفس الجلسة ممكن يوصل أكتر من مرة. تتبّع معرّفات الأحداث اللي عالجتها بالفعل. - ابعت
Idempotency-KeyعلىPOST /checkout/sessions. لو الطلب وقعت مهلته، إعادة المحاولة بنفس المفتاح بترجّع الجلسة الأصلية بدل ما تفتح تانية. شوف عدم التكرار. - اظبط content-security-policy بيسمح بـ
https://checkout.xpay.app. لو موقعك بيستخدم توجيهframe-srcأوscript-srcصارم، لازم تسمح بالاتنين سكربت الـ SDK والـ iframe المضمّن.
رايح فين بعد كده
نموذج الكائنات
إزاي جلسة الدفع، وPayment Intent، وCharge، والعميل بيرتبطوا، وأنهي معرّفات تحتفظ بيها في سجل الطلب بتاعك.
المبالغ المستردة
اعكس دفعة ناجحة باستخدام الـ pi_* من result.paymentIntentId.
إعداد الـ Webhooks
أضِف نقطة نهاية وخُد السرّ بتاع التوقيع whsec_*.
التحقق من التواقيع
طريقة التحقق الكاملة بـ HMAC-SHA256 مع الحماية من إعادة التشغيل.
وضع الاختبار وبطاقات الاختبار
قائمة بطاقات الاختبار والنتائج اللي تقدر تحاكيها.
عايز نمط مختلف؟
قارن Drop-in بالدفع المستضاف، وElements، وروابط الدفع.
مثال HTML عادي
drop-in-modal.html وdrop-in-inline.html: نفس نمط الصفحة دي كملفات HTML عادية جاهزة للتشغيل
بالـ factory العام XPay()، من غير build step.
متجر مثال Next.js
الـ drop-in modal على /checkout?ui=embedded ومبالغ ديناميكية على /donate، جوّه متجر Next.js
كامل.