subscribe stripe e kto

main
miranshala 7 months ago
parent f5927f2e76
commit bfedd5fdaa

@ -62,12 +62,8 @@ export async function POST(request: NextRequest) {
const newUser = result.rows[0];
// Create a basic subscription for the user
await client.query(
`INSERT INTO subscriptions (user_id, tier, status, storage_limit_gb, current_usage_gb)
VALUES ($1, $2, $3, $4, $5)`,
[newUser.id, "BASIC", "ACTIVE", 5, 0]
);
// Don't create subscription automatically - users need to purchase a plan
// Subscriptions will be created when users complete payment via Stripe webhook
return NextResponse.json({
success: true,

@ -0,0 +1,123 @@
import { NextRequest, NextResponse } from "next/server";
import { stripe } from "@/lib/stripe";
import { formatAmountForStripe } from "@/lib/stripe";
import { getPool } from "@/lib/database";
// Define pricing tiers
const PRICING_TIERS = {
basic: {
name: "Basic",
amount: 9.99,
storageLimit: 5,
},
standard: {
name: "Standard",
amount: 19.99,
storageLimit: 20,
},
premium: {
name: "Premium",
amount: 39.99,
storageLimit: 100,
},
};
export async function POST(request: NextRequest) {
try {
// Check if Stripe is configured
if (!process.env.STRIPE_SECRET_KEY) {
console.error("Stripe secret key is not configured");
return NextResponse.json(
{
error:
"Stripe is not configured. Please add STRIPE_SECRET_KEY to your environment variables.",
},
{ status: 500 }
);
}
const { tier, userId } = await request.json();
// Validate tier
const selectedTier = PRICING_TIERS[tier as keyof typeof PRICING_TIERS];
if (!selectedTier) {
return NextResponse.json(
{ error: "Invalid tier selected" },
{ status: 400 }
);
}
// Validate user
if (!userId) {
return NextResponse.json(
{ error: "User ID is required" },
{ status: 400 }
);
}
// Check if user already has an active subscription
const pool = getPool();
const client = await pool.connect();
try {
const existingSub = await client.query(
`SELECT id, status FROM subscriptions WHERE user_id = $1 AND status = 'ACTIVE'`,
[userId]
);
if (existingSub.rows.length > 0) {
return NextResponse.json(
{
error:
"You already have an active subscription. Please cancel your current subscription before purchasing a new one.",
},
{ status: 400 }
);
}
} catch (dbError) {
console.error("Database error checking existing subscription:", dbError);
// Continue with payment creation even if check fails
} finally {
client.release();
}
// Get the base URL
const origin = request.headers.get("origin") || "http://localhost:3000";
// Create checkout session
const session = await stripe.checkout.sessions.create({
mode: "subscription",
payment_method_types: ["card"],
line_items: [
{
price_data: {
currency: "usd",
product_data: {
name: selectedTier.name,
description: `${selectedTier.storageLimit}GB storage limit`,
},
unit_amount: formatAmountForStripe(selectedTier.amount),
recurring: {
interval: "month",
},
},
quantity: 1,
},
],
success_url: `${origin}/?success=true&session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${origin}/?canceled=true`,
metadata: {
userId: userId,
tier: tier,
},
});
return NextResponse.json({ sessionId: session.id, url: session.url });
} catch (error: any) {
console.error("Error creating checkout session:", error);
return NextResponse.json(
{ error: error.message || "Failed to create checkout session" },
{ status: 500 }
);
}
}

@ -0,0 +1,42 @@
import { NextRequest, NextResponse } from "next/server";
import { getPool } from "@/lib/database";
export async function POST(request: NextRequest) {
try {
// Get user ID from auth token or session
const userId = request.headers.get("user-id");
if (!userId) {
return NextResponse.json(
{ error: "User not authenticated" },
{ status: 401 }
);
}
const pool = getPool();
const client = await pool.connect();
try {
// Cancel the subscription
await client.query(
`UPDATE subscriptions
SET status = 'CANCELLED', updated_at = CURRENT_TIMESTAMP
WHERE user_id = $1 AND status = 'ACTIVE'`,
[userId]
);
return NextResponse.json({
success: true,
message: "Subscription cancelled",
});
} finally {
client.release();
}
} catch (error) {
console.error("Error cancelling subscription:", error);
return NextResponse.json(
{ error: "Failed to cancel subscription" },
{ status: 500 }
);
}
}

@ -0,0 +1,53 @@
import { NextRequest, NextResponse } from "next/server";
import { getPool } from "@/lib/database";
export async function GET(request: NextRequest) {
try {
// Get user ID from auth token or session
const userId = request.headers.get("user-id");
if (!userId) {
return NextResponse.json(
{ error: "User not authenticated" },
{ status: 401 }
);
}
const pool = getPool();
const client = await pool.connect();
try {
// Try to get active subscription first, otherwise get the most recent one
const result = await client.query(
`SELECT id, tier, status, storage_limit_gb, current_usage_gb, created_at, updated_at
FROM subscriptions
WHERE user_id = $1
ORDER BY
CASE WHEN status = 'ACTIVE' THEN 0 ELSE 1 END,
created_at DESC
LIMIT 1`,
[userId]
);
console.log(
`Found ${result.rows.length} subscriptions for user ${userId}`
);
if (result.rows.length === 0) {
console.log("No subscriptions found");
return NextResponse.json({ subscription: null });
}
console.log("Returning subscription:", result.rows[0]);
return NextResponse.json({ subscription: result.rows[0] });
} finally {
client.release();
}
} catch (error) {
console.error("Error fetching subscription:", error);
return NextResponse.json(
{ error: "Failed to fetch subscription" },
{ status: 500 }
);
}
}

@ -0,0 +1,124 @@
import { NextRequest, NextResponse } from "next/server";
import { stripe } from "@/lib/stripe";
import { getPool } from "@/lib/database";
import Stripe from "stripe";
export async function POST(request: NextRequest) {
const body = await request.text();
const signature = request.headers.get("stripe-signature");
if (!signature) {
return NextResponse.json({ error: "No signature" }, { status: 400 });
}
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
if (!webhookSecret) {
console.error("Missing STRIPE_WEBHOOK_SECRET");
return NextResponse.json(
{ error: "Webhook secret not configured" },
{ status: 500 }
);
}
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
} catch (err: any) {
console.error(`Webhook signature verification failed: ${err.message}`);
return NextResponse.json(
{ error: `Webhook Error: ${err.message}` },
{ status: 400 }
);
}
const pool = getPool();
const client = await pool.connect();
try {
// Handle different event types
switch (event.type) {
case "checkout.session.completed": {
const session = event.data.object as Stripe.Checkout.Session;
const userId = session.metadata?.userId;
const tier = session.metadata?.tier;
console.log("Webhook received - userId:", userId, "tier:", tier);
if (!userId || !tier) {
console.error("Missing user or tier in session metadata");
break;
}
// Check if subscription exists
const existing = await client.query(
`SELECT id FROM subscriptions WHERE user_id = $1`,
[userId]
);
// Normalize tier name
const tierName = tier.toUpperCase();
console.log("Processing tier:", tierName);
const storageLimit =
tierName === "BASIC"
? 5
: tierName === "STANDARD"
? 20
: tierName === "PREMIUM"
? 100
: 5;
if (existing.rows.length > 0) {
// Update existing subscription
await client.query(
`UPDATE subscriptions
SET status = 'ACTIVE', tier = $1, storage_limit_gb = $2, updated_at = CURRENT_TIMESTAMP
WHERE user_id = $3`,
[tierName, storageLimit, userId]
);
console.log(`Subscription updated for user ${userId}`);
} else {
// Create new subscription
await client.query(
`INSERT INTO subscriptions (user_id, tier, status, storage_limit_gb, current_usage_gb)
VALUES ($1, $2, 'ACTIVE', $3, 0)`,
[userId, tierName, storageLimit]
);
console.log(`Subscription created for user ${userId}`);
}
break;
}
case "customer.subscription.deleted":
case "customer.subscription.updated": {
const subscription = event.data.object as Stripe.Subscription;
const customerId = subscription.customer as string;
// Find user by customer ID and update subscription status
const status = subscription.status === "active" ? "ACTIVE" : "INACTIVE";
await client.query(
`UPDATE subscriptions
SET status = $1, updated_at = CURRENT_TIMESTAMP
WHERE stripe_customer_id = $2`,
[status, customerId]
);
console.log(`Subscription ${event.type} for customer ${customerId}`);
break;
}
default:
console.log(`Unhandled event type: ${event.type}`);
}
return NextResponse.json({ received: true });
} catch (error) {
console.error("Error processing webhook:", error);
return NextResponse.json(
{ error: "Webhook processing failed" },
{ status: 500 }
);
} finally {
client.release();
}
}

@ -1,5 +1,6 @@
"use client";
import { useState, useEffect } from "react";
import Image from "next/image";
import UploadArea from "@/components/UploadArea";
import { useAuth } from "@/lib/auth-context";
@ -19,6 +20,46 @@ import { ChevronDownIcon } from "lucide-react";
export default function Home() {
const { isAuthenticated, user, logout } = useAuth();
const [showSuccess, setShowSuccess] = useState(false);
const [userSubscription, setUserSubscription] = useState<any>(null);
// Check for success message in URL
useEffect(() => {
if (typeof window !== "undefined") {
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get("success") === "true") {
setShowSuccess(true);
// Remove the query parameter from URL
window.history.replaceState({}, "", window.location.pathname);
// Hide after 5 seconds
setTimeout(() => setShowSuccess(false), 5000);
}
}
}, []);
// Fetch user subscription when authenticated
useEffect(() => {
const fetchUserSubscription = async () => {
if (!isAuthenticated || !user?.id) return;
try {
const response = await fetch("/api/subscriptions/me", {
headers: {
"user-id": user.id,
},
});
if (response.ok) {
const data = await response.json();
setUserSubscription(data.subscription);
}
} catch (error) {
console.error("Error fetching subscription:", error);
}
};
fetchUserSubscription();
}, [isAuthenticated, user]);
// Get user initials for the circle icon
const getUserInitials = () => {
@ -28,8 +69,81 @@ export default function Home() {
return (firstName.charAt(0) + lastName.charAt(0)).toUpperCase() || "U";
};
// Handle subscription button click
const handleSubscribe = async (tier: string) => {
if (!isAuthenticated) {
// Redirect to login
window.location.href = "/login?redirect=/";
return;
}
// Check if user already has an active subscription
if (userSubscription && userSubscription.status === "ACTIVE") {
alert(
"You already have an active subscription. Please manage it from the My Subscriptions page."
);
return;
}
// Show loading state
setLoading(true);
try {
const response = await fetch("/api/create-checkout-session", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
tier,
userId: user?.id,
}),
});
// Check if response is JSON before parsing
const contentType = response.headers.get("content-type");
if (!contentType || !contentType.includes("application/json")) {
throw new Error(
"Invalid response from server. Please check your Stripe configuration."
);
}
const data = await response.json();
if (!response.ok) {
const errorMessage = data.error || "Failed to create checkout session";
console.error("Payment API error:", errorMessage);
alert(errorMessage);
setLoading(false);
return;
}
// Redirect to Stripe Checkout
if (data.url) {
window.location.href = data.url;
} else {
alert("No checkout URL received");
setLoading(false);
}
} catch (error) {
console.error("Payment error:", error);
alert("Failed to start payment process. Please try again.");
setLoading(false);
}
};
const [loading, setLoading] = useState(false);
return (
<div className="min-h-screen" style={{ backgroundColor: "#FFFFFF" }}>
{/* Success Message Banner */}
{showSuccess && (
<div className="fixed top-16 left-1/2 transform -translate-x-1/2 z-50 bg-green-500 text-white px-6 py-3 rounded-lg shadow-lg flex items-center space-x-2">
<span></span>
<span>Subscription successful! Welcome to Reya Render.</span>
</div>
)}
{/* Sticky Navigation */}
<nav className="sticky top-0 z-50 bg-white/95 backdrop-blur-sm border-b border-gray-200">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
@ -166,7 +280,7 @@ export default function Home() {
</div>
<div className="absolute bottom-4 right-4">
<img
src="/3d-model.png"
src="/creativity.png"
alt="3D Model"
width={500}
height={500}
@ -176,6 +290,33 @@ export default function Home() {
</div>
</div>
{/* Subscription Status Indicator */}
{isAuthenticated &&
userSubscription &&
userSubscription.status === "ACTIVE" && (
<div className="flex justify-center mt-8">
<div className="bg-green-50 border-2 border-green-500 rounded-lg p-6 max-w-2xl">
<div className="flex items-center justify-between">
<div>
<p className="text-green-800 font-semibold text-lg mb-1">
You have an active {userSubscription.tier} subscription
</p>
<p className="text-green-700 text-sm">
Member since{" "}
{new Date(userSubscription.created_at).toLocaleDateString()}
</p>
</div>
<a
href="/subscriptions"
className="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded-md text-sm font-medium ml-4"
>
Manage Subscription
</a>
</div>
</div>
</div>
)}
{/* Pricing Section */}
<div className="flex justify-center mt-16 mb-16">
<div className="w-[90%]">
@ -194,8 +335,19 @@ export default function Home() {
/month
</span>
</div>
<button className="w-full bg-orange-500 text-white py-3 px-6 rounded-lg font-medium mb-6 hover:bg-orange-600 transition-colors hover:cursor-pointer">
Choose Basic
<button
onClick={() => handleSubscribe("basic")}
disabled={
loading ||
(userSubscription && userSubscription.status === "ACTIVE")
}
className="w-full bg-orange-500 text-white py-3 px-6 rounded-lg font-medium mb-6 hover:bg-orange-600 transition-colors hover:cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading
? "Processing..."
: userSubscription && userSubscription.status === "ACTIVE"
? "Current Plan"
: "Choose Basic"}
</button>
<ul className="text-left space-y-3">
<li className="flex items-center">
@ -232,8 +384,19 @@ export default function Home() {
/month
</span>
</div>
<button className="w-full bg-orange-500 text-white py-3 px-6 rounded-lg font-medium mb-6 hover:bg-orange-600 transition-colors hover:cursor-pointer">
Choose Standard
<button
onClick={() => handleSubscribe("standard")}
disabled={
loading ||
(userSubscription && userSubscription.status === "ACTIVE")
}
className="w-full bg-orange-500 text-white py-3 px-6 rounded-lg font-medium mb-6 hover:bg-orange-600 transition-colors hover:cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading
? "Processing..."
: userSubscription && userSubscription.status === "ACTIVE"
? "Current Plan"
: "Choose Standard"}
</button>
<ul className="text-left space-y-3">
<li className="flex items-center">
@ -278,8 +441,19 @@ export default function Home() {
/month
</span>
</div>
<button className="w-full bg-orange-500 text-white py-3 px-6 rounded-lg font-medium mb-6 hover:bg-orange-600 transition-colors hover:cursor-pointer">
Choose Premium
<button
onClick={() => handleSubscribe("premium")}
disabled={
loading ||
(userSubscription && userSubscription.status === "ACTIVE")
}
className="w-full bg-orange-500 text-white py-3 px-6 rounded-lg font-medium mb-6 hover:bg-orange-600 transition-colors hover:cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading
? "Processing..."
: userSubscription && userSubscription.status === "ACTIVE"
? "Current Plan"
: "Choose Premium"}
</button>
<ul className="text-left space-y-3">
<li className="flex items-center">
@ -323,18 +497,6 @@ export default function Home() {
</div>
</div>
</div>
{/* Upload area below hero, same width as orange container - only show when authenticated */}
{isAuthenticated && (
<div className="flex justify-center">
<div className="w-[90%] mt-6 mb-12">
<h2 className="text-4xl font-bold text-left mb-8 text-gray-800">
Start by uploading at least one .blend file
</h2>
<UploadArea className="w-full shadow-sm" />
</div>
</div>
)}
</div>
);
}

@ -0,0 +1,303 @@
"use client";
import { useEffect, useState } from "react";
import { useAuth } from "@/lib/auth-context";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
interface Subscription {
id: string;
tier: string;
status: string;
storage_limit_gb: number;
current_usage_gb: number;
created_at: string;
}
export default function SubscriptionsPage() {
const { isAuthenticated, user, logout, token } = useAuth();
const router = useRouter();
const [subscription, setSubscription] = useState<Subscription | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [authInitialized, setAuthInitialized] = useState(false);
// Wait for auth to initialize from localStorage
useEffect(() => {
// Small delay to let auth context initialize
const timer = setTimeout(() => {
setAuthInitialized(true);
}, 100);
return () => clearTimeout(timer);
}, []);
useEffect(() => {
if (!authInitialized) return;
// Check if user is authenticated
if (user && token) {
// User is authenticated, fetch subscription
fetchSubscription();
} else {
// Not authenticated, redirect to login
router.push("/login?redirect=/subscriptions");
}
}, [authInitialized, user, token, router]);
const fetchSubscription = async () => {
if (!user?.id) return;
setLoading(true);
try {
const response = await fetch("/api/subscriptions/me", {
headers: {
"user-id": user.id,
},
});
if (response.ok) {
const data = await response.json();
setSubscription(data.subscription);
} else {
setError("Failed to fetch subscription data");
}
} catch (err) {
setError("Error loading subscription");
} finally {
setLoading(false);
}
};
const handleCancelSubscription = async () => {
if (!confirm("Are you sure you want to cancel your subscription?")) {
return;
}
try {
const response = await fetch("/api/subscriptions/cancel", {
method: "POST",
headers: {
"user-id": user?.id || "",
},
});
if (response.ok) {
alert("Subscription cancelled successfully");
fetchSubscription(); // Refresh data
} else {
const data = await response.json();
alert(data.error || "Failed to cancel subscription");
}
} catch (err) {
alert("Error cancelling subscription");
}
};
// Don't show content until auth has initialized
if (!authInitialized) {
return (
<div className="min-h-screen flex items-center justify-center">
Loading...
</div>
);
}
if (!isAuthenticated) {
return null; // Will redirect to login
}
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center">
Loading subscription...
</div>
);
}
if (error) {
return (
<div className="min-h-screen flex items-center justify-center text-red-500">
{error}
</div>
);
}
const getTierDisplayName = (tier: string) => {
const tierMap: Record<string, string> = {
BASIC: "Basic",
STANDARD: "Standard",
PREMIUM: "Premium",
};
return tierMap[tier.toUpperCase()] || tier;
};
const getTierPrice = (tier: string) => {
const priceMap: Record<string, string> = {
BASIC: "$9.99",
STANDARD: "$19.99",
PREMIUM: "$39.99",
};
return priceMap[tier.toUpperCase()] || "N/A";
};
return (
<div className="min-h-screen bg-gray-50 py-12">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Header */}
<div className="mb-8">
<h1 className="text-4xl font-bold text-gray-900 mb-2">
My Subscriptions
</h1>
<p className="text-gray-600">
Manage your subscription and billing information
</p>
</div>
{!subscription ? (
// No subscription
<div className="bg-white rounded-lg shadow-lg p-8 text-center">
<div className="mb-6">
<div className="w-20 h-20 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
<span className="text-3xl">📦</span>
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-2">
No Active Subscription
</h2>
<p className="text-gray-600 mb-6">
You don't have an active subscription. Subscribe to a plan to
start using our services.
</p>
<Button
onClick={() => router.push("/")}
className="bg-orange-500 hover:bg-orange-600 text-white font-medium px-6 py-3 rounded-md"
>
Browse Plans
</Button>
</div>
</div>
) : (
// Active subscription
<div className="space-y-6">
{/* Subscription Card */}
<div className="bg-white rounded-lg shadow-lg p-8 border-2 border-gray-200">
<div className="flex justify-between items-start mb-6">
<div>
<h2 className="text-2xl font-bold text-gray-900 mb-2">
{getTierDisplayName(subscription.tier)} Plan
</h2>
<p className="text-gray-600">Current subscription details</p>
</div>
<div className="text-right">
<span
className={`px-3 py-1 rounded-full text-sm font-semibold ${
subscription.status === "ACTIVE"
? "bg-green-100 text-green-800"
: "bg-red-100 text-red-800"
}`}
>
{subscription.status}
</span>
</div>
</div>
{/* Subscription Details */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<div>
<p className="text-sm text-gray-500 mb-1">Plan</p>
<p className="text-lg font-semibold text-gray-900">
{getTierDisplayName(subscription.tier)}
</p>
</div>
<div>
<p className="text-sm text-gray-500 mb-1">Monthly Price</p>
<p className="text-lg font-semibold text-gray-900">
{getTierPrice(subscription.tier)}/month
</p>
</div>
<div>
<p className="text-sm text-gray-500 mb-1">Storage Limit</p>
<p className="text-lg font-semibold text-gray-900">
{subscription.storage_limit_gb} GB
</p>
</div>
<div>
<p className="text-sm text-gray-500 mb-1">Current Usage</p>
<p className="text-lg font-semibold text-gray-900">
{parseFloat(subscription.current_usage_gb).toFixed(2)} GB
</p>
</div>
</div>
{/* Storage Usage Bar */}
<div className="mb-6">
<p className="text-sm text-gray-500 mb-2">Storage Usage</p>
<div className="w-full bg-gray-200 rounded-full h-4">
<div
className="bg-orange-500 h-4 rounded-full"
style={{
width: `${Math.min(
(parseFloat(subscription.current_usage_gb) /
subscription.storage_limit_gb) *
100,
100
)}%`,
}}
></div>
</div>
<p className="text-xs text-gray-500 mt-2">
{parseFloat(subscription.current_usage_gb).toFixed(2)} GB of{" "}
{subscription.storage_limit_gb} GB used
</p>
</div>
{/* Billing Info */}
<div className="border-t pt-4">
<p className="text-sm text-gray-500 mb-1">Member Since</p>
<p className="text-gray-900">
{new Date(subscription.created_at).toLocaleDateString(
"en-US",
{
year: "numeric",
month: "long",
day: "numeric",
}
)}
</p>
</div>
{/* Actions */}
{subscription.status === "ACTIVE" && (
<div className="mt-6 pt-6 border-t">
<Button
onClick={handleCancelSubscription}
variant="outline"
className="border-red-300 text-red-600 hover:bg-red-50 hover:border-red-400"
>
Cancel Subscription
</Button>
</div>
)}
</div>
{/* Upgrade/Downgrade Options */}
<div className="bg-white rounded-lg shadow-lg p-6">
<h3 className="text-xl font-bold text-gray-900 mb-4">
Change Plan
</h3>
<p className="text-gray-600 mb-4">
Looking to upgrade or downgrade? Cancel your current
subscription and choose a new plan.
</p>
<Button
onClick={() => router.push("/")}
className="bg-orange-500 hover:bg-orange-600 text-white font-medium px-6 py-3 rounded-md"
>
Browse All Plans
</Button>
</div>
</div>
)}
</div>
</div>
);
}

