التوثيق
الدمجأنماط الدمج

Elements

ابنِ واجهة الدفع بتاعتك. فورمك بيجمّع بيانات العميل، والـ PaymentElement بتاعنا بيتولّى البطاقات والوسائل المحلية، وكودك بيستدعي confirm(). أقصى تحكم.

Elements بيخلّيك تبني واجهة الدفع كاملة على صفحتك: فورم التواصل بتاعك، وملخص الطلب بتاعك، وزراريرك، وتنسيقك. XPay بيوفّر مكوّن drop-in واحد (<PaymentElement /> أو paymentElement.mount(...) في الـ vanilla) بيتولّى مختار وسيلة الدفع، وفورم البطاقة، والـ 3D Secure، والوسائل المحلية زي ڤاليو وفوري. وكل حاجة تانية كودك.

اختار النمط ده لما تحتاج تحكم كامل في التخطيط، أو عايز تخلط حقول الدفع مع تجربة المستخدم بتاعتك (خطوات، وأشرطة تقدّم، وإكمال تلقائي للعنوان)، أو بتدمج في نظام تصميم موجود. لو "شكله حلو كنافذة منبثقة" كفاية، يبقى Drop-in نص الكود.

أمثلة الكود تحت بتغطي الاتنين Vanilla JavaScript (أي إطار، أو HTML عادي) وReact. الـ SDK الاتنين بيشتركوا في نفس الـ API؛ اختار التاب اللي يناسب الستاك بتاعك. كل صفحة في القسم ده تغطية مزدوجة فأطر العمل غير React (Vue، Svelte، Solid، Angular، Lit، وHTML عادي) بتاخد معاملة متساوية.

Elements هو النمط الوحيد اللي بيتطلّب uiMode: "custom" على جلسة الدفع. الوضع ده بيرفض كل مفاتيح التحصيل من ناحية السيرفر (nameCollection، phoneNumberCollection، billingAddressCollection، shippingAddressCollection)، وsubmitType، وcancelUrl، و أسعار النوع CUSTOM، لإن فورمك هو اللي بيملك كل ده. للمرجع الكامل للجلسة، شوف جلسة الدفع.

إزاي بيشتغل

  1. السيرفر بتاعك بيستدعي POST /checkout/sessions بـ uiMode: "custom" والبنود. XPay بيرجّع جلسة فيها clientSecret.
  2. السيرفر بتاعك بيشحن الـ clientSecret للواجهة بتاعتك.
  3. الواجهة بتاعتك بتحمّل الـ SDK، وبتهيّئ الدفع بالـ clientSecret، وبتركّب الـ payment element جوّه فورمك.
  4. العميل بيختار وسيلة دفع و(للبطاقات) بيملا فورم البطاقة. وفورمك بيجمّع كل حاجة تانية.
  5. كودك بيستدعي checkout.confirm({ customerDetails }). الـ Promise بيتحلّ بالنتيجة، أو الصفحة بتتنقّل بعيد لو ظبطت redirect: "always".
  6. XPay بيبعت webhook checkout.session.completed لنقطة النهاية بتاعتك بشكل مستقل.

الـ payment element بيشتغل جوّه iframe بيشاور على https://checkout.xpay.app. بيانات البطاقة عمرها ما بتدخل الـ DOM بتاعك.

نفّذه

1. خُد مفاتيح API للاختبار

زي Drop-in بالظبط: sk_test_* للسيرفر بتاعك، وpk_test_* للواجهة بتاعتك. لاقي الاتنين في لوحة التحكم تحت المطورين ← مفاتيح API.

2. أنشئ جلسة دفع بـ uiMode: "custom"

الجلسة شكلها زي العادية، بفرقين أساسيين:

  • uiMode: "custom" مطلوب.
  • ما تظبطش nameCollection، ولا phoneNumberCollection، ولا billingAddressCollection، ولا shippingAddressCollection، ولا submitType، ولا cancelUrl. كلهم بيترفضوا وقت الإنشاء. فورمك هو المسؤول عنهم.
