🌐 Detecting your location…

كيفية إضافة مدفوعات شريطية إلى تطبيق Next.js في عام 2026: دليل التكامل الكامل

⏱️4 min read  ·  712 words

تفشل معظم عمليات تكامل Stripe في نفس المكان: فهي تتعامل مع إعادة توجيه المتصفح كدليل على الدفع. ليس كذلك. المصدر الوحيد الموثوق للحقيقة هو خطاف الويب الذي يرسله Stripe إلى خادمك. ينشئ هذا الدليل تكاملًا صحيحًا في Next.js مع جهاز توجيه التطبيقات — الخروج، وخطافات الويب مع التحقق من التوقيع، والوفاء غير الفعال، والاشتراكات.

العمارة أولا

إن فهم التدفق يمنع الخطأين اللذين يؤديان إلى كسر معظم عمليات التكامل.

  1. يقوم الخادم الخاص بك بإنشاء جلسة Checkout ويعيد عنوان URL الخاص بها.
  2. يقوم المتصفح بإعادة التوجيه إلى Stripe. يدفع المستخدم على صفحة Stripe – تفاصيل البطاقة لا تمس الخادم الخاص بك أبدًا.
  3. يقوم Stripe بإعادة توجيه المتصفح مرة أخرى إلى عنوان URL الخاص بالنجاح.
  4. بشكل منفصل، يرسل Stripe خطافًا على الويب إلى الخادم الخاص بك لتأكيد الدفع.
  5. معالج webhook الخاص بك يمنح الوصول. ليست صفحة النجاح

الخطوة 5 هي اللعبة بأكملها. يمكن للمستخدم إغلاق علامة التبويب قبل إعادة التوجيه، أو فقدان الاتصال، أو ببساطة تعديل عنوان URL للنجاح وزيارته مباشرة. منح الوصول فقط على webhook.

الخطوة 1: تثبيت وتكوين

npm install stripe @stripe/stripe-js

تخزين المفاتيح في متغيرات البيئة. يجب ألا يصل المفتاح السري إلى العميل أبدًا، لذلك لن يحصل علىNEXT_PUBLIC_ بادئة.

# .env.local
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
NEXT_PUBLIC_BASE_URL=http://localhost:3000

قم بإنشاء عميل Stripe مشترك واحد حتى لا تقوم بإنشاء عميل لكل طلب.

// lib/stripe.ts
import Stripe from 'stripe';

if (!process.env.STRIPE_SECRET_KEY) {
  throw new Error('STRIPE_SECRET_KEY is not set');
}

export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
  apiVersion: '2026-06-30',
  typescript: true,
});

تثبيتapiVersion يهم. وبدونها، فإنك ترث أي إصدار افتراضي لحسابك، ويمكن أن يتغير ذلك في عهدك.

الخطوة 2: إنشاء جلسة الخروج

لا تقبل أبدًا سعرًا من العميل. أرسل معرف المنتج وابحث عن السعر على الخادم، أو يقوم المستخدم ببساطة بتحرير الطلب ويدفع سنتًا واحدًا.

// app/api/checkout/route.ts
import { NextResponse } from 'next/server';
import { stripe } from '@/lib/stripe';
import { getCurrentUser } from '@/lib/auth';

const PRICES: Record<string, string> = {
  pro_monthly: 'price_1AbCdEfGhIjKlMnO',
  pro_yearly:  'price_1XyZaBcDeFgHiJkL',
};

