From bfedd5fdaa63e51074bc52ce92ec93ad4d726572 Mon Sep 17 00:00:00 2001 From: miranshala Date: Tue, 28 Oct 2025 10:59:15 +0100 Subject: [PATCH] subscribe stripe e kto --- app/api/auth/register/route.ts | 8 +- app/api/create-checkout-session/route.ts | 123 +++++++++ app/api/subscriptions/cancel/route.ts | 42 ++++ app/api/subscriptions/me/route.ts | 53 ++++ app/api/webhooks/stripe/route.ts | 124 ++++++++++ app/page.tsx | 200 +++++++++++++-- app/subscriptions/page.tsx | 303 +++++++++++++++++++++++ app/upload/page.tsx | 144 +++++++++++ components/StripePaymentButton.tsx | 79 ++++++ lib/stripe.ts | 23 ++ package-lock.json | 61 +++-- package.json | 2 + public/creativity.png | Bin 0 -> 201677 bytes 13 files changed, 1119 insertions(+), 43 deletions(-) create mode 100644 app/api/create-checkout-session/route.ts create mode 100644 app/api/subscriptions/cancel/route.ts create mode 100644 app/api/subscriptions/me/route.ts create mode 100644 app/api/webhooks/stripe/route.ts create mode 100644 app/subscriptions/page.tsx create mode 100644 app/upload/page.tsx create mode 100644 components/StripePaymentButton.tsx create mode 100644 lib/stripe.ts create mode 100644 public/creativity.png diff --git a/app/api/auth/register/route.ts b/app/api/auth/register/route.ts index aca383f..df9d4bb 100644 --- a/app/api/auth/register/route.ts +++ b/app/api/auth/register/route.ts @@ -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, diff --git a/app/api/create-checkout-session/route.ts b/app/api/create-checkout-session/route.ts new file mode 100644 index 0000000..2981c74 --- /dev/null +++ b/app/api/create-checkout-session/route.ts @@ -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 } + ); + } +} diff --git a/app/api/subscriptions/cancel/route.ts b/app/api/subscriptions/cancel/route.ts new file mode 100644 index 0000000..31bef0a --- /dev/null +++ b/app/api/subscriptions/cancel/route.ts @@ -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 } + ); + } +} diff --git a/app/api/subscriptions/me/route.ts b/app/api/subscriptions/me/route.ts new file mode 100644 index 0000000..932bee8 --- /dev/null +++ b/app/api/subscriptions/me/route.ts @@ -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 } + ); + } +} diff --git a/app/api/webhooks/stripe/route.ts b/app/api/webhooks/stripe/route.ts new file mode 100644 index 0000000..b48a83d --- /dev/null +++ b/app/api/webhooks/stripe/route.ts @@ -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(); + } +} diff --git a/app/page.tsx b/app/page.tsx index f75f1c3..564052f 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -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(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 (
+ {/* Success Message Banner */} + {showSuccess && ( +
+ + Subscription successful! Welcome to Reya Render. +
+ )} + {/* Sticky Navigation */}
-