سلة دفع لا تثق بالمتصفح
العميل يؤكد الدفعة، والعميل يجب ألا يُنشئ الطلب أبدًا. أين تنتمي كل قطعة من سلة الدفع في تطبيق App Router، وأي القطع قابلة للعبث.
لسلة الدفع ثلاثة أطراف: متصفح العميل، وتطبيقك، ومزوّد الدفع. وبناؤها بشكل صحيح هو بالكامل تقريبًا مسألة من منهم يحق له أن يدّعي ماذا، والمتصفح يحق له أن يدّعي القليل جدًا.
وفي تطبيق App Router يحطّ هذا التقسيم في ملفات بعينها، ويستحق أن يُكتَب قبل أن يوجد أيٌّ منها.
ما يقرره الخادم
متصفح العميل يرسل معرّفات منتجات وكميات. هذا كل ما يحق له أن يسهم به.
أما السعر والضريبة والشحن والخصم والمبلغ النهائي فتُحسَب على الخادم من بياناتك أنت، في كل مرة، بما في ذلك الطلب الذي يُنشئ الدفعة. لا تُقرأ من حقل مخفي، ولا تُؤخَذ من إجمالي عربة في المتصفح، ولا تُصدَّق لأن الطلب السابق حسبها صحيحًا.
// app/checkout/actions.ts
'use server'
export async function startCheckout(items: CartItem[]) {
const session = await auth()
const priced = await priceBasket(items) // أسعار الخادم وقواعد الخادم
const order = await db.order.create({
data: { userId: session.user.id, status: 'pending', total: priced.total },
})
const intent = await stripe.paymentIntents.create({
amount: priced.total, // الوحدة الصغرى، من الخادم
currency: priced.currency,
metadata: { orderId: order.id },
automatic_payment_methods: { enabled: true },
})
return { clientSecret: intent.client_secret, orderId: order.id }
}ثلاثة أمور هناك مقصودة. الطلب يوجد قبل الدفعة، بحالة معلّقة، فيكون هناك دائمًا ما تُعلَّق عليه النتيجة. ومعرّفه يسافر في بيانات الدفعة الوصفية، وهو ما يتيح لـ webhook يصل بعد أربعين دقيقة أن يجده بلا جدول بحث. والشيء الوحيد العائد إلى المتصفح هو client secret يخوّل تأكيد هذه الدفعة وحدها ولا شيء غيرها.
ما يفعله المتصفح وما لا يفعله
المتصفح يجمع البطاقة ويؤكد الدفعة مع Stripe مباشرة. ولا تلمس البطاقة خادمك قط، وهذا ما يُبقيك خارج منظومة الامتثال التي كان التعامل مع بيانات البطاقات سيُدخلك فيها.
وذلك التأكيد مكوّن عميل - أحد المواضع القليلة في موقع تجاري التي يكون فيها مكوّن العميل ضروريًا فعلًا لا مريحًا فحسب، ويستحق الاستثناء من إبقاء الحد ضيقًا. فهو يحتاج جافاسكربت Stripe، ويحتاج تركيب إطار، ويحتاج التفاعل مع بنك العميل.
وما لا يفعله هو إخبار خادمك بأن الدفعة نجحت. فنداء التأكيد يُحَل في المتصفح، والوعد المُحَل في متصفح ليس حقيقة عن المال. إنه سبب وجيه لعرض شاشة نجاح؛ وليس سببًا لتجهيز طلب.
النتيجة تصل من مكان آخر
الـ Route Handler يستقبل الـ webhook، ويتحقق من التوقيع على الجسم الخام، ويسلّم الحدث إلى شيء دائم.
// app/api/stripe/webhook/route.ts
export async function POST(request: Request) {
const raw = await request.text() // بايتات خام، لا json()
let event
try {
event = stripe.webhooks.constructEvent(
raw, request.headers.get('stripe-signature')!, process.env.STRIPE_WEBHOOK_SECRET!,
)
} catch {
return new Response(null, { status: 400 })
}
await recordAndEnqueue(event.id, raw) // خارج هذه العملية
return new Response(null, { status: 200 })
}وrequest.json() كان سيكسر التوقيع، لأن التجزئة على البايتات المرسلة، وإعادة
ترميز كائن لا تعيد إنتاجها. ويبقى المعالج قصيرًا عن قصد: التجهيز والبريد وخصم
المخزون والمحاسبة كلها تقع بعد الرد، على شيء يستطيع إعادة المحاولة.
لا في after، التي تشغّل الدالة بعد إرسال الرد لكنها تبقى داخل الاستدعاء نفسه
والمدة القصوى نفسها للمسار - لا تُعاد محاولتها ولا تنجو من اختفاء النسخة. وهذا
الحد نفسه الذي يصطدم به كل تكامل خارجي،
والمدفوعات هي الحالة التي يكلّف فيها تجاوزه مالًا حقيقيًا.
الصفحة بين الاثنتين
بين التأكيد والـ webhook فجوة، ثوانٍ عادةً ودقائق أحيانًا، والعميل جالس على صفحة طوالها.
اعرض من صف الطلب عندك، لا من أي شيء أبلغ به المتصفح. الطلب معلّق، فتقول الصفحة إن الدفعة قيد التأكيد وتعرض رقم الطلب. وحين يصل الـ webhook وينقلب الصف إلى مدفوع، تعرض الصفحة الإيصال.
والخطأ الرخيص هو عرض النجاح تفاؤلًا لأن وعد المتصفح تحقق. يبدو صحيحًا في التطوير حيث يصل الـ webhook خلال مئات من الأجزاء من الثانية، ويُنتج في الإنتاج شاشة طلب مؤكد لدفعة رُفضت في الخطوة الأخيرة. اعرض ما تعرفه قاعدة بياناتك؛ فهي الطرف الوحيد هنا الذي تتحكم به.
وتلك الحالة الوسطى شاشة حقيقية بنص حقيقي، وكيفية بنائها تهم في الدفع أكثر من أي مكان آخر، لأن هذه هي اللحظة التي يقرر فيها العميل أيحاول ثانية فيدفع مرتين.
أين يضع هذا البنية
تطبيق Next.js يملك شاشة الدفع، والتسعير، والطلب المعلّق، ومستقبل الـ webhook. ولا يملك إعادة المحاولة ولا المطابقة ولا أي شيء يجب أن يقع بجدول. تلك لعامل أو لخدمة خلفية، للسبب المعتاد: الطلب ينتهي، وقصة الدفعة لا تنتهي.
عملنا التجاري يبدأ من هذا الحد لا من التصميم، لأن سلة دفع جميلة تثق بالمتصفح هي سلة ستُستغَل خلال شهر من الإطلاق.