curl -X POST https://api.xpay.app/checkout/sessions \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{
    "uiMode": "custom",
    "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: "custom",
    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 frontend
import 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": "custom",
        "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

كام قاعدة تستاهل تعرفها من الأول:

  • أسعار النوع CUSTOM بتترفض مع uiMode: "custom". ابنِ الجلسة بـ unitAmount ثابت بدالها. أسعار النوع CUSTOM محتاجة خانة إدخال المبلغ المستضافة بتاعتنا، وده مش بتاخده مع Elements.
  • paymentMethodTypes وpaymentMethodConfigurationId لسه بيشتغلوا لتقييد الوسائل اللي الـ payment element بيعرضها.
  • brandingSettings لسه بيشتغل كخط أساس. الـ appearance بتاعك في وقت التشغيل بيتجاوزه.

3. اظبط الـ SDK

ثبّت الـ JS SDK:

npm install @xpayeg/sdk

حمّل XPay على مستوى الموديول (أو بالـ CDN script tag لو مش بتعمل bundling)، وبعدين استدعي xpay.initCheckout({ clientSecret }) بمجرد ما يبقى عندك جلسة. كائن الـ checkout اللي بيرجع بيحمل بيانات الجلسة ودوال الإجراءات مع بعض.

<form id="checkout-form">
  <!-- Your contact-info fields, your order summary, your styling -->
  <input id="email" type="email" required />
  <input id="name" type="text" required />

  <!-- The one piece you don't build -->
  <div id="payment-element"></div>

  <button id="pay-button" type="submit" disabled>Pay</button>
  <p id="error" role="alert"></p>
</form>
import { loadXPay } from "@xpayeg/sdk";

// Module level: call once, share across the page.
const xpayPromise = loadXPay("pk_test_...");

async function start(clientSecret: string) {
  const xpay = await xpayPromise;
  if (!xpay) return; // SSR returns null on the server

  // Returns a single object with session fields and action methods merged together.
  const checkout = await xpay.initCheckout({ clientSecret });

  // Handle terminal session states before mounting anything.
  if (checkout.status.type === "expired") {
    showExpiredView();
    return;
  }
  if (checkout.status.type === "complete") {
    showAlreadyPaidView();
    return;
  }

  // Mount the payment element (next step).
  mountPaymentElement(checkout);
}

لو مش على bundler، حطّ script tag واستخدم الـ factory العام XPay اللي بيكشفه وقت التشغيل:

<script src="https://checkout.xpay.app/v1/sdk.js"></script>
<script type="module">
  const xpay = XPay("pk_test_...");
  const checkout = await xpay.initCheckout({ clientSecret });
</script>

ثبّت الـ package الاتنين:

npm install @xpayeg/sdk @xpayeg/react

غلّف الجزء من تطبيقك اللي بيعرض الدفع في <XPayProvider>. حمّل الـ SDK على مستوى الموديول، مش جوّه مكوّن. الـ Provider بيقبل الـ Promise مباشرة.

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

// Module level (runs once per page)
const xpayPromise = loadXPay("pk_test_...");

export default function CheckoutPage({ clientSecret }: { clientSecret: string }) {
  return (
    <XPayProvider xpay={xpayPromise} options={{ clientSecret }}>
      <CheckoutForm />
    </XPayProvider>
  );
}

options.clientSecret بيقبل الاتنين string وPromise<string>، فلو بتجيب الجلسة من الـ backend بتاعك تقدر تمرّر الـ Promise مباشرة من غير ما تدير حالة تحميل.

4. ركّب الـ payment element واحكم على زرار الدفع

خُد نسخة Elements من الـ checkout، أنشئ PaymentElement، ركّبه جوّه حاوية فورمك، واستنى أحداث الـ change علشان تتابع اكتمال الفورم.