@ -0,0 +1,144 @@
"use client";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
import { useAuth } from "@/lib/auth-context";
import { useState } from "react";
import UploadArea from "@/components/UploadArea";
export default function UploadPage() {
const { isAuthenticated, user } = useAuth();
const router = useRouter();
const [userSubscription, setUserSubscription] = useState<any>(null);
const [authInitialized, setAuthInitialized] = useState(false);
// Wait for auth to initialize from localStorage
useEffect(() => {
const timer = setTimeout(() => {
setAuthInitialized(true);
}, 100);
return () => clearTimeout(timer);
}, []);
// Check authentication and fetch subscription
useEffect(() => {
if (!authInitialized) return;
if (!isAuthenticated || !user) {
router.push("/login?redirect=/upload");
return;
}
// Fetch user subscription
const fetchSubscription = async () => {
if (!user?.id) return;
try {
const response = await fetch("/api/subscriptions/me", {
headers: {
"user-id": user.id,
},
});
if (response.ok) {
const data = await response.json();
setUserSubscription(data.subscription);
}
} catch (error) {
console.error("Error fetching subscription:", error);
}
};
fetchSubscription();
}, [authInitialized, isAuthenticated, user, router]);
// Show loading while auth initializes
if (!authInitialized || !isAuthenticated) {
return (
<div className="min-h-screen flex items-center justify-center">
Loading...
</div>
);
}
// If no subscription found after loading, treat it as no subscription
// Don't block the page from showing
const hasActiveSubscription =
userSubscription && userSubscription.status === "ACTIVE";
return (
<div className="min-h-screen bg-gray-50 py-12">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Header */}
<div className="mb-8">
<h1 className="text-4xl font-bold text-gray-900 mb-2">
Upload Files
</h1>
{/* <p className="text-gray-600">
Upload your .blend files to start rendering
</p> */}
</div>
{/* Subscription Warning */}
{!hasActiveSubscription && (
<div className="bg-red-50 border-2 border-red-500 rounded-lg p-6 mb-8">
<p className="text-red-800 font-semibold text-lg">
Subscribe to use our services
</p>
<p className="text-red-700 text-sm mt-2">
You need an active subscription to upload files. Subscribe to a
plan to continue.
</p>
<button
onClick={() => router.push("/")}
className="mt-4 bg-red-600 hover:bg-red-700 text-white px-6 py-2 rounded-md text-sm font-medium"
>
Browse Plans
</button>
</div>
)}
{/* Subscription Info - Only show if active */}
{hasActiveSubscription && userSubscription && (
<div className="bg-white rounded-lg shadow-md p-6 mb-8">
<h2 className="text-lg font-semibold text-gray-900 mb-2">
Your {userSubscription.tier} Plan
</h2>
<p className="text-sm text-gray-600">
Storage:{" "}
{parseFloat(userSubscription.current_usage_gb).toFixed(2)} GB /{" "}
{userSubscription.storage_limit_gb} GB used
</p>
<div className="mt-3 w-full bg-gray-200 rounded-full h-2">
<div
className="bg-orange-500 h-2 rounded-full"
style={{
width: `${Math.min(
(parseFloat(userSubscription.current_usage_gb) /
userSubscription.storage_limit_gb) *
100,
100
)}%`,
}}
></div>
</div>
</div>
)}
{/* Upload Area */}
<div className="bg-white rounded-lg shadow-lg p-8">
<h2 className="text-2xl font-bold text-gray-900 mb-6">
Start by uploading at least one .blend file
</h2>
<div
className={
!hasActiveSubscription ? "pointer-events-none opacity-50" : ""
}
>
<UploadArea className="w-full shadow-sm" />
</div>
</div>
</div>
</div>
);
}

