stripe
This commit is contained in:
parent
bccf771ffc
commit
cd3ee5fc8f
|
|
@ -2,7 +2,20 @@
|
|||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(docker-compose:*)",
|
||||
"Bash(docker container prune:*)"
|
||||
"Bash(docker container prune:*)",
|
||||
"Bash(npx prisma migrate dev:*)",
|
||||
"Bash(npx prisma:*)",
|
||||
"Bash(npm run dev)",
|
||||
"Bash(timeout:*)",
|
||||
"Bash(taskkill:*)",
|
||||
"Bash(npx kill-port:*)",
|
||||
"Bash(docker compose:*)",
|
||||
"Bash(curl -I https://fonts.googleapis.com)",
|
||||
"Bash(wsl:*)",
|
||||
"Read(//c/Users/a931627/.ssh/**)",
|
||||
"Bash(ssh-keygen:*)",
|
||||
"Bash(cat:*)",
|
||||
"Bash(git remote add:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ services:
|
|||
POSTGRES_DB: qrmaster
|
||||
POSTGRES_INITDB_ARGS: "-E UTF8 --locale=en_US.utf8"
|
||||
ports:
|
||||
- "5432:5432"
|
||||
- "5435:5432"
|
||||
volumes:
|
||||
- dbdata:/var/lib/postgresql/data
|
||||
- ./docker/init-db.sh:/docker-entrypoint-initdb.d/init-db.sh
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -27,6 +27,7 @@
|
|||
"dependencies": {
|
||||
"@auth/prisma-adapter": "^1.0.12",
|
||||
"@prisma/client": "^5.7.0",
|
||||
"@stripe/stripe-js": "^8.0.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"chart.js": "^4.4.0",
|
||||
"clsx": "^2.0.0",
|
||||
|
|
@ -44,6 +45,7 @@
|
|||
"react-dropzone": "^14.2.3",
|
||||
"react-i18next": "^13.5.0",
|
||||
"sharp": "^0.33.1",
|
||||
"stripe": "^19.1.0",
|
||||
"tailwind-merge": "^2.2.0",
|
||||
"xlsx": "^0.18.5",
|
||||
"zod": "^3.22.4"
|
||||
|
|
@ -68,4 +70,4 @@
|
|||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
||||
|
|
@ -16,22 +17,42 @@ export default function CreatePage() {
|
|||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [userPlan, setUserPlan] = useState<string>('FREE');
|
||||
|
||||
// Form state
|
||||
const [title, setTitle] = useState('');
|
||||
const [contentType, setContentType] = useState('URL');
|
||||
const [content, setContent] = useState<any>({ url: '' });
|
||||
const [isDynamic, setIsDynamic] = useState(true);
|
||||
const [tags, setTags] = useState('');
|
||||
|
||||
|
||||
// Style state
|
||||
const [foregroundColor, setForegroundColor] = useState('#000000');
|
||||
const [backgroundColor, setBackgroundColor] = useState('#FFFFFF');
|
||||
const [cornerStyle, setCornerStyle] = useState('square');
|
||||
const [size, setSize] = useState(200);
|
||||
|
||||
|
||||
// QR preview
|
||||
const [qrDataUrl, setQrDataUrl] = useState('');
|
||||
|
||||
// Check if user can customize colors (PRO+ only)
|
||||
const canCustomizeColors = userPlan === 'PRO' || userPlan === 'BUSINESS';
|
||||
|
||||
// Load user plan
|
||||
useEffect(() => {
|
||||
const fetchUserPlan = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/user/plan');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setUserPlan(data.plan || 'FREE');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching user plan:', error);
|
||||
}
|
||||
};
|
||||
fetchUserPlan();
|
||||
}, []);
|
||||
|
||||
const contrast = calculateContrast(foregroundColor, backgroundColor);
|
||||
const hasGoodContrast = contrast >= 4.5;
|
||||
|
|
@ -141,8 +162,9 @@ export default function CreatePage() {
|
|||
isStatic: !isDynamic, // Add this flag
|
||||
tags: tags.split(',').map(t => t.trim()).filter(Boolean),
|
||||
style: {
|
||||
foregroundColor,
|
||||
backgroundColor,
|
||||
// FREE users can only use black/white
|
||||
foregroundColor: canCustomizeColors ? foregroundColor : '#000000',
|
||||
backgroundColor: canCustomizeColors ? backgroundColor : '#FFFFFF',
|
||||
cornerStyle,
|
||||
size,
|
||||
},
|
||||
|
|
@ -354,9 +376,26 @@ export default function CreatePage() {
|
|||
{/* Style Section */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('create.style')}</CardTitle>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle>{t('create.style')}</CardTitle>
|
||||
{!canCustomizeColors && (
|
||||
<Badge variant="warning">PRO Feature</Badge>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{!canCustomizeColors && (
|
||||
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg mb-4">
|
||||
<p className="text-sm text-blue-900">
|
||||
<strong>Upgrade to PRO</strong> to customize colors, add logos, and brand your QR codes.
|
||||
</p>
|
||||
<Link href="/pricing">
|
||||
<Button variant="primary" size="sm" className="mt-2">
|
||||
Upgrade Now
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
|
|
@ -368,15 +407,17 @@ export default function CreatePage() {
|
|||
value={foregroundColor}
|
||||
onChange={(e) => setForegroundColor(e.target.value)}
|
||||
className="w-12 h-10 rounded border border-gray-300"
|
||||
disabled={!canCustomizeColors}
|
||||
/>
|
||||
<Input
|
||||
value={foregroundColor}
|
||||
onChange={(e) => setForegroundColor(e.target.value)}
|
||||
className="flex-1"
|
||||
disabled={!canCustomizeColors}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Background Color
|
||||
|
|
@ -387,11 +428,13 @@ export default function CreatePage() {
|
|||
value={backgroundColor}
|
||||
onChange={(e) => setBackgroundColor(e.target.value)}
|
||||
className="w-12 h-10 rounded border border-gray-300"
|
||||
disabled={!canCustomizeColors}
|
||||
/>
|
||||
<Input
|
||||
value={backgroundColor}
|
||||
onChange={(e) => setBackgroundColor(e.target.value)}
|
||||
className="flex-1"
|
||||
disabled={!canCustomizeColors}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,13 +2,15 @@
|
|||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { StatsGrid } from '@/components/dashboard/StatsGrid';
|
||||
import { QRCodeCard } from '@/components/dashboard/QRCodeCard';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { showToast } from '@/components/ui/Toast';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/Dialog';
|
||||
|
||||
interface QRCodeData {
|
||||
id: string;
|
||||
|
|
@ -24,10 +26,13 @@ interface QRCodeData {
|
|||
|
||||
export default function DashboardPage() {
|
||||
const { t } = useTranslation();
|
||||
const { data: session } = useSession();
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const [qrCodes, setQrCodes] = useState<QRCodeData[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [userPlan, setUserPlan] = useState<string>('FREE');
|
||||
const [showUpgradeDialog, setShowUpgradeDialog] = useState(false);
|
||||
const [upgradedPlan, setUpgradedPlan] = useState<string>('');
|
||||
const [stats, setStats] = useState({
|
||||
totalScans: 0,
|
||||
activeQRCodes: 0,
|
||||
|
|
@ -118,6 +123,35 @@ export default function DashboardPage() {
|
|||
},
|
||||
];
|
||||
|
||||
// Check for successful payment and verify session
|
||||
useEffect(() => {
|
||||
const success = searchParams.get('success');
|
||||
if (success === 'true') {
|
||||
const verifySession = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/stripe/verify-session', {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setUserPlan(data.plan);
|
||||
setUpgradedPlan(data.plan);
|
||||
setShowUpgradeDialog(true);
|
||||
// Remove success parameter from URL
|
||||
router.replace('/dashboard');
|
||||
} else {
|
||||
console.error('Failed to verify session:', await response.text());
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error verifying session:', error);
|
||||
}
|
||||
};
|
||||
|
||||
verifySession();
|
||||
}
|
||||
}, [searchParams, router]);
|
||||
|
||||
useEffect(() => {
|
||||
// Load real QR codes and user plan from API
|
||||
const fetchData = async () => {
|
||||
|
|
@ -148,13 +182,11 @@ export default function DashboardPage() {
|
|||
});
|
||||
}
|
||||
|
||||
// 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');
|
||||
}
|
||||
// Fetch user plan (using cookie-based auth, no session needed)
|
||||
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);
|
||||
|
|
@ -170,7 +202,7 @@ export default function DashboardPage() {
|
|||
};
|
||||
|
||||
fetchData();
|
||||
}, [session]);
|
||||
}, []);
|
||||
|
||||
const handleEdit = (id: string) => {
|
||||
console.log('Edit QR:', id);
|
||||
|
|
@ -200,16 +232,8 @@ export default function DashboardPage() {
|
|||
};
|
||||
|
||||
const getPlanEmoji = (plan: string) => {
|
||||
switch (plan) {
|
||||
case 'FREE':
|
||||
return '🟢';
|
||||
case 'PRO':
|
||||
return '🔵';
|
||||
case 'BUSINESS':
|
||||
return '🟣';
|
||||
default:
|
||||
return '⚪';
|
||||
}
|
||||
// No emojis anymore
|
||||
return '';
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -222,7 +246,7 @@ export default function DashboardPage() {
|
|||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<Badge variant={getPlanBadgeColor(userPlan)} className="text-lg px-4 py-2">
|
||||
{getPlanEmoji(userPlan)} {userPlan} Plan
|
||||
{userPlan} Plan
|
||||
</Badge>
|
||||
{userPlan === 'FREE' && (
|
||||
<Link href="/pricing">
|
||||
|
|
@ -302,6 +326,89 @@ export default function DashboardPage() {
|
|||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Upgrade Success Dialog */}
|
||||
<Dialog open={showUpgradeDialog} onOpenChange={setShowUpgradeDialog}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl text-center">
|
||||
Upgrade erfolgreich!
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-center text-base pt-4">
|
||||
Willkommen im <strong>{upgradedPlan} Plan</strong>! Ihr Konto wurde erfolgreich aktualisiert.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="py-6 space-y-4">
|
||||
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 p-6 rounded-lg border border-blue-200">
|
||||
<h3 className="font-semibold text-gray-900 mb-3">Ihre neuen Features:</h3>
|
||||
<ul className="space-y-2 text-sm text-gray-700">
|
||||
{upgradedPlan === 'PRO' && (
|
||||
<>
|
||||
<li className="flex items-start">
|
||||
<span className="text-green-600 mr-2">✓</span>
|
||||
<span>50 dynamische QR-Codes</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-green-600 mr-2">✓</span>
|
||||
<span>Branding (Logo, Farben anpassen)</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-green-600 mr-2">✓</span>
|
||||
<span>Detaillierte Analytics</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-green-600 mr-2">✓</span>
|
||||
<span>CSV-Export</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-green-600 mr-2">✓</span>
|
||||
<span>Passwortschutz für QR-Codes</span>
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
{upgradedPlan === 'BUSINESS' && (
|
||||
<>
|
||||
<li className="flex items-start">
|
||||
<span className="text-green-600 mr-2">✓</span>
|
||||
<span>500 dynamische QR-Codes</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-green-600 mr-2">✓</span>
|
||||
<span>Team-Zugänge (bis zu 3 User)</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-green-600 mr-2">✓</span>
|
||||
<span>Benutzerdefinierte Domains</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-green-600 mr-2">✓</span>
|
||||
<span>White-Label</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-green-600 mr-2">✓</span>
|
||||
<span>Prioritäts-Support</span>
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
setShowUpgradeDialog(false);
|
||||
router.push('/create');
|
||||
}}
|
||||
className="w-full"
|
||||
>
|
||||
Ersten QR-Code erstellen
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
|
|
@ -10,15 +9,25 @@ import { showToast } from '@/components/ui/Toast';
|
|||
|
||||
export default function PricingPage() {
|
||||
const router = useRouter();
|
||||
const { data: session } = useSession();
|
||||
const searchParams = useSearchParams();
|
||||
const [user, setUser] = useState<any>(null);
|
||||
const [loading, setLoading] = useState<string | null>(null);
|
||||
const [billingInterval, setBillingInterval] = useState<'monthly' | 'yearly'>('monthly');
|
||||
const [hasTriggeredCheckout, setHasTriggeredCheckout] = useState(false);
|
||||
|
||||
// Check for user in localStorage
|
||||
useEffect(() => {
|
||||
const storedUser = localStorage.getItem('user');
|
||||
if (storedUser) {
|
||||
setUser(JSON.parse(storedUser));
|
||||
}
|
||||
}, []);
|
||||
|
||||
const plans = [
|
||||
{
|
||||
id: 'FREE',
|
||||
name: 'Free / Starter',
|
||||
icon: '🟢',
|
||||
icon: '',
|
||||
price: 0,
|
||||
priceYearly: 0,
|
||||
description: 'Privatnutzer & Testkunden',
|
||||
|
|
@ -36,7 +45,7 @@ export default function PricingPage() {
|
|||
{
|
||||
id: 'PRO',
|
||||
name: 'Pro',
|
||||
icon: '🔵',
|
||||
icon: '',
|
||||
price: 9,
|
||||
priceYearly: 90,
|
||||
description: 'Selbstständige / kleine Firmen',
|
||||
|
|
@ -55,14 +64,13 @@ export default function PricingPage() {
|
|||
{
|
||||
id: 'BUSINESS',
|
||||
name: 'Business',
|
||||
icon: '🟣',
|
||||
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',
|
||||
|
|
@ -75,8 +83,24 @@ export default function PricingPage() {
|
|||
];
|
||||
|
||||
const handleSubscribe = async (planId: string, priceId: string | null | undefined) => {
|
||||
if (!session) {
|
||||
router.push('/login?redirect=/pricing');
|
||||
console.log('🔵 handleSubscribe called:', { planId, priceId, hasUser: !!user });
|
||||
|
||||
if (!user) {
|
||||
// Save the plan selection in localStorage so we can continue after login
|
||||
const pendingPlan = {
|
||||
planId,
|
||||
interval: billingInterval,
|
||||
};
|
||||
console.log('💾 Saving pending plan to localStorage:', pendingPlan);
|
||||
localStorage.setItem('pendingPlan', JSON.stringify(pendingPlan));
|
||||
|
||||
// Verify it was saved
|
||||
const saved = localStorage.getItem('pendingPlan');
|
||||
console.log('✅ Verified saved:', saved);
|
||||
|
||||
// Use window.location instead of router.push to ensure localStorage is written
|
||||
console.log('🔄 Redirecting to login...');
|
||||
window.location.href = '/login?redirect=/pricing';
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -99,6 +123,7 @@ export default function PricingPage() {
|
|||
body: JSON.stringify({
|
||||
priceId,
|
||||
plan: planId,
|
||||
userEmail: user.email,
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
@ -117,6 +142,67 @@ export default function PricingPage() {
|
|||
}
|
||||
};
|
||||
|
||||
// Auto-trigger checkout after login if plan is selected
|
||||
useEffect(() => {
|
||||
console.log('Pricing useEffect triggered:', {
|
||||
hasUser: !!user,
|
||||
hasTriggeredCheckout,
|
||||
});
|
||||
|
||||
// Only run once and only when authenticated
|
||||
if (hasTriggeredCheckout) {
|
||||
console.log('Already triggered checkout, skipping...');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
console.log('Not authenticated - no user in localStorage');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for pending plan in localStorage
|
||||
const pendingPlanStr = localStorage.getItem('pendingPlan');
|
||||
if (pendingPlanStr) {
|
||||
try {
|
||||
const pendingPlan = JSON.parse(pendingPlanStr);
|
||||
console.log('✅ Found pending plan:', pendingPlan);
|
||||
|
||||
// Clear pending plan immediately
|
||||
localStorage.removeItem('pendingPlan');
|
||||
|
||||
// Mark as triggered to prevent re-runs
|
||||
setHasTriggeredCheckout(true);
|
||||
|
||||
// Set the billing interval
|
||||
setBillingInterval(pendingPlan.interval);
|
||||
|
||||
// Find the plan
|
||||
const selectedPlan = plans.find((p) => p.id === pendingPlan.planId);
|
||||
if (selectedPlan) {
|
||||
const priceId =
|
||||
pendingPlan.interval === 'yearly'
|
||||
? selectedPlan.priceIdYearly
|
||||
: selectedPlan.priceIdMonthly;
|
||||
|
||||
console.log('✅ Found plan and priceId:', selectedPlan.name, priceId);
|
||||
|
||||
// Trigger checkout after a short delay
|
||||
setTimeout(() => {
|
||||
console.log('🚀 Calling handleSubscribe now...');
|
||||
handleSubscribe(selectedPlan.id, priceId);
|
||||
}, 500);
|
||||
} else {
|
||||
console.error('❌ Plan not found:', pendingPlan.planId);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error parsing pending plan:', e);
|
||||
localStorage.removeItem('pendingPlan');
|
||||
}
|
||||
} else {
|
||||
console.log('No pending plan in localStorage');
|
||||
}
|
||||
}, [user, hasTriggeredCheckout]);
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-12">
|
||||
<div className="text-center mb-12">
|
||||
|
|
@ -180,7 +266,7 @@ export default function PricingPage() {
|
|||
)}
|
||||
|
||||
<CardHeader className="text-center pb-6">
|
||||
<div className="text-4xl mb-4">{plan.icon}</div>
|
||||
{plan.icon && <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>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
|
|
@ -10,6 +10,7 @@ import { useTranslation } from '@/hooks/useTranslation';
|
|||
|
||||
export default function LoginPage() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const { t } = useTranslation();
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
|
|
@ -33,7 +34,10 @@ export default function LoginPage() {
|
|||
if (response.ok && data.success) {
|
||||
// Store user in localStorage for client-side
|
||||
localStorage.setItem('user', JSON.stringify(data.user));
|
||||
router.push('/dashboard');
|
||||
|
||||
// Check for redirect parameter
|
||||
const redirectUrl = searchParams.get('redirect') || '/dashboard';
|
||||
router.push(redirectUrl);
|
||||
router.refresh();
|
||||
} else {
|
||||
setError(data.error || 'Invalid email or password');
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Pricing } from '@/components/marketing/Pricing';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
|
||||
export default function PricingPage() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="py-20">
|
||||
<Pricing t={t} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -38,32 +38,67 @@ export async function GET(request: NextRequest) {
|
|||
}
|
||||
}
|
||||
|
||||
// Plan limits
|
||||
const PLAN_LIMITS = {
|
||||
FREE: 3,
|
||||
PRO: 50,
|
||||
BUSINESS: 500,
|
||||
};
|
||||
|
||||
// POST /api/qrs - Create a new QR code
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const userId = cookies().get('userId')?.value;
|
||||
console.log('POST /api/qrs - userId from cookie:', userId);
|
||||
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized - no userId cookie' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Check if user exists
|
||||
const userExists = await db.user.findUnique({
|
||||
where: { id: userId }
|
||||
// Check if user exists and get their plan
|
||||
const user = await db.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { plan: true },
|
||||
});
|
||||
|
||||
console.log('User exists:', !!userExists);
|
||||
|
||||
if (!userExists) {
|
||||
|
||||
console.log('User exists:', !!user);
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: `User not found: ${userId}` }, { status: 404 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
console.log('Request body:', body);
|
||||
|
||||
|
||||
// Check if this is a static QR request
|
||||
const isStatic = body.isStatic === true;
|
||||
|
||||
// Only check limits for DYNAMIC QR codes (static QR codes are unlimited)
|
||||
if (!isStatic) {
|
||||
// Count existing dynamic QR codes
|
||||
const dynamicQRCount = await db.qRCode.count({
|
||||
where: {
|
||||
userId,
|
||||
type: 'DYNAMIC',
|
||||
},
|
||||
});
|
||||
|
||||
const userPlan = user.plan || 'FREE';
|
||||
const limit = PLAN_LIMITS[userPlan as keyof typeof PLAN_LIMITS] || PLAN_LIMITS.FREE;
|
||||
|
||||
if (dynamicQRCount >= limit) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Limit reached',
|
||||
message: `You have reached the limit of ${limit} dynamic QR codes for your ${userPlan} plan. Please upgrade to create more.`,
|
||||
currentCount: dynamicQRCount,
|
||||
limit,
|
||||
plan: userPlan,
|
||||
},
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let enrichedContent = body.content;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +1,17 @@
|
|||
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';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions);
|
||||
// Get user email from request body (since we're using simple auth, not NextAuth)
|
||||
const { priceId, plan, userEmail } = await request.json();
|
||||
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
if (!userEmail) {
|
||||
return NextResponse.json({ error: 'Unauthorized - No user email provided' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { priceId, plan } = await request.json();
|
||||
|
||||
if (!priceId || !plan) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Missing priceId or plan' },
|
||||
|
|
@ -23,7 +21,7 @@ export async function POST(request: NextRequest) {
|
|||
|
||||
// Get user from database
|
||||
const user = await db.user.findUnique({
|
||||
where: { email: session.user.email },
|
||||
where: { email: userEmail },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
|
|
@ -54,7 +52,7 @@ export async function POST(request: NextRequest) {
|
|||
const checkoutSession = await stripe.checkout.sessions.create({
|
||||
customer: customerId,
|
||||
mode: 'subscription',
|
||||
payment_method_types: ['card', 'sepa_debit'],
|
||||
payment_method_types: ['card'],
|
||||
line_items: [
|
||||
{
|
||||
price: priceId,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,76 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import { stripe } from '@/lib/stripe';
|
||||
import { db } from '@/lib/db';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Use cookie-based auth instead of NextAuth
|
||||
const userId = cookies().get('userId')?.value;
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const user = await db.user.findUnique({
|
||||
where: { id: userId },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (!user.stripeCustomerId) {
|
||||
return NextResponse.json({ error: 'No Stripe customer ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Get the most recent checkout session for this customer
|
||||
const checkoutSessions = await stripe.checkout.sessions.list({
|
||||
customer: user.stripeCustomerId,
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
if (checkoutSessions.data.length === 0) {
|
||||
return NextResponse.json({ error: 'No checkout session found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const checkoutSession = checkoutSessions.data[0];
|
||||
|
||||
// Only process if payment was successful
|
||||
if (checkoutSession.payment_status === 'paid' && checkoutSession.subscription) {
|
||||
const subscription = await stripe.subscriptions.retrieve(
|
||||
checkoutSession.subscription as string
|
||||
);
|
||||
|
||||
// Determine plan from metadata or price ID
|
||||
const plan = checkoutSession.metadata?.plan || 'PRO';
|
||||
|
||||
// Update user in database
|
||||
await db.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
stripeSubscriptionId: subscription.id,
|
||||
stripePriceId: subscription.items.data[0].price.id,
|
||||
stripeCurrentPeriodEnd: new Date(
|
||||
subscription.current_period_end * 1000
|
||||
),
|
||||
plan: plan as any,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
plan,
|
||||
subscriptionId: subscription.id,
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Payment not completed' }, { status: 400 });
|
||||
} catch (error) {
|
||||
console.error('Error verifying session:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +1,18 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/lib/auth';
|
||||
import { cookies } from 'next/headers';
|
||||
import { db } from '@/lib/db';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions);
|
||||
// Use cookie-based auth instead of NextAuth
|
||||
const userId = cookies().get('userId')?.value;
|
||||
|
||||
if (!session?.user?.email) {
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const user = await db.user.findUnique({
|
||||
where: { email: session.user.email },
|
||||
where: { id: userId },
|
||||
select: {
|
||||
plan: true,
|
||||
stripeCurrentPeriodEnd: true,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
import type { Metadata } from 'next';
|
||||
import { Inter } from 'next/font/google';
|
||||
import '@/styles/globals.css';
|
||||
import { ToastContainer } from '@/components/ui/Toast';
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] });
|
||||
import AuthProvider from '@/components/SessionProvider';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'QR Master - Create Custom QR Codes in Seconds',
|
||||
|
|
@ -33,8 +31,10 @@ export default function RootLayout({
|
|||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>
|
||||
{children}
|
||||
<body className="font-sans">
|
||||
<AuthProvider>
|
||||
{children}
|
||||
</AuthProvider>
|
||||
<ToastContainer />
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
'use client';
|
||||
|
||||
import { SessionProvider } from 'next-auth/react';
|
||||
|
||||
export default function AuthProvider({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return <SessionProvider>{children}</SessionProvider>;
|
||||
}
|
||||
Loading…
Reference in New Issue