function mountPaymentElement(checkout) {
  const elements = checkout.getElements();
  const paymentElement = elements.create("payment");
  paymentElement.mount("#payment-element");

  const payButton = document.getElementById("pay-button") as HTMLButtonElement;
  let paymentReady = false;

  paymentElement.on("change", (event) => {
    paymentReady = event.complete;
    payButton.disabled = !paymentReady || !checkout.canConfirm;
  });

  // Update the button label when totals change (promo codes, quantities, etc.)
  checkout.on("change", (session) => {
    payButton.textContent = `Pay ${session.currency} ${(session.amountTotal / 100).toFixed(2)}`;
  });
}

كام حاجة بتحصل هنا:

  • event.complete من الـ payment element. بيتابع إذا كان العميل ملا الفورم بما يكفي علشان يحاول الدفع. استخدمه علشان تعطّل زرار الدفع بتاعك.
  • checkout.canConfirm هو البوابة الخاصة بـ XPay. لازم العلَمين الاتنين يكونوا true قبل ما تستدعي confirm().
  • checkout.on("change", session => ...) بيطلق كل مرة الجلسة بتتغيّر (تطبيق كود خصم، تحديث كمية، إعادة حساب رسوم). استخدمه علشان تحدّث الـ DOM بتاعك. مع initCheckout، حقول كائن الـ checkout مش تفاعلية لوحدها، فاشترك في الـ change لأي إجماليات أو ملخصات بتعرضها.

useCheckout() بيرجّع tagged union. ضيّق على الـ type قبل ما تقرا بيانات الـ checkout أو تستدعي دوال الإجراءات.

"use client";

import { useState } from "react";
import { PaymentElement, useCheckout } from "@xpayeg/react";

function CheckoutForm() {
  const state = useCheckout();
  const [paymentReady, setPaymentReady] = useState(false);

  if (state.type === "loading") return <Skeleton />;
  if (state.type === "error") return <ErrorView message={state.error.message} />;

  const { checkout } = state;

  // Handle terminal states
  if (checkout.status.type === "expired") return <ExpiredView />;
  if (checkout.status.type === "complete") return <AlreadyPaidView />;

  return (
    <form>
      {/* Your contact-info form, your order summary, your styling */}
      <YourContactFields />

      {/* The one piece you don't build */}
      <PaymentElement
        onChange={(event) => setPaymentReady(event.complete)}
        onLoadError={(err) => console.error(err.message)}
      />

      <PayButton
        disabled={!paymentReady || !checkout.canConfirm}
        amount={checkout.amountTotal}
        currency={checkout.currency}
      />
    </form>
  );
}

كام حاجة بتحصل هنا:

  • تضييق state.type. loading أثناء تحميل الجلسة، وerror لو فشلت في التحميل (شبكة، سرّ غلط، جلسة منتهية)، وsuccess بمجرد ما كل حاجة تجهز.
  • تضييق checkout.status.type. جوّه success، الجلسة نفسها ممكن تكون open أو expired أو complete. اعرض واجهة مختلفة لكل واحدة. open بس هي اللي المفروض تخلّي العميل يحاول يدفع.
  • event.complete من <PaymentElement />. بيتابع إذا كان العميل ملا الفورم بما يكفي علشان يحاول الدفع. استخدمه علشان تعطّل زرار الدفع بتاعك. فيه كمان event.empty، وevent.collapsed، وevent.value.type (الوسيلة المختارة)، وevent.session (لقطة الجلسة الكاملة).
  • checkout.canConfirm هو البوابة الخاصة بـ XPay. لازم العلَمين الاتنين يكونوا true قبل ما تستدعي confirm().
  • مفيش حاجة لمستمع change يدوي. useCheckout() بيعيد العرض تلقائيًا لما الإجماليات أو البنود أو الخصومات بتتغيّر، فقراءة checkout.amountTotal بترجّعلك آخر قيمة دايمًا.