export async function POST(req: Request) {
  const user = await getCurrentUser();
  if (!user) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const { plan } = await req.json();
  const priceId = PRICES[plan];
  if (!priceId) {
    return NextResponse.json({ error: 'Unknown plan' }, { status: 400 });
  }

  const session = await stripe.checkout.sessions.create({
    mode: 'subscription',
    line_items: [{ price: priceId, quantity: 1 }],
    success_url: `${process.env.NEXT_PUBLIC_BASE_URL}/welcome?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url:  `${process.env.NEXT_PUBLIC_BASE_URL}/pricing`,
    customer_email: user.email,
    // client_reference_id survives the round trip and arrives in the webhook.
    client_reference_id: user.id,
    metadata: { userId: user.id, plan },
  });

  return NextResponse.json({ url: session.url });
}

client_reference_id وmetadata هي كيفية معرفة المستخدم الذي قام بالدفع عند وصول خطاف الويب. احذفها وسوف تقوم بمطابقة المدفوعات مع الحسابات عبر البريد الإلكتروني، وهو ما يقطع اللحظة التي يدفع فيها شخص ما بعنوان مختلف.

الخطوة 3: إعادة التوجيه من العميل

'use client';

export function UpgradeButton({ plan }: { plan: string }) {
  async function handleClick() {
    const res = await fetch('/api/checkout', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ plan }),
    });

    if (!res.ok) {
      alert('Could not start checkout. Please try again.');
      return;
    }

    const { url } = await res.json();
    window.location.href = url;
  }

  return <button onClick={handleClick}>Upgrade</button>;
}

الخطوة 4: الخطاف الإلكتروني – حيث تعيش الصحة

شيئان يعطلان خطافات الويب في Next.js. أولاً، يجب عليك التحقق من صحة التوقيع مقابلخام هيئة الطلب؛ إذا قام أي شيء بتوزيعه إلى JSON أولاً، فسيفشل التحقق. ثانيًا، يجب عليك التعامل مع عمليات التسليم المكررة، لأن Stripe يعيد المحاولة وسيقوم بتسليم نفس الحدث أكثر من مرة.

// app/api/webhooks/stripe/route.ts
import { NextResponse } from 'next/server';
import { headers } from 'next/headers';
import { stripe } from '@/lib/stripe';
import { grantAccess, revokeAccess, hasProcessed, markProcessed } from '@/lib/billing';

export async function POST(req: Request) {
  // req.text() gives the raw body, which signature verification requires.
  const body = await req.text();
  const signature = (await headers()).get('stripe-signature');

  if (!signature) {
    return NextResponse.json({ error: 'Missing signature' }, { status: 400 });
  }

  let event;
  try {
    event = stripe.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch (err) {
    console.error('Signature verification failed:', err);
    return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });
  }

  // Stripe retries on any non-2xx, so the same event can arrive repeatedly.
  if (await hasProcessed(event.id)) {
    return NextResponse.json({ received: true });
  }

  try {
    switch (event.type) {
      case 'checkout.session.completed': {
        const session = event.data.object;
        const userId = session.metadata?.userId ?? session.client_reference_id;
        if (userId) {
          await grantAccess(userId, {
            customerId: session.customer as string,
            subscriptionId: session.subscription as string,
          });
        }
        break;
      }

      case 'customer.subscription.deleted': {
        await revokeAccess(event.data.object.customer as string);
        break;
      }

      case 'invoice.payment_failed': {
        // Notify the user; do not revoke immediately — cards fail temporarily.
        break;
      }
    }

    await markProcessed(event.id);
    return NextResponse.json({ received: true });
  } catch (err) {
    console.error('Webhook handler failed:', err);
    // Return 500 so Stripe retries rather than dropping the event.
    return NextResponse.json({ error: 'Handler failed' }, { status: 500 });
  }
}

hasProcessed وmarkProcessed يجب الكتابة إلى جدول مرتبط بمعرف حدث Stripe مع قيد فريد. هذا القيد الوحيد هو ما يجعل التنفيذ عاجزًا – وبدونه، ستؤدي إعادة المحاولة إلى إضافة رصيد مزدوج إلى الحساب.

الخطوة 5: اختبار Webhooks محليًا

لا يمكن للشريط أن يصل إلىlocalhost، لذا قم بإعادة توجيه الأحداث باستخدام CLI.

stripe login
stripe listen --forward-to localhost:3000/api/webhooks/stripe

# In another terminal, fire a test event:
stripe trigger checkout.session.completed

stripe listen يطبع سر توقيع webhook. استخدم هذه القيمة لـSTRIPE_WEBHOOK_SECRET قيد التطوير — وهو يختلف عن الموجود في لوحة المعلومات.

الخطوة 6: تؤكد صفحة النجاح، ولا تمنح

يجب أن تقرأ صفحة النجاح الحالة التي كتبها خطاف الويب الخاص بك بالفعل. إذا لم يصل خطاف الويب بعد، فاعرض الحالة المعلقة بدلاً من منح أي شيء.

// app/welcome/page.tsx
import { getCurrentUser } from '@/lib/auth';
import { getSubscription } from '@/lib/billing';

export default async function WelcomePage() {
  const user = await getCurrentUser();
  const subscription = await getSubscription(user.id);

  if (!subscription?.active) {
    return (
      <p>Payment received. Your account is being activated — this usually takes a few seconds.</p>
    );
  }

  return <h1>Welcome to Pro</h1>;
}

الخطوة 7: اسمح للعملاء بإدارة اشتراكاتهم الخاصة

تتعامل بوابة الفواتير مع تغييرات الخطة والإلغاءات والفواتير وتحديثات البطاقة. إن بناء تلك التدفقات بنفسك يتطلب أسابيع من العمل لا تحتاج إلى القيام بها.

// app/api/portal/route.ts
import { NextResponse } from 'next/server';
import { stripe } from '@/lib/stripe';
import { getCurrentUser } from '@/lib/auth';
import { getCustomerId } from '@/lib/billing';

export async function POST() {
  const user = await getCurrentUser();
  if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });

  const customerId = await getCustomerId(user.id);
  if (!customerId) return NextResponse.json({ error: 'No subscription' }, { status: 400 });

  const session = await stripe.billingPortal.sessions.create({
    customer: customerId,
    return_url: `${process.env.NEXT_PUBLIC_BASE_URL}/account`,
  });

  return NextResponse.json({ url: session.url });
}

قائمة التحقق من البث المباشر

  • قم بتبديل مفاتيح الاختبار للمفاتيح المباشرة، وقم بإنشاءمنفصل نقطة نهاية webhook في الوضع المباشر مع سر التوقيع الخاص بها
  • تأكد من أن عنوان URL الخاص بخطاف الويب الخاص بك يمكن الوصول إليه بشكل عام ويعيد 2xx بسرعة – قم بالعمل البطيء بشكل غير متزامن
  • قم بتمكين ضريبة الشريط إذا كنت تبيع عبر نطاقات قضائية
  • تشغيل قواعد الرادار للحماية من الاحتيال
  • اختبر التدفق الكامل ببطاقة حقيقية، ثم قم برد المبلغ
  • تحقق من أن جدول Idempotency الخاص بك يحتوي على فهرس فريد في معرف الحدث

أخطاء شائعة

منح الوصول إلى صفحة النجاح. المستخدمون الذين لم يصلوا إليه مطلقًا يستمرون في الدفع، والمستخدمون الذين يزورونه مباشرة لم يفعلوا ذلك مطلقًا.

تحليل الجسم قبل التحقق من التوقيع. يحتاج التحقق إلى وحدات البايت الأولية. استخدمreq.text() ولا شيء آخر أولاً.

الثقة في السعر من العميل. ابحث دائمًا عن الأسعار من جانب الخادم حسب المعرف.

إرجاع 200 عند فشل المعالج. يتعامل Stripe مع 2xx على أنه نجاح ويتوقف عن إعادة المحاولة، وبالتالي يتم فقدان الحدث نهائيًا. قم بإرجاع 500 ودعها تعيد المحاولة.

إلغاء الوصول عند فشل الدفعة الأولى. تفشل البطاقات لأسباب مؤقتة. اتبع عملية مطالبة Stripe بدلاً من ذلك.

الخلاصة

يعتمد تكامل Stripe الصحيح على بعض القواعد:قم بإنشاء جلسات من جانب الخادم بأسعار تتحكم فيها، وتعامل مع خطاف الويب باعتباره المصدر الوحيد للحقيقة، وتحقق من التوقيعات مقابل النص الخام، واجعل التنفيذ غير فعال مع قيد فريد على معرف الحدث، وقم بإرجاع غير 2xx عند فشل المعالجة حتى يعيد Stripe المحاولة. قم بتسليم إدارة الاشتراك إلى بوابة الفوترة بدلاً من إعادة بنائها. احصل على هذه الأمور بشكل صحيح وستصبح المدفوعات هي الجزء الأقل حافلًا بالأحداث في طلبك.

MD Rafikul Islam

Written by

MD Rafikul Islam is a software developer and the editor of TechPulse. He writes about developer tooling, hardware, and the practical decisions that come up in day-to-day engineering work — which laptop to buy, which framework to commit to, why a build broke at 2am. He tests the tools he writes about and says plainly when something is not worth the money. Corrections and corrections requests are welcome at rony.yf25@gmail.com.

✍️ Leave a Comment

Your email address will not be published. Required fields are marked *

🌐 Read in:🇬🇧 English🇩🇪 Deutsch🇧🇷 Português🇸🇦 العربية🇮🇳 हिन्दी🇧🇩 বাংলা