أطلق واربح من عملك
تحقيق الدخل وأنظمة الدفع
5 دقيقة للقراءة
Effective monetization requires the right pricing model for your product. We'll implement subscription billing, usage metering, and customer management using Stripe.
Choosing a Pricing Model
// Pricing models and when to use them:
// 1. Flat-rate subscription
// Best for: Simple products, predictable value
// Example: $29/month for unlimited access
// 2. Tiered subscriptions
// Best for: Different user segments, feature gating
// Example: Free, Pro ($19), Team ($49)
// 3. Usage-based (pay-as-you-go)
// Best for: Variable usage, AI/API products
// Example: $0.01 per API call
// 4. Hybrid (base + usage)
// Best for: Predictable base with variable scaling
// Example: $29/month + $0.001 per request over 10k
// 5. Per-seat pricing
// Best for: B2B, team collaboration tools
// Example: $12/user/month
Stripe Product Setup
// lib/stripe/config.ts
import Stripe from 'stripe';
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2024-11-20.acacia',
typescript: true,
});
// Define your pricing configuration
export const PLANS = {
free: {
name: 'Free',
description: 'For individuals getting started',
priceId: null,
features: ['5 projects', '1,000 API calls/month', 'Community support'],
limits: {
projects: 5,
apiCalls: 1000,
teamMembers: 1,
},
},
pro: {
name: 'Pro',
description: 'For professionals and growing teams',
priceId: process.env.STRIPE_PRO_PRICE_ID!,
features: ['Unlimited projects', '50,000 API calls/month', 'Priority support', 'Advanced analytics'],
limits: {
projects: -1, // unlimited
apiCalls: 50000,
teamMembers: 5,
},
},
team: {
name: 'Team',
description: 'For organizations that need more',
priceId: process.env.STRIPE_TEAM_PRICE_ID!,
features: ['Everything in Pro', '500,000 API calls/month', 'SSO', 'Dedicated support', 'Custom integrations'],
limits: {
projects: -1,
apiCalls: 500000,
teamMembers: -1,
},
},
} as const;
export type PlanType = keyof typeof PLANS;
Creating Checkout Sessions
// app/api/stripe/checkout/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@/lib/auth';
import { stripe, PLANS, PlanType } from '@/lib/stripe/config';
import { db } from '@/lib/db';
import { users } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';
export async function POST(request: NextRequest) {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { plan } = await request.json() as { plan: PlanType };
if (!PLANS[plan] || !PLANS[plan].priceId) {
return NextResponse.json({ error: 'Invalid plan' }, { status: 400 });
}
// Get or create Stripe customer
const user = await db.query.users.findFirst({
where: eq(users.id, session.user.id),
});
let customerId = user?.stripeCustomerId;
if (!customerId) {
const customer = await stripe.customers.create({
email: session.user.email!,
name: session.user.name || undefined,
metadata: {
userId: session.user.id,
},
});
customerId = customer.id;
await db.update(users)
.set({ stripeCustomerId: customerId })
.where(eq(users.id, session.user.id));
}
// Create checkout session
const checkoutSession = await stripe.checkout.sessions.create({
customer: customerId,
mode: 'subscription',
payment_method_types: ['card'],
line_items: [
{
price: PLANS[plan].priceId,
quantity: 1,
},
],
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?checkout=success`,
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing?checkout=cancelled`,
subscription_data: {
metadata: {
userId: session.user.id,
plan,
},
},
allow_promotion_codes: true,
});
return NextResponse.json({ url: checkoutSession.url });
}
Handling Webhooks
// app/api/stripe/webhook/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { stripe } from '@/lib/stripe/config';
import { db } from '@/lib/db';
import { users, subscriptions } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';
import Stripe from 'stripe';
export async function POST(request: NextRequest) {
const body = await request.text();
const signature = request.headers.get('stripe-signature')!;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (err) {
console.error('Webhook signature verification failed:', err);
return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });
}
try {
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object as Stripe.Checkout.Session;
await handleCheckoutComplete(session);
break;
}
case 'customer.subscription.updated': {
const subscription = event.data.object as Stripe.Subscription;
await handleSubscriptionUpdate(subscription);
break;
}
case 'customer.subscription.deleted': {
const subscription = event.data.object as Stripe.Subscription;
await handleSubscriptionCanceled(subscription);
break;
}
case 'invoice.payment_failed': {
const invoice = event.data.object as Stripe.Invoice;
await handlePaymentFailed(invoice);
break;
}
case 'invoice.payment_succeeded': {
const invoice = event.data.object as Stripe.Invoice;
await handlePaymentSucceeded(invoice);
break;
}
}
return NextResponse.json({ received: true });
} catch (error) {
console.error('Webhook handler error:', error);
return NextResponse.json({ error: 'Webhook handler failed' }, { status: 500 });
}
}
async function handleCheckoutComplete(session: Stripe.Checkout.Session) {
const userId = session.metadata?.userId;
if (!userId) return;
const subscription = await stripe.subscriptions.retrieve(session.subscription as string);
await db.insert(subscriptions).values({
id: crypto.randomUUID(),
userId,
stripeSubscriptionId: subscription.id,
stripePriceId: subscription.items.data[0].price.id,
status: subscription.status,
currentPeriodStart: new Date(subscription.current_period_start * 1000),
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
cancelAtPeriodEnd: subscription.cancel_at_period_end,
});
// Update user's plan
await db.update(users)
.set({ plan: session.metadata?.plan || 'pro' })
.where(eq(users.id, userId));
}
async function handleSubscriptionUpdate(subscription: Stripe.Subscription) {
await db.update(subscriptions)
.set({
status: subscription.status,
stripePriceId: subscription.items.data[0].price.id,
currentPeriodStart: new Date(subscription.current_period_start * 1000),
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
cancelAtPeriodEnd: subscription.cancel_at_period_end,
})
.where(eq(subscriptions.stripeSubscriptionId, subscription.id));
}
async function handleSubscriptionCanceled(subscription: Stripe.Subscription) {
const sub = await db.query.subscriptions.findFirst({
where: eq(subscriptions.stripeSubscriptionId, subscription.id),
});
if (sub) {
await db.update(subscriptions)
.set({ status: 'canceled' })
.where(eq(subscriptions.id, sub.id));
await db.update(users)
.set({ plan: 'free' })
.where(eq(users.id, sub.userId));
}
}
async function handlePaymentFailed(invoice: Stripe.Invoice) {
// Send email notification about failed payment
const customerId = invoice.customer as string;
const customer = await stripe.customers.retrieve(customerId);
if ('email' in customer && customer.email) {
// await sendEmail({ to: customer.email, template: 'payment-failed' });
console.log('Payment failed for:', customer.email);
}
}
async function handlePaymentSucceeded(invoice: Stripe.Invoice) {
// Could log successful payments or send receipts
console.log('Payment succeeded:', invoice.id);
}
Customer Portal
// app/api/stripe/portal/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@/lib/auth';
import { stripe } from '@/lib/stripe/config';
import { db } from '@/lib/db';
import { users } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';
export async function POST(request: NextRequest) {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const user = await db.query.users.findFirst({
where: eq(users.id, session.user.id),
});
if (!user?.stripeCustomerId) {
return NextResponse.json({ error: 'No billing account' }, { status: 400 });
}
const portalSession = await stripe.billingPortal.sessions.create({
customer: user.stripeCustomerId,
return_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard/settings`,
});
return NextResponse.json({ url: portalSession.url });
}
Usage-Based Billing
// lib/stripe/usage.ts
import { stripe } from './config';
import { db } from '@/lib/db';
import { usageRecords, subscriptions } from '@/lib/db/schema';
import { eq, and, gte } from 'drizzle-orm';
export async function trackUsage(userId: string, type: string, quantity: number = 1) {
// Record locally for analytics
await db.insert(usageRecords).values({
id: crypto.randomUUID(),
userId,
type,
quantity,
timestamp: new Date(),
});
// Report to Stripe for metered billing
const subscription = await db.query.subscriptions.findFirst({
where: eq(subscriptions.userId, userId),
});
if (subscription?.stripeSubscriptionId) {
const stripeSubscription = await stripe.subscriptions.retrieve(subscription.stripeSubscriptionId);
// Find the metered price item
const meteredItem = stripeSubscription.items.data.find(
(item) => item.price.recurring?.usage_type === 'metered'
);
if (meteredItem) {
await stripe.subscriptionItems.createUsageRecord(meteredItem.id, {
quantity,
timestamp: Math.floor(Date.now() / 1000),
action: 'increment',
});
}
}
}
// Check if user is within their plan limits
export async function checkUsageLimit(userId: string, type: string, limit: number): Promise<boolean> {
const startOfMonth = new Date();
startOfMonth.setDate(1);
startOfMonth.setHours(0, 0, 0, 0);
const usage = await db.query.usageRecords.findMany({
where: and(
eq(usageRecords.userId, userId),
eq(usageRecords.type, type),
gte(usageRecords.timestamp, startOfMonth)
),
});
const totalUsage = usage.reduce((sum, record) => sum + record.quantity, 0);
return limit === -1 || totalUsage < limit; // -1 means unlimited
}
Usage Tracking Middleware
// middleware/track-usage.ts
import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@/lib/auth';
import { trackUsage, checkUsageLimit } from '@/lib/stripe/usage';
import { PLANS } from '@/lib/stripe/config';
export async function trackApiUsage(request: NextRequest) {
const session = await auth();
if (!session?.user?.id) return NextResponse.next();
const user = session.user as { id: string; plan: string };
const plan = PLANS[user.plan as keyof typeof PLANS] || PLANS.free;
// Check if within limits
const withinLimit = await checkUsageLimit(user.id, 'api_call', plan.limits.apiCalls);
if (!withinLimit) {
return NextResponse.json(
{ error: 'Usage limit exceeded. Please upgrade your plan.' },
{ status: 429 }
);
}
// Track the usage
await trackUsage(user.id, 'api_call', 1);
return NextResponse.next();
}
Pricing Page Component
// components/pricing/pricing-page.tsx
'use client';
import { useState } from 'react';
import { PLANS, PlanType } from '@/lib/stripe/config';
import { useSession } from 'next-auth/react';
export function PricingPage() {
const { data: session } = useSession();
const [loading, setLoading] = useState<PlanType | null>(null);
const [billingCycle, setBillingCycle] = useState<'monthly' | 'yearly'>('monthly');
const handleSubscribe = async (plan: PlanType) => {
if (!session) {
window.location.href = '/auth/signin?callbackUrl=/pricing';
return;
}
setLoading(plan);
try {
const response = await fetch('/api/stripe/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ plan, billingCycle }),
});
const { url } = await response.json();
window.location.href = url;
} catch (error) {
console.error('Checkout error:', error);
} finally {
setLoading(null);
}
};
return (
<div className="py-12">
<div className="text-center mb-12">
<h1 className="text-4xl font-bold mb-4">Simple, Transparent Pricing</h1>
<p className="text-gray-600 max-w-2xl mx-auto">
Choose the plan that fits your needs. All plans include a 14-day free trial.
</p>
{/* Billing cycle toggle */}
<div className="mt-6 inline-flex items-center bg-gray-100 rounded-lg p-1">
<button
onClick={() => setBillingCycle('monthly')}
className={`px-4 py-2 rounded-lg ${billingCycle === 'monthly' ? 'bg-white shadow' : ''}`}
>
Monthly
</button>
<button
onClick={() => setBillingCycle('yearly')}
className={`px-4 py-2 rounded-lg ${billingCycle === 'yearly' ? 'bg-white shadow' : ''}`}
>
Yearly <span className="text-green-600 text-sm">Save 20%</span>
</button>
</div>
</div>
<div className="grid md:grid-cols-3 gap-8 max-w-6xl mx-auto px-4">
{(Object.entries(PLANS) as [PlanType, typeof PLANS[PlanType]][]).map(([key, plan]) => (
<div
key={key}
className={`border rounded-xl p-8 ${key === 'pro' ? 'border-blue-500 shadow-lg scale-105' : ''}`}
>
{key === 'pro' && (
<div className="text-blue-500 font-medium text-sm mb-4">MOST POPULAR</div>
)}
<h3 className="text-2xl font-bold">{plan.name}</h3>
<p className="text-gray-600 mt-2">{plan.description}</p>
<div className="mt-6">
{key === 'free' ? (
<span className="text-4xl font-bold">$0</span>
) : (
<>
<span className="text-4xl font-bold">
${key === 'pro' ? (billingCycle === 'yearly' ? 15 : 19) : (billingCycle === 'yearly' ? 39 : 49)}
</span>
<span className="text-gray-500">/month</span>
</>
)}
</div>
<ul className="mt-8 space-y-4">
{plan.features.map((feature) => (
<li key={feature} className="flex items-center gap-2">
<svg className="w-5 h-5 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
{feature}
</li>
))}
</ul>
<button
onClick={() => key !== 'free' && handleSubscribe(key)}
disabled={loading === key || key === 'free'}
className={`w-full mt-8 py-3 rounded-lg font-medium ${
key === 'pro'
? 'bg-blue-500 text-white hover:bg-blue-600'
: key === 'free'
? 'bg-gray-100 text-gray-500 cursor-not-allowed'
: 'bg-gray-900 text-white hover:bg-gray-800'
}`}
>
{loading === key ? 'Loading...' : key === 'free' ? 'Current Plan' : 'Get Started'}
</button>
</div>
))}
</div>
</div>
);
}
Revenue Dashboard
// app/api/admin/revenue/route.ts
import { NextResponse } from 'next/server';
import { stripe } from '@/lib/stripe/config';
import { auth } from '@/lib/auth';
export async function GET() {
const session = await auth();
if (session?.user?.role !== 'admin') {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
// Get revenue metrics from Stripe
const now = new Date();
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
const [balance, charges, subscriptions] = await Promise.all([
stripe.balance.retrieve(),
stripe.charges.list({
created: { gte: Math.floor(startOfMonth.getTime() / 1000) },
limit: 100,
}),
stripe.subscriptions.list({
status: 'active',
limit: 100,
}),
]);
const mrr = subscriptions.data.reduce((sum, sub) => {
const price = sub.items.data[0]?.price;
if (price?.recurring?.interval === 'month') {
return sum + (price.unit_amount || 0);
} else if (price?.recurring?.interval === 'year') {
return sum + (price.unit_amount || 0) / 12;
}
return sum;
}, 0) / 100;
const revenue = charges.data
.filter((charge) => charge.status === 'succeeded')
.reduce((sum, charge) => sum + charge.amount, 0) / 100;
return NextResponse.json({
mrr,
revenue,
activeSubscriptions: subscriptions.data.length,
balance: balance.available.reduce((sum, b) => sum + b.amount, 0) / 100,
});
}
What You've Learned
In this lesson, you've built:
- Pricing models - Subscription, usage-based, and hybrid billing
- Stripe integration - Checkout, webhooks, and customer portal
- Usage tracking - Metered billing and limit enforcement
- Pricing UI - Interactive pricing page with plan comparison
- Revenue tracking - Admin dashboard for financial metrics
Proper payment integration is critical for SaaS success. Stripe handles the complexity of recurring billing, compliance, and global payments.
تحقيق الدخل وأنظمة الدفع
تحقيق الدخل الفعال يتطلب نموذج التسعير المناسب لمنتجك. سننفذ فوترة الاشتراكات وقياس الاستخدام وإدارة العملاء باستخدام Stripe.
اختيار نموذج التسعير
// نماذج التسعير ومتى تستخدمها:
// 1. اشتراك بسعر ثابت
// الأفضل لـ: المنتجات البسيطة، القيمة المتوقعة
// مثال: $29/شهر للوصول غير المحدود
// 2. اشتراكات متدرجة
// الأفضل لـ: شرائح مستخدمين مختلفة، تقييد الميزات
// مثال: مجاني، احترافي ($19)، فريق ($49)
// 3. قائم على الاستخدام (ادفع حسب الاستخدام)
// الأفضل لـ: الاستخدام المتغير، منتجات AI/API
// مثال: $0.01 لكل استدعاء API
// 4. هجين (قاعدة + استخدام)
// الأفضل لـ: قاعدة متوقعة مع توسع متغير
// مثال: $29/شهر + $0.001 لكل طلب فوق 10 آلاف
// 5. تسعير لكل مقعد
// الأفضل لـ: B2B، أدوات التعاون الجماعي
// مثال: $12/مستخدم/شهر
إعداد منتج Stripe
// lib/stripe/config.ts
import Stripe from 'stripe';
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2024-11-20.acacia',
typescript: true,
});
// تعريف تكوين التسعير
export const PLANS = {
free: {
name: 'مجاني',
description: 'للأفراد الذين يبدأون',
priceId: null,
features: ['5 مشاريع', '1,000 استدعاء API/شهر', 'دعم المجتمع'],
limits: {
projects: 5,
apiCalls: 1000,
teamMembers: 1,
},
},
pro: {
name: 'احترافي',
description: 'للمحترفين والفرق النامية',
priceId: process.env.STRIPE_PRO_PRICE_ID!,
features: ['مشاريع غير محدودة', '50,000 استدعاء API/شهر', 'دعم أولوية', 'تحليلات متقدمة'],
limits: {
projects: -1, // غير محدود
apiCalls: 50000,
teamMembers: 5,
},
},
team: {
name: 'فريق',
description: 'للمؤسسات التي تحتاج المزيد',
priceId: process.env.STRIPE_TEAM_PRICE_ID!,
features: ['كل شيء في الاحترافي', '500,000 استدعاء API/شهر', 'SSO', 'دعم مخصص', 'تكاملات مخصصة'],
limits: {
projects: -1,
apiCalls: 500000,
teamMembers: -1,
},
},
} as const;
export type PlanType = keyof typeof PLANS;
إنشاء جلسات الدفع
// app/api/stripe/checkout/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@/lib/auth';
import { stripe, PLANS, PlanType } from '@/lib/stripe/config';
import { db } from '@/lib/db';
import { users } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';
export async function POST(request: NextRequest) {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ error: 'غير مصرح' }, { status: 401 });
}
const { plan } = await request.json() as { plan: PlanType };
if (!PLANS[plan] || !PLANS[plan].priceId) {
return NextResponse.json({ error: 'خطة غير صالحة' }, { status: 400 });
}
// الحصول على أو إنشاء عميل Stripe
const user = await db.query.users.findFirst({
where: eq(users.id, session.user.id),
});
let customerId = user?.stripeCustomerId;
if (!customerId) {
const customer = await stripe.customers.create({
email: session.user.email!,
name: session.user.name || undefined,
metadata: {
userId: session.user.id,
},
});
customerId = customer.id;
await db.update(users)
.set({ stripeCustomerId: customerId })
.where(eq(users.id, session.user.id));
}
// إنشاء جلسة الدفع
const checkoutSession = await stripe.checkout.sessions.create({
customer: customerId,
mode: 'subscription',
payment_method_types: ['card'],
line_items: [
{
price: PLANS[plan].priceId,
quantity: 1,
},
],
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?checkout=success`,
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing?checkout=cancelled`,
subscription_data: {
metadata: {
userId: session.user.id,
plan,
},
},
allow_promotion_codes: true,
});
return NextResponse.json({ url: checkoutSession.url });
}
التعامل مع Webhooks
// app/api/stripe/webhook/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { stripe } from '@/lib/stripe/config';
import { db } from '@/lib/db';
import { users, subscriptions } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';
import Stripe from 'stripe';
export async function POST(request: NextRequest) {
const body = await request.text();
const signature = request.headers.get('stripe-signature')!;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (err) {
console.error('فشل التحقق من توقيع Webhook:', err);
return NextResponse.json({ error: 'توقيع غير صالح' }, { status: 400 });
}
try {
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object as Stripe.Checkout.Session;
await handleCheckoutComplete(session);
break;
}
case 'customer.subscription.updated': {
const subscription = event.data.object as Stripe.Subscription;
await handleSubscriptionUpdate(subscription);
break;
}
case 'customer.subscription.deleted': {
const subscription = event.data.object as Stripe.Subscription;
await handleSubscriptionCanceled(subscription);
break;
}
case 'invoice.payment_failed': {
const invoice = event.data.object as Stripe.Invoice;
await handlePaymentFailed(invoice);
break;
}
case 'invoice.payment_succeeded': {
const invoice = event.data.object as Stripe.Invoice;
await handlePaymentSucceeded(invoice);
break;
}
}
return NextResponse.json({ received: true });
} catch (error) {
console.error('خطأ معالج Webhook:', error);
return NextResponse.json({ error: 'فشل معالج Webhook' }, { status: 500 });
}
}
async function handleCheckoutComplete(session: Stripe.Checkout.Session) {
const userId = session.metadata?.userId;
if (!userId) return;
const subscription = await stripe.subscriptions.retrieve(session.subscription as string);
await db.insert(subscriptions).values({
id: crypto.randomUUID(),
userId,
stripeSubscriptionId: subscription.id,
stripePriceId: subscription.items.data[0].price.id,
status: subscription.status,
currentPeriodStart: new Date(subscription.current_period_start * 1000),
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
cancelAtPeriodEnd: subscription.cancel_at_period_end,
});
// تحديث خطة المستخدم
await db.update(users)
.set({ plan: session.metadata?.plan || 'pro' })
.where(eq(users.id, userId));
}
async function handleSubscriptionUpdate(subscription: Stripe.Subscription) {
await db.update(subscriptions)
.set({
status: subscription.status,
stripePriceId: subscription.items.data[0].price.id,
currentPeriodStart: new Date(subscription.current_period_start * 1000),
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
cancelAtPeriodEnd: subscription.cancel_at_period_end,
})
.where(eq(subscriptions.stripeSubscriptionId, subscription.id));
}
async function handleSubscriptionCanceled(subscription: Stripe.Subscription) {
const sub = await db.query.subscriptions.findFirst({
where: eq(subscriptions.stripeSubscriptionId, subscription.id),
});
if (sub) {
await db.update(subscriptions)
.set({ status: 'canceled' })
.where(eq(subscriptions.id, sub.id));
await db.update(users)
.set({ plan: 'free' })
.where(eq(users.id, sub.userId));
}
}
async function handlePaymentFailed(invoice: Stripe.Invoice) {
// إرسال إشعار بريد إلكتروني عن فشل الدفع
const customerId = invoice.customer as string;
const customer = await stripe.customers.retrieve(customerId);
if ('email' in customer && customer.email) {
// await sendEmail({ to: customer.email, template: 'payment-failed' });
console.log('فشل الدفع لـ:', customer.email);
}
}
async function handlePaymentSucceeded(invoice: Stripe.Invoice) {
// يمكن تسجيل المدفوعات الناجحة أو إرسال الإيصالات
console.log('نجح الدفع:', invoice.id);
}
بوابة العملاء
// app/api/stripe/portal/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@/lib/auth';
import { stripe } from '@/lib/stripe/config';
import { db } from '@/lib/db';
import { users } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';
export async function POST(request: NextRequest) {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ error: 'غير مصرح' }, { status: 401 });
}
const user = await db.query.users.findFirst({
where: eq(users.id, session.user.id),
});
if (!user?.stripeCustomerId) {
return NextResponse.json({ error: 'لا يوجد حساب فوترة' }, { status: 400 });
}
const portalSession = await stripe.billingPortal.sessions.create({
customer: user.stripeCustomerId,
return_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard/settings`,
});
return NextResponse.json({ url: portalSession.url });
}
الفوترة القائمة على الاستخدام
// lib/stripe/usage.ts
import { stripe } from './config';
import { db } from '@/lib/db';
import { usageRecords, subscriptions } from '@/lib/db/schema';
import { eq, and, gte } from 'drizzle-orm';
export async function trackUsage(userId: string, type: string, quantity: number = 1) {
// التسجيل محلياً للتحليلات
await db.insert(usageRecords).values({
id: crypto.randomUUID(),
userId,
type,
quantity,
timestamp: new Date(),
});
// الإبلاغ لـ Stripe للفوترة المقاسة
const subscription = await db.query.subscriptions.findFirst({
where: eq(subscriptions.userId, userId),
});
if (subscription?.stripeSubscriptionId) {
const stripeSubscription = await stripe.subscriptions.retrieve(subscription.stripeSubscriptionId);
// إيجاد عنصر السعر المقاس
const meteredItem = stripeSubscription.items.data.find(
(item) => item.price.recurring?.usage_type === 'metered'
);
if (meteredItem) {
await stripe.subscriptionItems.createUsageRecord(meteredItem.id, {
quantity,
timestamp: Math.floor(Date.now() / 1000),
action: 'increment',
});
}
}
}
// التحقق مما إذا كان المستخدم ضمن حدود خطته
export async function checkUsageLimit(userId: string, type: string, limit: number): Promise<boolean> {
const startOfMonth = new Date();
startOfMonth.setDate(1);
startOfMonth.setHours(0, 0, 0, 0);
const usage = await db.query.usageRecords.findMany({
where: and(
eq(usageRecords.userId, userId),
eq(usageRecords.type, type),
gte(usageRecords.timestamp, startOfMonth)
),
});
const totalUsage = usage.reduce((sum, record) => sum + record.quantity, 0);
return limit === -1 || totalUsage < limit; // -1 تعني غير محدود
}
وسيط تتبع الاستخدام
// middleware/track-usage.ts
import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@/lib/auth';
import { trackUsage, checkUsageLimit } from '@/lib/stripe/usage';
import { PLANS } from '@/lib/stripe/config';
export async function trackApiUsage(request: NextRequest) {
const session = await auth();
if (!session?.user?.id) return NextResponse.next();
const user = session.user as { id: string; plan: string };
const plan = PLANS[user.plan as keyof typeof PLANS] || PLANS.free;
// التحقق مما إذا كان ضمن الحدود
const withinLimit = await checkUsageLimit(user.id, 'api_call', plan.limits.apiCalls);
if (!withinLimit) {
return NextResponse.json(
{ error: 'تم تجاوز حد الاستخدام. يرجى ترقية خطتك.' },
{ status: 429 }
);
}
// تتبع الاستخدام
await trackUsage(user.id, 'api_call', 1);
return NextResponse.next();
}
مكون صفحة التسعير
// components/pricing/pricing-page.tsx
'use client';
import { useState } from 'react';
import { PLANS, PlanType } from '@/lib/stripe/config';
import { useSession } from 'next-auth/react';
export function PricingPage() {
const { data: session } = useSession();
const [loading, setLoading] = useState<PlanType | null>(null);
const [billingCycle, setBillingCycle] = useState<'monthly' | 'yearly'>('monthly');
const handleSubscribe = async (plan: PlanType) => {
if (!session) {
window.location.href = '/auth/signin?callbackUrl=/pricing';
return;
}
setLoading(plan);
try {
const response = await fetch('/api/stripe/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ plan, billingCycle }),
});
const { url } = await response.json();
window.location.href = url;
} catch (error) {
console.error('خطأ الدفع:', error);
} finally {
setLoading(null);
}
};
return (
<div className="py-12">
<div className="text-center mb-12">
<h1 className="text-4xl font-bold mb-4">تسعير بسيط وشفاف</h1>
<p className="text-gray-600 max-w-2xl mx-auto">
اختر الخطة التي تناسب احتياجاتك. جميع الخطط تتضمن فترة تجريبية مجانية 14 يوماً.
</p>
{/* مبدل دورة الفوترة */}
<div className="mt-6 inline-flex items-center bg-gray-100 rounded-lg p-1">
<button
onClick={() => setBillingCycle('monthly')}
className={`px-4 py-2 rounded-lg ${billingCycle === 'monthly' ? 'bg-white shadow' : ''}`}
>
شهري
</button>
<button
onClick={() => setBillingCycle('yearly')}
className={`px-4 py-2 rounded-lg ${billingCycle === 'yearly' ? 'bg-white shadow' : ''}`}
>
سنوي <span className="text-green-600 text-sm">وفر 20%</span>
</button>
</div>
</div>
<div className="grid md:grid-cols-3 gap-8 max-w-6xl mx-auto px-4">
{(Object.entries(PLANS) as [PlanType, typeof PLANS[PlanType]][]).map(([key, plan]) => (
<div
key={key}
className={`border rounded-xl p-8 ${key === 'pro' ? 'border-blue-500 shadow-lg scale-105' : ''}`}
>
{key === 'pro' && (
<div className="text-blue-500 font-medium text-sm mb-4">الأكثر شيوعاً</div>
)}
<h3 className="text-2xl font-bold">{plan.name}</h3>
<p className="text-gray-600 mt-2">{plan.description}</p>
<div className="mt-6">
{key === 'free' ? (
<span className="text-4xl font-bold">$0</span>
) : (
<>
<span className="text-4xl font-bold">
${key === 'pro' ? (billingCycle === 'yearly' ? 15 : 19) : (billingCycle === 'yearly' ? 39 : 49)}
</span>
<span className="text-gray-500">/شهر</span>
</>
)}
</div>
<ul className="mt-8 space-y-4">
{plan.features.map((feature) => (
<li key={feature} className="flex items-center gap-2">
<svg className="w-5 h-5 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
{feature}
</li>
))}
</ul>
<button
onClick={() => key !== 'free' && handleSubscribe(key)}
disabled={loading === key || key === 'free'}
className={`w-full mt-8 py-3 rounded-lg font-medium ${
key === 'pro'
? 'bg-blue-500 text-white hover:bg-blue-600'
: key === 'free'
? 'bg-gray-100 text-gray-500 cursor-not-allowed'
: 'bg-gray-900 text-white hover:bg-gray-800'
}`}
>
{loading === key ? 'جاري التحميل...' : key === 'free' ? 'الخطة الحالية' : 'ابدأ الآن'}
</button>
</div>
))}
</div>
</div>
);
}
لوحة معلومات الإيرادات
// app/api/admin/revenue/route.ts
import { NextResponse } from 'next/server';
import { stripe } from '@/lib/stripe/config';
import { auth } from '@/lib/auth';
export async function GET() {
const session = await auth();
if (session?.user?.role !== 'admin') {
return NextResponse.json({ error: 'ممنوع' }, { status: 403 });
}
// الحصول على مقاييس الإيرادات من Stripe
const now = new Date();
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
const [balance, charges, subscriptions] = await Promise.all([
stripe.balance.retrieve(),
stripe.charges.list({
created: { gte: Math.floor(startOfMonth.getTime() / 1000) },
limit: 100,
}),
stripe.subscriptions.list({
status: 'active',
limit: 100,
}),
]);
const mrr = subscriptions.data.reduce((sum, sub) => {
const price = sub.items.data[0]?.price;
if (price?.recurring?.interval === 'month') {
return sum + (price.unit_amount || 0);
} else if (price?.recurring?.interval === 'year') {
return sum + (price.unit_amount || 0) / 12;
}
return sum;
}, 0) / 100;
const revenue = charges.data
.filter((charge) => charge.status === 'succeeded')
.reduce((sum, charge) => sum + charge.amount, 0) / 100;
return NextResponse.json({
mrr,
revenue,
activeSubscriptions: subscriptions.data.length,
balance: balance.available.reduce((sum, b) => sum + b.amount, 0) / 100,
});
}
ما تعلمته
في هذا الدرس، بنيت:
- نماذج التسعير - اشتراك وقائم على الاستخدام وفوترة هجينة
- تكامل Stripe - الدفع والويب هوكس وبوابة العملاء
- تتبع الاستخدام - الفوترة المقاسة وفرض الحدود
- واجهة التسعير - صفحة تسعير تفاعلية مع مقارنة الخطط
- تتبع الإيرادات - لوحة معلومات إدارية للمقاييس المالية
تكامل الدفع المناسب أساسي لنجاح SaaS. Stripe يتعامل مع تعقيدات الفوترة المتكررة والامتثال والمدفوعات العالمية.