5. أكّد الدفعة

اربط زرار الدفع بتاعك بـ checkout.confirm(). أنت بتوفّر بيانات العميل اللي فورمك جمّعها؛ وXPay بيتولّى الـ 3D Secure، والوسائل القائمة على التحويل، والنتيجة.

document.getElementById("checkout-form")!.addEventListener("submit", async (e) => {
  e.preventDefault();

  const errorEl = document.getElementById("error")!;
  errorEl.textContent = "";

  const result = await checkout.confirm({
    customerDetails: {
      email: (document.getElementById("email") as HTMLInputElement).value,
      name: (document.getElementById("name") as HTMLInputElement).value,
      // Optional billing/shipping details
    },
    // "if_required" (default): result returns to your code; navigate yourself
    // "always": redirects to afterCompletion.redirect.url on success
    redirect: "if_required",
  });

  if (result.type === "error") {
    errorEl.textContent = result.error.message;
    return;
  }

  // Success: navigate, show a success view, etc.
  window.location.href = `/success?session_id=${checkout.id}`;
});
async function handleSubmit() {
  setSubmitting(true);
  setError("");

  const result = await checkout.confirm({
    customerDetails: {
      email,
      name,
      phone,
      // Optional billing/shipping details
      billingDetails: { address: { line1, city, country } },
    },
    // "if_required" (default): result returns to your code; navigate yourself
    // "always": redirects to afterCompletion.redirect.url on success
    redirect: "if_required",
  });

  if (result.type === "error") {
    setError(result.error.message);
    setSubmitting(false);
    return;
  }

  // Success: navigate, show a success view, etc.
  router.push(`/success?session_id=${checkout.id}`);
}

شكل الـ result (واحد في الاتنين):

  • { type: "success", session }: الدفعة نجحت. الـ session هي Checkout SessionAPI المحدّثة بـ status: { type: "complete", paymentStatus: "paid" }.
  • { type: "error", error }: الدفعة فشلت. الـ error بيحمل type، وcode، وmessage، وحقول خاصة بالرفض زي declineCode لأخطاء البطاقة. شوف أخطاء الدفع.

تحدّيات الـ 3D Secure، ونوافذ تأكيد ڤاليو، ونوافذ مرجع فوري المنبثقة: كلهم بيتعالجوا جوّه الـ payment element وفي وقت ما الـ confirm() مستني. الـ Promise بيتحلّ بس بعد ما مسار الدفع الكامل يكتمل (أو يفشل).

تعامل مع تغيّرات حالة الجلسة

دوال الإجراءات بترجّع Promise<ActionResult> بنفس شكل الـ success / error بتاع confirm(). الـ API واحد بين الـ vanilla وReact؛ الفرق هو إزاي الواجهة بتاعتك بترد.

// Promotion code
const result = await checkout.applyPromotionCode("SAVE20");
if (result.type === "error") setPromoError(result.error.message);

// Remove the applied code
await checkout.removePromotionCode();

// Update a line item's quantity
await checkout.updateLineItemQuantity({ lineItem: "li_test_abc", quantity: 3 });

// Re-fetch the session from the server (after a server-side change)
await checkout.fetchUpdates();

الـ checkout كائن عادي، فحقوله مش بتتحدّث تلقائيًا. اشترك في الـ change علشان تحدّث أي حاجة بتعرضها من الجلسة.

checkout.on("change", (session) => {
  totalEl.textContent = `${session.currency} ${(session.amountTotal / 100).toFixed(2)}`;
  // Re-render line items, discounts, fee breakdown, etc.
});

الـ handler بيستلم لقطة CheckoutSession الكاملة المحدّثة.

useCheckout() بيعيد العرض تلقائيًا بعد كل إجراء، فـ checkout.amountTotal، وcheckout.totalDetails، وcheckout.lineItems كلهم بيعكسوا حالة السيرفر الجديدة في العرض اللي بعده. مفيش حاجة لاشتراك on("change", ...) للحالة الأساسية.

