Add Stripe subscription integration with pricing plans
This commit is contained in:
parent
157e53af83
commit
bccf771ffc
|
|
@ -20,12 +20,25 @@ model User {
|
|||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// Stripe subscription fields
|
||||
stripeCustomerId String? @unique
|
||||
stripeSubscriptionId String? @unique
|
||||
stripePriceId String?
|
||||
stripeCurrentPeriodEnd DateTime?
|
||||
plan Plan @default(FREE)
|
||||
|
||||
qrCodes QRCode[]
|
||||
integrations Integration[]
|
||||
accounts Account[]
|
||||
sessions Session[]
|
||||
}
|
||||
|
||||
enum Plan {
|
||||
FREE
|
||||
PRO
|
||||
BUSINESS
|
||||
}
|
||||
|
||||
model Account {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import { StatsGrid } from '@/components/dashboard/StatsGrid';
|
||||
import { QRCodeCard } from '@/components/dashboard/QRCodeCard';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
||||
|
|
@ -23,8 +24,10 @@ interface QRCodeData {
|
|||
|
||||
export default function DashboardPage() {
|
||||
const { t } = useTranslation();
|
||||
const { data: session } = useSession();
|
||||
const [qrCodes, setQrCodes] = useState<QRCodeData[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [userPlan, setUserPlan] = useState<string>('FREE');
|
||||
const [stats, setStats] = useState({
|
||||
totalScans: 0,
|
||||
activeQRCodes: 0,
|
||||
|
|
@ -116,12 +119,13 @@ export default function DashboardPage() {
|
|||
];
|
||||
|
||||
useEffect(() => {
|
||||
// Load real QR codes from API
|
||||
// Load real QR codes and user plan from API
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/qrs');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
// Fetch QR codes
|
||||
const qrResponse = await fetch('/api/qrs');
|
||||
if (qrResponse.ok) {
|
||||
const data = await qrResponse.json();
|
||||
setQrCodes(data);
|
||||
|
||||
// Calculate real stats
|
||||
|
|
@ -143,6 +147,15 @@ export default function DashboardPage() {
|
|||
conversionRate: 0,
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch user plan
|
||||
if (session?.user?.email) {
|
||||
const userResponse = await fetch('/api/user/plan');
|
||||
if (userResponse.ok) {
|
||||
const userData = await userResponse.json();
|
||||
setUserPlan(userData.plan || 'FREE');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
setQrCodes([]);
|
||||
|
|
@ -157,7 +170,7 @@ export default function DashboardPage() {
|
|||
};
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
}, [session]);
|
||||
|
||||
const handleEdit = (id: string) => {
|
||||
console.log('Edit QR:', id);
|
||||
|
|
@ -175,12 +188,48 @@ export default function DashboardPage() {
|
|||
console.log('Delete QR:', id);
|
||||
};
|
||||
|
||||
const getPlanBadgeColor = (plan: string) => {
|
||||
switch (plan) {
|
||||
case 'PRO':
|
||||
return 'info';
|
||||
case 'BUSINESS':
|
||||
return 'warning';
|
||||
default:
|
||||
return 'default';
|
||||
}
|
||||
};
|
||||
|
||||
const getPlanEmoji = (plan: string) => {
|
||||
switch (plan) {
|
||||
case 'FREE':
|
||||
return '🟢';
|
||||
case 'PRO':
|
||||
return '🔵';
|
||||
case 'BUSINESS':
|
||||
return '🟣';
|
||||
default:
|
||||
return '⚪';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">{t('dashboard.title')}</h1>
|
||||
<p className="text-gray-600 mt-2">{t('dashboard.subtitle')}</p>
|
||||
{/* Header with Plan Badge */}
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">{t('dashboard.title')}</h1>
|
||||
<p className="text-gray-600 mt-2">{t('dashboard.subtitle')}</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<Badge variant={getPlanBadgeColor(userPlan)} className="text-lg px-4 py-2">
|
||||
{getPlanEmoji(userPlan)} {userPlan} Plan
|
||||
</Badge>
|
||||
{userPlan === 'FREE' && (
|
||||
<Link href="/pricing">
|
||||
<Button variant="primary">Upgrade</Button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Grid */}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,15 @@ export default function AppLayout({
|
|||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Pricing',
|
||||
href: '/pricing',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -0,0 +1,272 @@
|
|||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { showToast } from '@/components/ui/Toast';
|
||||
|
||||
export default function PricingPage() {
|
||||
const router = useRouter();
|
||||
const { data: session } = useSession();
|
||||
const [loading, setLoading] = useState<string | null>(null);
|
||||
const [billingInterval, setBillingInterval] = useState<'monthly' | 'yearly'>('monthly');
|
||||
|
||||
const plans = [
|
||||
{
|
||||
id: 'FREE',
|
||||
name: 'Free / Starter',
|
||||
icon: '🟢',
|
||||
price: 0,
|
||||
priceYearly: 0,
|
||||
description: 'Privatnutzer & Testkunden',
|
||||
features: [
|
||||
'3 dynamische QR-Codes',
|
||||
'Basis-Tracking (Scans + Standort)',
|
||||
'Einfache Designs',
|
||||
'Unbegrenzte statische QR-Codes',
|
||||
],
|
||||
cta: 'Get Started',
|
||||
popular: false,
|
||||
priceIdMonthly: null,
|
||||
priceIdYearly: null,
|
||||
},
|
||||
{
|
||||
id: 'PRO',
|
||||
name: 'Pro',
|
||||
icon: '🔵',
|
||||
price: 9,
|
||||
priceYearly: 90,
|
||||
description: 'Selbstständige / kleine Firmen',
|
||||
features: [
|
||||
'50 QR-Codes',
|
||||
'Branding (Logo, Farben)',
|
||||
'Detaillierte Analytics',
|
||||
'CSV-Export',
|
||||
'Passwortschutz',
|
||||
],
|
||||
cta: 'Upgrade to Pro',
|
||||
popular: true,
|
||||
priceIdMonthly: process.env.NEXT_PUBLIC_STRIPE_PRICE_ID_PRO_MONTHLY,
|
||||
priceIdYearly: process.env.NEXT_PUBLIC_STRIPE_PRICE_ID_PRO_YEARLY,
|
||||
},
|
||||
{
|
||||
id: 'BUSINESS',
|
||||
name: 'Business',
|
||||
icon: '🟣',
|
||||
price: 29,
|
||||
priceYearly: 290,
|
||||
description: 'Agenturen / Startups',
|
||||
features: [
|
||||
'500 QR-Codes',
|
||||
'Team-Zugänge (bis 3 User)',
|
||||
'API-Zugang',
|
||||
'Benutzerdefinierte Domains',
|
||||
'White-Label',
|
||||
'Prioritäts-Support',
|
||||
],
|
||||
cta: 'Upgrade to Business',
|
||||
popular: false,
|
||||
priceIdMonthly: process.env.NEXT_PUBLIC_STRIPE_PRICE_ID_BUSINESS_MONTHLY,
|
||||
priceIdYearly: process.env.NEXT_PUBLIC_STRIPE_PRICE_ID_BUSINESS_YEARLY,
|
||||
},
|
||||
];
|
||||
|
||||
const handleSubscribe = async (planId: string, priceId: string | null | undefined) => {
|
||||
if (!session) {
|
||||
router.push('/login?redirect=/pricing');
|
||||
return;
|
||||
}
|
||||
|
||||
if (planId === 'FREE') {
|
||||
showToast('Sie nutzen bereits den kostenlosen Plan!', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!priceId) {
|
||||
showToast('Preisdetails nicht verfügbar', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(planId);
|
||||
|
||||
const response = await fetch('/api/stripe/checkout', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
priceId,
|
||||
plan: planId,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok && data.url) {
|
||||
window.location.href = data.url;
|
||||
} else {
|
||||
showToast(data.error || 'Fehler beim Erstellen der Checkout-Session', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating checkout session:', error);
|
||||
showToast('Ein Fehler ist aufgetreten', 'error');
|
||||
} finally {
|
||||
setLoading(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-12">
|
||||
<div className="text-center mb-12">
|
||||
<h1 className="text-4xl lg:text-5xl font-bold text-gray-900 mb-4">
|
||||
Wählen Sie Ihren Plan
|
||||
</h1>
|
||||
<p className="text-xl text-gray-600 mb-8">
|
||||
Starten Sie kostenlos. Upgraden Sie jederzeit.
|
||||
</p>
|
||||
|
||||
{/* Billing Toggle */}
|
||||
<div className="inline-flex items-center space-x-4 bg-gray-100 p-1 rounded-lg">
|
||||
<button
|
||||
onClick={() => setBillingInterval('monthly')}
|
||||
className={`px-6 py-2 rounded-md font-medium transition-colors ${
|
||||
billingInterval === 'monthly'
|
||||
? 'bg-white text-gray-900 shadow'
|
||||
: 'text-gray-600'
|
||||
}`}
|
||||
>
|
||||
Monatlich
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setBillingInterval('yearly')}
|
||||
className={`px-6 py-2 rounded-md font-medium transition-colors ${
|
||||
billingInterval === 'yearly'
|
||||
? 'bg-white text-gray-900 shadow'
|
||||
: 'text-gray-600'
|
||||
}`}
|
||||
>
|
||||
Jährlich
|
||||
<Badge variant="success" className="ml-2">
|
||||
Spare 17%
|
||||
</Badge>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-8 max-w-6xl mx-auto">
|
||||
{plans.map((plan) => {
|
||||
const price = billingInterval === 'yearly' ? plan.priceYearly : plan.price;
|
||||
const priceId =
|
||||
billingInterval === 'yearly' ? plan.priceIdYearly : plan.priceIdMonthly;
|
||||
const isLoading = loading === plan.id;
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={plan.id}
|
||||
className={`relative ${
|
||||
plan.popular
|
||||
? 'border-primary-500 border-2 shadow-xl scale-105'
|
||||
: 'border-gray-200'
|
||||
}`}
|
||||
>
|
||||
{plan.popular && (
|
||||
<div className="absolute -top-4 left-1/2 transform -translate-x-1/2">
|
||||
<Badge variant="info" className="px-4 py-1 text-sm">
|
||||
Beliebteste Wahl
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CardHeader className="text-center pb-6">
|
||||
<div className="text-4xl mb-4">{plan.icon}</div>
|
||||
<CardTitle className="text-2xl mb-2">{plan.name}</CardTitle>
|
||||
<p className="text-sm text-gray-600">{plan.description}</p>
|
||||
|
||||
<div className="mt-6">
|
||||
<div className="flex items-baseline justify-center">
|
||||
<span className="text-5xl font-bold">{price}€</span>
|
||||
<span className="text-gray-600 ml-2">
|
||||
/{billingInterval === 'yearly' ? 'Jahr' : 'Monat'}
|
||||
</span>
|
||||
</div>
|
||||
{billingInterval === 'yearly' && plan.price > 0 && (
|
||||
<p className="text-sm text-gray-500 mt-2">
|
||||
{(price / 12).toFixed(2)}€ pro Monat
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-6">
|
||||
<ul className="space-y-3">
|
||||
{plan.features.map((feature, index) => (
|
||||
<li key={index} className="flex items-start space-x-3">
|
||||
<svg
|
||||
className="w-5 h-5 text-success-500 flex-shrink-0 mt-0.5"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<span className="text-gray-700">{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<Button
|
||||
variant={plan.popular ? 'primary' : 'outline'}
|
||||
className="w-full"
|
||||
size="lg"
|
||||
onClick={() => handleSubscribe(plan.id, priceId)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? 'Lädt...' : plan.cta}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* FAQ Section */}
|
||||
<div className="mt-20 max-w-3xl mx-auto">
|
||||
<h2 className="text-3xl font-bold text-center mb-8">Häufige Fragen</h2>
|
||||
<div className="space-y-4">
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<h3 className="font-semibold mb-2">Kann ich jederzeit kündigen?</h3>
|
||||
<p className="text-gray-600">
|
||||
Ja, Sie können Ihr Abo jederzeit kündigen. Es läuft dann bis zum Ende des
|
||||
bezahlten Zeitraums weiter.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<h3 className="font-semibold mb-2">Welche Zahlungsmethoden akzeptieren Sie?</h3>
|
||||
<p className="text-gray-600">
|
||||
Wir akzeptieren alle gängigen Kreditkarten und SEPA-Lastschrift über Stripe.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<h3 className="font-semibold mb-2">Was passiert mit meinen QR-Codes bei Downgrade?</h3>
|
||||
<p className="text-gray-600">
|
||||
Ihre QR-Codes bleiben erhalten, Sie können nur keine neuen mehr erstellen, wenn das Limit erreicht ist.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/lib/auth';
|
||||
import { stripe } from '@/lib/stripe';
|
||||
import { db } from '@/lib/db';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { priceId, plan } = await request.json();
|
||||
|
||||
if (!priceId || !plan) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Missing priceId or plan' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Get user from database
|
||||
const user = await db.user.findUnique({
|
||||
where: { email: session.user.email },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Create or get Stripe customer
|
||||
let customerId = user.stripeCustomerId;
|
||||
|
||||
if (!customerId) {
|
||||
const customer = await stripe.customers.create({
|
||||
email: user.email,
|
||||
metadata: {
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
customerId = customer.id;
|
||||
|
||||
// Update user with Stripe customer ID
|
||||
await db.user.update({
|
||||
where: { id: user.id },
|
||||
data: { stripeCustomerId: customerId },
|
||||
});
|
||||
}
|
||||
|
||||
// Create Stripe Checkout Session
|
||||
const checkoutSession = await stripe.checkout.sessions.create({
|
||||
customer: customerId,
|
||||
mode: 'subscription',
|
||||
payment_method_types: ['card', 'sepa_debit'],
|
||||
line_items: [
|
||||
{
|
||||
price: priceId,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?success=true`,
|
||||
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing?canceled=true`,
|
||||
metadata: {
|
||||
userId: user.id,
|
||||
plan,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({ url: checkoutSession.url });
|
||||
} catch (error) {
|
||||
console.error('Error creating checkout session:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { headers } from 'next/headers';
|
||||
import { stripe } from '@/lib/stripe';
|
||||
import { db } from '@/lib/db';
|
||||
import Stripe from 'stripe';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const body = await request.text();
|
||||
const signature = headers().get('stripe-signature');
|
||||
|
||||
if (!signature) {
|
||||
return NextResponse.json(
|
||||
{ error: 'No signature' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
let event: Stripe.Event;
|
||||
|
||||
try {
|
||||
event = stripe.webhooks.constructEvent(
|
||||
body,
|
||||
signature,
|
||||
process.env.STRIPE_WEBHOOK_SECRET!
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Webhook signature verification failed:', error);
|
||||
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;
|
||||
|
||||
if (session.mode === 'subscription') {
|
||||
const subscription = await stripe.subscriptions.retrieve(
|
||||
session.subscription as string
|
||||
);
|
||||
|
||||
await db.user.update({
|
||||
where: {
|
||||
stripeCustomerId: session.customer as string,
|
||||
},
|
||||
data: {
|
||||
stripeSubscriptionId: subscription.id,
|
||||
stripePriceId: subscription.items.data[0].price.id,
|
||||
stripeCurrentPeriodEnd: new Date(
|
||||
subscription.current_period_end * 1000
|
||||
),
|
||||
plan: (session.metadata?.plan || 'FREE') as any,
|
||||
},
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'customer.subscription.updated': {
|
||||
const subscription = event.data.object as Stripe.Subscription;
|
||||
|
||||
await db.user.update({
|
||||
where: {
|
||||
stripeSubscriptionId: subscription.id,
|
||||
},
|
||||
data: {
|
||||
stripePriceId: subscription.items.data[0].price.id,
|
||||
stripeCurrentPeriodEnd: new Date(
|
||||
subscription.current_period_end * 1000
|
||||
),
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'customer.subscription.deleted': {
|
||||
const subscription = event.data.object as Stripe.Subscription;
|
||||
|
||||
await db.user.update({
|
||||
where: {
|
||||
stripeSubscriptionId: subscription.id,
|
||||
},
|
||||
data: {
|
||||
stripeSubscriptionId: null,
|
||||
stripePriceId: null,
|
||||
stripeCurrentPeriodEnd: null,
|
||||
plan: 'FREE',
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ received: true });
|
||||
} catch (error) {
|
||||
console.error('Error processing webhook:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Webhook processing failed' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/lib/auth';
|
||||
import { db } from '@/lib/db';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const user = await db.user.findUnique({
|
||||
where: { email: session.user.email },
|
||||
select: {
|
||||
plan: true,
|
||||
stripeCurrentPeriodEnd: true,
|
||||
stripePriceId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
plan: user.plan,
|
||||
currentPeriodEnd: user.stripeCurrentPeriodEnd,
|
||||
priceId: user.stripePriceId,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching user plan:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
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: '2024-11-20.acacia',
|
||||
typescript: true,
|
||||
});
|
||||
|
||||
export const STRIPE_PLANS = {
|
||||
FREE: {
|
||||
name: 'Free / Starter',
|
||||
price: 0,
|
||||
currency: 'EUR',
|
||||
interval: 'month',
|
||||
features: [
|
||||
'3 dynamische QR-Codes',
|
||||
'Basis-Tracking (Scans + Standort)',
|
||||
'Einfache Designs',
|
||||
'Unbegrenzte statische QR-Codes',
|
||||
],
|
||||
limits: {
|
||||
dynamicQRCodes: 3,
|
||||
staticQRCodes: -1, // unlimited
|
||||
teamMembers: 1,
|
||||
},
|
||||
priceId: null, // No Stripe price for free plan
|
||||
},
|
||||
PRO: {
|
||||
name: 'Pro',
|
||||
price: 9,
|
||||
priceYearly: 90,
|
||||
currency: 'EUR',
|
||||
interval: 'month',
|
||||
features: [
|
||||
'50 QR-Codes',
|
||||
'Branding (Logo, Farben)',
|
||||
'Detaillierte Analytics (Datum, Gerät, Stadt)',
|
||||
'CSV-Export',
|
||||
'Passwortschutz',
|
||||
],
|
||||
limits: {
|
||||
dynamicQRCodes: 50,
|
||||
staticQRCodes: -1,
|
||||
teamMembers: 1,
|
||||
},
|
||||
priceId: process.env.STRIPE_PRICE_ID_PRO_MONTHLY,
|
||||
priceIdYearly: process.env.STRIPE_PRICE_ID_PRO_YEARLY,
|
||||
},
|
||||
BUSINESS: {
|
||||
name: 'Business',
|
||||
price: 29,
|
||||
priceYearly: 290,
|
||||
currency: 'EUR',
|
||||
interval: 'month',
|
||||
features: [
|
||||
'500 QR-Codes',
|
||||
'Team-Zugänge (bis 3 User)',
|
||||
'API-Zugang',
|
||||
'Benutzerdefinierte Domains',
|
||||
'White-Label',
|
||||
'Prioritäts-Support',
|
||||
],
|
||||
limits: {
|
||||
dynamicQRCodes: 500,
|
||||
staticQRCodes: -1,
|
||||
teamMembers: 3,
|
||||
},
|
||||
priceId: process.env.STRIPE_PRICE_ID_BUSINESS_MONTHLY,
|
||||
priceIdYearly: process.env.STRIPE_PRICE_ID_BUSINESS_YEARLY,
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type PlanType = keyof typeof STRIPE_PLANS;
|
||||
Loading…
Reference in New Issue