3D Secure يأخذ العميل خارج صفحتك
المصادقة ترسل المتصفح إلى البنك وتعيده، وكل ما كان React يحمله يزول. كيف تُبقي حالة الدفعة حيث لا يهدمها التحويل، وماذا تعرض ريثما تعرف.
يضغط العميل على الدفع. فيقرر بنكه أن هذه الدفعة تحتاج مصادقة. فيغادر المتصفح موقعك إلى صفحة لم ترها قط، ويوافق العميل بتطبيق أو برمز، ويعود المتصفح.
وفي تلك اللحظة، تكون كل حالة React التي كانت سلة الدفع تحملها قد زالت. شجرة المكوّنات فُكّت، والخطافات صُفّرت، والمتغير الذي يحمل أي طلب كان هذا غير موجود. وهذا ليس علّة ولا شيء لإصلاحه في شيفرتك - فالتحويل تنقّل، والتنقّل يهدم حالة العميل.
فوجب بناء سلة الدفع بحيث لا يكلّف فقدانها شيئًا.
ما يجب أن يوجد قبل مغادرة العميل
قبل أن يذهب المتصفح إلى أي مكان، يجب أن يكون الخادم يحمل أصلًا كل ما يلزم لفهم العودة. وعمليًا هذا يعني صف طلب، بحالة معلّقة، بمعرّف خاص بك.
وذلك المعرّف يذهب إلى موضعين. يذهب إلى بيانات الدفعة الوصفية عند المزوّد، ليجد
حدثٌ يصل لاحقًا الطلبَ بلا جدول بحث. ويذهب في return_url مقطعًا في المسار:
return_url: `https://example.com/orders/${order.id}/confirm`مقطع مسار لا معامل استعلام، لأن المسار صفحة تتحكم بها والمعامل شيء يستطيع العميل تغييره. فهذا الرابط سيزوره متصفح بعد جولة عبر طرف ثالث، وكل ما يحمله ينبغي أن يكون إشارة إلى شيء يستطيع الخادم التحقق منه لا ادعاءً عليه أن يصدّقه.
ولا شيء عن المبلغ أو الحالة أو العميل يدخل ذلك الرابط. إنه يسمّي طلبًا؛ وما يعنيه ذلك الطلب يقرره الخادم.
صفحة العودة تقرأ قاعدة البيانات
مسار التأكيد مكوّن خادم، يحمّل الطلب بالمعرّف الذي في المسار، ويتأكد أنه يخص السائل، ويعرض ما يقوله الصف.
export default async function Confirm({ params }) {
const { id } = await params
const session = await auth()
const order = await db.order.findUnique({ where: { id } })
if (!order || order.userId !== session?.user?.id) notFound()
if (order.status === 'paid') return <Receipt order={order} />
if (order.status === 'failed') return <Retry order={order} />
return <Pending order={order} />
}وفحص الملكية ليس اختياريًا. فمعرّفات الطلبات تنتهي في سجل المتصفح وفي روابط مشتركة وفي تذاكر دعم، والمعرّف وحده ليس إذنًا برؤية إيصال عليه عنوان أحدهم.
ولاحظ ما لا تنظر إليه الصفحة: معاملات الاستعلام التي ألحقها المزوّد في طريق العودة. بعضها يحمل حالة فعلًا، وهي مفيدة لاختيار أي رسالة تُعرض أولًا، لكنها جاءت عبر متصفح وليست هي ما يقرر أمدفوع الطلب أم لا.
الحالة الثالثة هي المهمة
تبني أغلب الفرق نتيجتين، نجاح وفشل، ثم تكتشف الثالثة في الإنتاج. والثالثة أن الدفعة حقيقية، والعميل هنا، وقاعدة بياناتك لا تعرف الجواب بعدُ لأن الـ webhook لم يصل.
وتدوم عادةً ثوانٍ. وأحيانًا دقائق، وهي الحالة التي يجب أن تُصمَّم الشاشة لأجلها.
استعلم عن صف الطلب مدة قصيرة - مكوّن عميل يعيد الفحص مرتين أو ثلاثًا على مدى عشر أو خمس عشرة ثانية يغطي كل الحالات الواقعية تقريبًا. ثم توقّف. فالصفحة التي تدور إلى الأبد تعلّم العميل أن شيئًا تعطّل، وأول ما يفعله بعدها أن يدفع مرة أخرى، فيصير webhook بطيء خصمًا مكررًا واستردادًا.
فشاشة الانتظار، بعد أن يستسلم الاستعلام، ينبغي أن تقول بوضوح إن الدفعة وصلت وإن التأكيد قيد الإنهاء، وتعرض رقم الطلب، وتَعِد ببريد. وذلك النص يؤدي عملًا حقيقيًا: هو الفرق بين عميل ينتظر وعميل يدفع مرتين.
الحالة الوحيدة الناجية هي ما في الرابط
وراء هذا مبدأ أوسع يتجاوز المدفوعات بكثير، وهذا أحدّ أمثلته.
فكل ما يحتاجه التطبيق بعد تنقّل لا يتحكم به يجب أن يعيش حيث لا يصل التنقّل: مسار
الرابط، أو كعكة، أو قاعدة البيانات. وحالة React لا تصلح. وsessionStorage لا
يصلح، لأن العميل قد يعود في لسان آخر، أو على جهاز آخر، أو من رابط في بريد.
والمكوّن الذي يحمل الجواب لا يصلح، لأنه لن يكون مركّبًا.
وعمليًا يدفع هذا حالة سلة الدفع إلى الخادم، وهو حيث يريدها App Router أصلًا.
وسؤال التخزين المؤقت له عندئذ جواب حاسم: هذه
الصفحات لا تُخزَّن أبدًا. فتأكيد الطلب خاص بكل عميل ويتغير تحتك؛ وقراءة
cookies() في المسار تجعله ديناميكيًا، وهنا هذا هو السلوك المطلوب لا كلفة.
وبقية سلة الدفع - من يحسب السعر، ومن يحق له إنشاء الطلب، وأين يحطّ الـ webhook - هي توزيع الثقة نفسه، والتحويل ببساطة هو اللحظة التي يُختبر فيها.
مشاريعنا التجارية تعامل هذه الشاشة جزءًا من عمل المدفوعات لا من عمل التصميم، لأنها آخر ما يراه العميل قبل أن يقرر أخرج المال أم لا.