لو عايز ترد على التغيّرات بره شجرة العرض (تسجيل، تحليلات)، لسه تقدر تشترك عن طريق useEffect:

useEffect(() => {
  if (state.type !== "success") return;
  state.checkout.on("change", (session) => {
    analytics.track("checkout_updated", { amount: session.amountTotal });
  });
}, [state]);

خصّص المظهر في وقت التشغيل

appearance بيقبل نفس شكل brandingSettings على الجلسة. لقائمة الحقول الكاملة، شوف الإعدادات المتقدمة → العلامة التجارية.

مرّر appearance على initCheckout، أو استدعي checkout.changeAppearance(...) بعدين.

const checkout = await xpay.initCheckout({
  clientSecret,
  appearance: {
    colorMode: "dark",
    borderStyle: "rounded",
    colors: { primary: "#635bff" },
  },
  locale: "ar",
});

// Later, sync with your site's theme toggle.
themeToggle.addEventListener("change", () => {
  checkout.changeAppearance({
    colorMode: themeToggle.checked ? "dark" : "light",
  });
});

مرّر options.appearance على <XPayProvider> علشان تظبط المظهر المبدئي، أو استدعي checkout.changeAppearance(...) بعدين.

<XPayProvider
  xpay={xpayPromise}
  options={{
    clientSecret,
    appearance: {
      colorMode: "dark",
      borderStyle: "rounded",
      colors: { primary: "#635bff" },
    },
    locale: "ar",
  }}
>
  <CheckoutForm />
</XPayProvider>

نمط شائع: زامن مظهر XPay مع مفتاح الثيم بتاع موقعك.

function CheckoutForm({ checkout }: { checkout: Checkout }) {
  const { resolvedTheme } = useTheme(); // your app's theme hook

  useEffect(() => {
    checkout.changeAppearance({
      colorMode: resolvedTheme === "dark" ? "dark" : "light",
    });
  }, [resolvedTheme, checkout]);

  return /* ... */;
}

استراتيجيات التأكيد

confirm() ليه وضعين تحويل. اختار اللي يناسب توجيه ما بعد الدفع بتاعك. الـ API واحد في الـ vanilla وReact.

redirectبيحصل إيه عند النجاح
"if_required" (افتراضي)الـ Promise بيتحلّ بـ { type: "success", session }. كودك بيتنقّل / يعرض واجهة النجاح.
"always"الصفحة بتتنقّل لـ afterCompletion.redirect.url. الكود اللي بعد await بيشتغل بس عند الخطأ.
// Pattern A: handle success in your code
const result = await checkout.confirm({ customerDetails: { email, name } });
if (result.type === "success") {
  // navigate, show a success view, etc.
}

// Pattern B: let XPay redirect you
await checkout.confirm({
  customerDetails: { email, name },
  redirect: "always",
});
// ^ On success, the page navigates away. Code below only runs on error.

// Pattern B with a custom return URL (overrides the session's afterCompletion.redirect.url)
await checkout.confirm({
  customerDetails: { email, name },
  redirect: "always",
  returnUrl: "https://yourshop.example/checkout/done",
});

النمط A أشيع في تطبيقات الصفحة الواحدة. النمط B أبسط لو عندك صفحة نجاح ثابتة.

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

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

الـ API واحد في الـ vanilla وReact: checkout.on("error", handler).

checkout.on("error", (error) => {
  bannerEl.textContent = error.message;
});
useEffect(() => {
  if (state.type !== "success") return;
  state.checkout.on("error", (error) => {
    setBanner(error.message);
  });
}, [state]);

كائن الخطأ ليه نفس الشكل اللي دوال الإجراءات بترجّعه: type، وcode، وmessage، وحقول خاصة بالرفض زي declineCode لأخطاء البطاقة. شوف أخطاء الدفع.

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