@ -0,0 +1,79 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import { useAuth } from "@/lib/auth-context";
interface StripePaymentButtonProps {
tier: "basic" | "pro" | "enterprise";
amount: number;
storageLimit: number;
}
export function StripePaymentButton({
tier,
amount,
storageLimit,
}: StripePaymentButtonProps) {
const [loading, setLoading] = useState(false);
const { user } = useAuth();
const router = useRouter();
const handlePayment = async () => {
if (!user) {
// Redirect to login page
router.push("/login?redirect=/pricing");
return;
}
setLoading(true);
try {
const response = await fetch("/api/create-checkout-session", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
tier,
userId: user.id,
}),
});
// Check if response is JSON before parsing
const contentType = response.headers.get("content-type");
if (!contentType || !contentType.includes("application/json")) {
throw new Error(
"Invalid response from server. Please check your Stripe configuration."
);
}
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || "Failed to create checkout session");
}
// Redirect to Stripe Checkout
if (data.url) {
window.location.href = data.url;
}
} catch (error) {
console.error("Payment error:", error);
alert("Failed to start payment process. Please try again.");
} finally {
setLoading(false);
}
};
return (
<Button
onClick={handlePayment}
disabled={loading}
className="w-full bg-orange-500 hover:bg-orange-600 text-white font-medium px-6 py-3 rounded-md transition-colors"
>
{loading ? "Processing..." : `Subscribe - $${amount}/month`}
</Button>
);
}

