تخطَّ إلى المحتوى

3D Secure يأخذ العميل خارج صفحتك

المصادقة ترسل المتصفح إلى البنك وتعيده، وكل ما كان React يحمله يزول. كيف تُبقي حالة الدفعة حيث لا يهدمها التحويل، وماذا تعرض ريثما تعرف.

4 دقيقة قراءة

يضغط العميل على الدفع. فيقرر بنكه أن هذه الدفعة تحتاج مصادقة. فيغادر المتصفح موقعك إلى صفحة لم ترها قط، ويوافق العميل بتطبيق أو برمز، ويعود المتصفح.

وفي تلك اللحظة، تكون كل حالة 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 - هي توزيع الثقة نفسه، والتحويل ببساطة هو اللحظة التي يُختبر فيها.

مشاريعنا التجارية تعامل هذه الشاشة جزءًا من عمل المدفوعات لا من عمل التصميم، لأنها آخر ما يراه العميل قبل أن يقرر أخرج المال أم لا.

أسئلة ذات صلة

هل يحدث هذا في كل دفعة؟
لا، ولهذا بالذات يُغفَل. فالمصادقة يطلبها بنك العميل بحسب المبلغ والبطاقة والبلد وتقديره للمخاطر، فيستطيع مطوّر يختبر ببطاقة واحدة في بلد واحد أن يبني سلة الدفع كلها دون أن يرى التحويل قط. ثم يظهر في الإنتاج في نسبة معتبرة من الدفعات الأوروبية.
هل نحفظ العربة في localStorage ونستعيدها بعد التحويل؟
لمحتويات العربة هذا تسهيل معقول. أما لأي شيء يقرر النتيجة فلا، لأنه يعيش في متصفح واحد ويغيب حين يفتح العميل نفسه رابط التأكيد على حاسوبه، ولأن ما يستطيع العميل تعديله ليس دليلًا على ما دفعه.
ماذا تعرض صفحة العودة إن كانت الدفعة قيد المعالجة؟
الحقيقة، ومخرجًا. استعلام قصير على صف الطلب يغطي الحالة الشائعة التي يصل فيها الـ webhook خلال ثوانٍ؛ وبعدها ينبغي أن تتوقف الصفحة عن الدوران وتقول إن التأكيد في طريقه، مع رقم الطلب ووعد ببريد. أما المؤشر الدائر بلا نهاية فهو كيف يقرر العميل أن الدفعة فشلت فيدفع مجددًا.
هل هذا خاص بـ Stripe؟
أبدًا. كل وسيلة قائمة على التحويل لها الشكل نفسه - iDEAL وBancontact وأغلب وسائل الحوالة والأنظمة المحلية في الخليج وتركيا و3D Secure على البطاقات في كل مكان. فإن كان المسار يرسل المتصفح إلى مكان ويتوقع عودته، انطبق كل ما هنا.

العودة إلى كل المقالات