استدعي checkout.submit() علشان تتحقّق من كل حقل في الـ payment element قبل ما تلتزم بـ confirm(). مفيد لما عايز تحكم نافذة تأكيد أو مسار متعدد الخطوات. نفس الـ API في العالمين:

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

// Fields are valid; show your confirm dialog or continue
const confirmed = await showDialog(`Pay with ${selectedPaymentMethod}?`);
if (!confirmed) return;

const result = await checkout.confirm({ customerDetails: { email, name } });

submit() بيتحلّ بنوع وسيلة الدفع المتحقَّق منها، فتقدر تذكرها في نافذتك ("ادفع بالبطاقة؟"، "ادفع بڤاليو؟").

أكّد بـ webhook

تحلّ الـ confirm() Promise بنجاح مجاملة لتجربة المستخدم. لازم السيرفر بتاعك يفضل يستنى checkout.session.completed علشان يعلّم الطلب مدفوع. العملاء ممكن يفقدوا الاتصال بين الـ confirm() والـ setState بتاعك، ومعالِجك ممكن يرمي خطأ، والـ SDK ممكن يفقد النتيجة. الـ webhook هو مصدر الحقيقة.

لشرح المعالِج الكامل (التحقق من التوقيع، وجدول حقول الحمولة، وسلوك إعادة المحاولة)، شوف الدفع المستضاف → أكّد بـ webhook. كود المعالِج واحد بغض النظر عن النمط اللي أنتج الجلسة.

اختبره

في وضع الاختبار، بطاقة النجاح 5123 4500 0000 0008 بتاريخ انتهاء 01/39 بتشغّل المسار الناجح جوّه الـ payment element. فورم البطاقة، وتحدّي الـ 3D Secure، والنتيجة كلهم بيمرّوا من خلال confirm(). لقائمة بطاقات الاختبار الكاملة ومصفوفة تاريخ الانتهاء للنتيجة، شوف وضع الاختبار وبطاقات الاختبار.

علشان تجرّب معالِج الـ webhook بتاعك محليًا قبل النشر، شوف تطوير الـ webhook محليًا.

قائمة التحقق قبل الإنتاج

قبل ما تحوّل للحساب الفعلي:

  • بدّل المفاتيح. sk_live_* على السيرفر، وpk_live_* على الواجهة. رابط الـ API مش بيتغيّر.
  • اظبط نقطة نهاية webhook فعلية. وضع الاختبار والحساب الفعلي ليهم نقاط نهاية منفصلة، كل واحدة بالسرّ بتاع التوقيع whsec_* الخاص بيها.
  • تحقّق على الـ webhook، مش على نتيجة confirm(). عامل النجاح اللي اتحلّ كتلميح للعرض، مش كتصريح.
  • ما تشحنش المفتاح السرّي. sk_* للسيرفر بس. الـ SDK بياخد مفتاح قابل للنشر (pk_*).
  • اسمح بـ https://checkout.xpay.app في الـ CSP بتاعك. الـ payment element iframe وسكربت الـ SDK الاتنين بيتحمّلوا من هناك. لو ظبطت توجيهات frame-src أو script-src، أدرجه.
  • امنع تكرار تسليمات الـ webhook على event.id. XPay بيعيد المحاولة عند الردود المش 2xx أو انتهاء المهلة، فنفس الحدث ممكن يوصل مرتين.
  • ابعت Idempotency-Key على POST /checkout/sessions. لو الطلب وقعت مهلته، إعادة المحاولة بنفس المفتاح بترجّع الجلسة الأصلية بدل ما تفتح تانية. شوف عدم التكرار.
  • تعامل مع الحالات النهائية. checkout.status.type === "expired" و"complete" المفروض الاتنين يعرضوا واجهات مخصّصة، مش يقعوا للفورم.

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

في الصفحة دي