@ -0,0 +1,23 @@
import Stripe from "stripe";
// Initialize Stripe with your secret key from environment variables
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "", {
apiVersion: "2024-11-20.acacia",
typescript: true,
});
// Helper function to format amount for Stripe (convert to cents)
export function formatAmountForStripe(
amount: number,
currency: string = "usd"
): number {
return Math.round(amount * 100);
}
// Helper function to format amount from Stripe (convert from cents)
export function formatAmountFromStripe(
amount: number,
currency: string = "usd"
): number {
return Math.round(amount) / 100;
}

61
package-lock.json generated

@ -13,6 +13,7 @@
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-navigation-menu": "^1.2.14",
"@radix-ui/react-slot": "^1.2.3",
"@stripe/stripe-js": "^8.1.0",
"@types/bcryptjs": "^3.0.0",
"@types/jsonwebtoken": "^9.0.10",
"@types/pg": "^8.15.5",
@ -28,6 +29,7 @@
"react-dom": "19.2.0",
"react-hook-form": "^7.65.0",
"sonner": "^2.0.7",
"stripe": "^19.1.0",
"tailwind-merge": "^3.3.1",
"zod": "^4.1.12"
},
@ -1846,6 +1848,14 @@
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="
},
"node_modules/@stripe/stripe-js": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-8.1.0.tgz",
"integrity": "sha512-bhhi0iSHDFRa2pPVv3WOHC6x/iGEu5AZqIiAvXsT8VOucsEre9gzgsK0jFzbzfGW2eeubF+mCdTTYwNAQCMJKg==",
"engines": {
"node": ">=12.16"
}
},
"node_modules/@swc/helpers": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
@ -3115,7 +3125,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dev": true,
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
@ -3128,7 +3137,6 @@
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"dev": true,
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
@ -3407,7 +3415,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dev": true,
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
@ -3522,7 +3529,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"dev": true,
"engines": {
"node": ">= 0.4"
}
@ -3531,7 +3537,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"engines": {
"node": ">= 0.4"
}
@ -3567,7 +3572,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dev": true,
"dependencies": {
"es-errors": "^1.3.0"
},
@ -4176,7 +4180,6 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@ -4232,7 +4235,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dev": true,
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
@ -4264,7 +4266,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dev": true,
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
@ -4346,7 +4347,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true,
"engines": {
"node": ">= 0.4"
},
@ -4418,7 +4418,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"dev": true,
"engines": {
"node": ">= 0.4"
},
@ -4445,7 +4444,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"dependencies": {
"function-bind": "^1.1.2"
},
@ -5466,7 +5464,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"dev": true,
"engines": {
"node": ">= 0.4"
}
@ -5688,7 +5685,6 @@
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"dev": true,
"engines": {
"node": ">= 0.4"
},
@ -6089,6 +6085,20 @@
"node": ">=6"
}
},
"node_modules/qs": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
"dependencies": {
"side-channel": "^1.1.0"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@ -6538,7 +6548,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
"dev": true,
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3",
@ -6557,7 +6566,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
"dev": true,
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3"
@ -6573,7 +6581,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
"dev": true,
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
@ -6591,7 +6598,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
"dev": true,
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
@ -6778,6 +6784,25 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/stripe": {
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/stripe/-/stripe-19.1.0.tgz",
"integrity": "sha512-FjgIiE98dMMTNssfdjMvFdD4eZyEzdWAOwPYqzhPRNZeg9ggFWlPXmX1iJKD5pPIwZBaPlC3SayQQkwsPo6/YQ==",
"dependencies": {
"qs": "^6.11.0"
},
"engines": {
"node": ">=16"
},
"peerDependencies": {
"@types/node": ">=16"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/styled-jsx": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",

@ -14,6 +14,7 @@
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-navigation-menu": "^1.2.14",
"@radix-ui/react-slot": "^1.2.3",
"@stripe/stripe-js": "^8.1.0",
"@types/bcryptjs": "^3.0.0",
"@types/jsonwebtoken": "^9.0.10",
"@types/pg": "^8.15.5",
@ -29,6 +30,7 @@
"react-dom": "19.2.0",
"react-hook-form": "^7.65.0",
"sonner": "^2.0.7",
"stripe": "^19.1.0",
"tailwind-merge": "^3.3.1",
"zod": "^4.1.12"
},

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

Loading…
Cancel
Save