From 4d148cebd936cc21420f5e4e56ee7e02403a85df Mon Sep 17 00:00:00 2001 From: semz-ui Date: Sat, 11 Oct 2025 11:24:14 +0100 Subject: [PATCH 01/10] new update --- apps/web/app/dashboard/plan/page.tsx | 10 ++++ .../dashboard/settings/BillingInfo.tsx | 4 +- .../dashboard/settings/UpgradePlan.tsx | 56 +++++++++++++++++++ .../components/landingPage/PricingSection.tsx | 12 +++- 4 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 apps/web/app/dashboard/plan/page.tsx create mode 100644 apps/web/components/dashboard/settings/UpgradePlan.tsx diff --git a/apps/web/app/dashboard/plan/page.tsx b/apps/web/app/dashboard/plan/page.tsx new file mode 100644 index 0000000..c433694 --- /dev/null +++ b/apps/web/app/dashboard/plan/page.tsx @@ -0,0 +1,10 @@ +import PricingSection from '@/components/landingPage/PricingSection' +import React from 'react' + +const page = () => { + return ( + + ) +} + +export default page \ No newline at end of file diff --git a/apps/web/components/dashboard/settings/BillingInfo.tsx b/apps/web/components/dashboard/settings/BillingInfo.tsx index 19b9d71..ca69467 100644 --- a/apps/web/components/dashboard/settings/BillingInfo.tsx +++ b/apps/web/components/dashboard/settings/BillingInfo.tsx @@ -7,6 +7,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com import { useSettings } from "@/hooks/useSettings"; import { CreditCard, Loader2 } from "lucide-react" import { useState, useEffect } from "react" +import { useRouter } from "next/navigation" // A type for the component's data state interface BillingData { @@ -18,6 +19,7 @@ interface BillingData { export function BillingInfo() { const { updateBilling, loadingBilling } = useSettings() + const router = useRouter() const [isLoadingData, setIsLoadingData] = useState(true) @@ -80,7 +82,7 @@ export function BillingInfo() { {isLoadingData ? ( ) : ( - + )} diff --git a/apps/web/components/dashboard/settings/UpgradePlan.tsx b/apps/web/components/dashboard/settings/UpgradePlan.tsx new file mode 100644 index 0000000..a66f1c3 --- /dev/null +++ b/apps/web/components/dashboard/settings/UpgradePlan.tsx @@ -0,0 +1,56 @@ +// components/Modal.tsx +"use client" + +import { Button } from "@/components/ui/button" +import { Dialog, DialogClose, DialogContent, DialogDescription, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger } from "@/components/ui/dialog" +// import * as Dialog from "@radix-ui/react-dialog" +import { X } from "lucide-react" + +export default function UpgradePlan() { + return ( + + + + + + + {/*OverlayDialogOverlay */} + + + {/* Content */} + +
+ Select Plan + {/* + + */} +
+ + + This is an example modal built using Radix UI Dialog. + + +
+ + + + +
+
+
+
+ ) +} diff --git a/apps/web/components/landingPage/PricingSection.tsx b/apps/web/components/landingPage/PricingSection.tsx index ad3ea0c..9f599f4 100644 --- a/apps/web/components/landingPage/PricingSection.tsx +++ b/apps/web/components/landingPage/PricingSection.tsx @@ -1,9 +1,14 @@ +"use client" import { cn } from "@/lib/utils" import { Link } from "lucide-react" import { Button } from "../ui/button" import { WobbleCard } from "../ui/wobble-card" +import { usePathname, useRouter } from "next/navigation" export default function PricingSection() { + const router = useRouter() + + const pathname = usePathname() const pricing = [ { @@ -33,9 +38,13 @@ export default function PricingSection() { return (
-

+ { + pathname === "/" ?

Pricing +

:

+ Recommended plan for you

+ }

Choose the plan that works best for your content creation needs.

@@ -64,6 +73,7 @@ export default function PricingSection() { /mo
+ {pathname !== "/" && }

{option.description}

From dd4b8a25d42b50e7784a8e6d20563423c9c805c5 Mon Sep 17 00:00:00 2001 From: semz-ui Date: Sat, 11 Oct 2025 19:44:25 +0100 Subject: [PATCH 02/10] stripe subscription integration --- .env.example | 7 +- apps/api/env.example | 2 + apps/web/app/api/stripe/route.ts | 83 +++++++++++ apps/web/app/api/stripe/webhook/route.ts | 141 ++++++++++++++++++ .../components/landingPage/PricingSection.tsx | 35 ++++- apps/web/components/ui/wobble-card.tsx | 2 +- apps/web/lib/stripe.ts | 5 + apps/web/middleware.ts | 2 + apps/web/package.json | 1 + pnpm-lock.yaml | 18 ++- 10 files changed, 285 insertions(+), 11 deletions(-) create mode 100644 apps/web/app/api/stripe/route.ts create mode 100644 apps/web/app/api/stripe/webhook/route.ts create mode 100644 apps/web/lib/stripe.ts diff --git a/.env.example b/.env.example index 07fc597..4843fd5 100644 --- a/.env.example +++ b/.env.example @@ -8,5 +8,8 @@ GOOGLE_GENERATIVE_AI_API_KEY=your_google_generative_ai_api_key LINGO_API_KEY=your_lingo_api_key YOUTUBE_API_KEY=your_youtube_api_key NEXT_PUBLIC_BASE_URL=http://localhost:3000/ -GOOGLE_CLIENT_SECRET= your_google_aouth_client_secret -GOOGLE_CLIENT_ID= your_google_aouth_client_id \ No newline at end of file +GOOGLE_CLIENT_SECRET=your_google_aouth_client_secret +GOOGLE_CLIENT_ID=your_google_aouth_client_id +NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=your_stripe_publishable_key +STRIPE_SECRET_KEY=your_stripe_secret_key +STRIPE_WEBHOOK_SECRET=your_stripe_webhook_secret \ No newline at end of file diff --git a/apps/api/env.example b/apps/api/env.example index 93e788e..2106d05 100644 --- a/apps/api/env.example +++ b/apps/api/env.example @@ -14,11 +14,13 @@ SUPABASE_SERVICE_KEY=your_supabase_service_role_key # Google AI Studio API Key for script generation GOOGLE_AI_STUDIO_API_KEY=your_google_ai_studio_api_key + # Optional: Additional AI Services # OPENAI_API_KEY=your_openai_api_key # ELEVENLABS_API_KEY=your_elevenlabs_api_key # Optional: Payment Processing +# NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=your_stripe_publishable_key # STRIPE_SECRET_KEY=your_stripe_secret_key # STRIPE_WEBHOOK_SECRET=your_stripe_webhook_secret diff --git a/apps/web/app/api/stripe/route.ts b/apps/web/app/api/stripe/route.ts new file mode 100644 index 0000000..0995623 --- /dev/null +++ b/apps/web/app/api/stripe/route.ts @@ -0,0 +1,83 @@ +import { NextResponse } from "next/server"; +import { stripe } from "@/lib/stripe"; +import { createClient } from "@/lib/supabase/server"; + + +export async function POST(req: Request) { + const supabase = await createClient(); + try { + const { data: { user }, error: userError } = await supabase.auth.getUser(); + if (userError || !user) { + return NextResponse.json({ message: 'Unauthorized' }, { status: 401 }); + } + + const { priceId, customerEmail } = await req.json(); + + const { data: existing } = await supabase + .from("subscriptions") + .select("stripe_customer_id") + .eq("user_id", user.id) + .single() + const { data: profile } = await supabase + .from("profiles") + .select("full_name") + .eq("user_id", user.id) + .single() + + console.log(user.id,"profile") + + let customerId = existing?.stripe_customer_id + + if (!customerId) { + const customer = await stripe.customers.create({ + name: profile?.full_name, + metadata: { user_id: user.id }, + email: user.email + }) + customerId = customer.id + } + + console.log(customerId, "customerId") + + const session = await stripe.checkout.sessions.create({ + mode: "subscription", + payment_method_types: ["card"], + customer: customerId, + line_items: [ + { + price: priceId, // e.g. "price_12345" + quantity: 1, + }, + ], + success_url: `${process.env.NEXT_PUBLIC_BASE_URL}/dashboard?session_id={CHECKOUT_SESSION_ID}`, + cancel_url: `${process.env.NEXT_PUBLIC_BASE_URL}/cancel`, + metadata:{ + user_id: user.id + } + }); + + + // const { error } = await supabase.from("subscriptions").upsert({ + // user_id: user.id, + // payment_details: session, + // stripe_customer_id: customerId, + // stripe_subscription_id: session.id, + // subscription_type: "active", + // }) + + // if (error) { + // console.log(error) + // return NextResponse.json({ message: 'Something happened', error }, { status: 401 }); + // } + + + return NextResponse.json({ + url: session.url + // url: "Done" + + }); + } catch (error: any) { + console.error("Stripe Error:", error); + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} diff --git a/apps/web/app/api/stripe/webhook/route.ts b/apps/web/app/api/stripe/webhook/route.ts new file mode 100644 index 0000000..dcbfc59 --- /dev/null +++ b/apps/web/app/api/stripe/webhook/route.ts @@ -0,0 +1,141 @@ +import { NextResponse } from "next/server"; +import { stripe } from "@/lib/stripe"; +import { headers } from "next/headers"; +import { createClient } from "@/lib/supabase/server"; + +export async function POST(req: Request) { + const supabase = await createClient(); + const body = await req.text(); + const headersList = await headers(); + const sig = headersList.get("stripe-signature"); + + let event; + + try { + event = stripe.webhooks.constructEvent( + body, + sig!, + process.env.STRIPE_WEBHOOK_SECRET! + ); + } catch (err: any) { + console.error("❌ Webhook signature verification failed:", err.message); + return NextResponse.json({ error: err.message }, { status: 400 }); + } + + try { + switch (event.type) { + case "customer.subscription.created": { + const session = event.data.object as any; + const subscriptionId = session.subscription; + const customerId = session.customer; + let userId = session.metadata?.user_id ?? session.metadata?.userId; + + if (!userId) { + try { + const customer = await stripe.customers.retrieve(customerId); + userId = (customer as any).metadata?.user_id ?? (customer as any).metadata?.userId; + } catch (err) { + console.error("Failed to retrieve Stripe customer:", err); + } + } + + if (!userId) { + console.warn("No userId found for subscription.created event, skipping credit increment."); + break; + } + + console.log("subscription created", session); + + // Read current credits and update + try { + const { data: userRow, error: fetchError } = await supabase + .from("profiles") + .select("credits") + .eq("user_id", userId) + .single(); + + if (fetchError) { + console.error("Failed to fetch user for credit update:", fetchError); + break; + } + + const currentCredits = (userRow as any)?.credits ?? 0; + console.log("currentCredits: ", currentCredits) + const newCredits = currentCredits + 60; + + const { error: updateError } = await supabase + .from("profiles") + .update({ credits: newCredits }) + .eq("user_id", userId); + + if (updateError) { + console.error("Failed to update user credits:", updateError); + break; + } + + console.log(`Added 60 credits to user ${userId}. New total: ${newCredits}`); + } catch (err) { + console.error("Error updating credits:", err); + } + + // await db.user.update({ + // where: { id: userId }, + // data: { + // stripeSubscriptionId: subscriptionId, + // status: "active", + // }, + // }); + break; + } + + case "invoice.payment_succeeded": { + const invoice = event.data.object as any; + const customerId = invoice.customer; + const customer = await stripe.customers.retrieve(customerId); + const userId = (customer as any).metadata.userId; + console.log(invoice, "invoice") + + // await db.user.update({ + // where: { id: userId }, + // data: { status: "active" }, + // }); + break; + } + + case "invoice.payment_failed": { + const invoice = event.data.object as any; + const customerId = invoice.customer; + const customer = await stripe.customers.retrieve(customerId); + const userId = (customer as any).metadata.userId; + console.log(invoice, "invoice") + + // await db.user.update({ + // where: { id: userId }, + // data: { status: "past_due" }, + // }); + break; + } + + case "customer.subscription.deleted": { + const subscription = event.data.object as any; + const customerId = subscription.customer; + const customer = await stripe.customers.retrieve(customerId); + const userId = (customer as any).metadata.userId; + + // await db.user.update({ + // where: { id: userId }, + // data: { status: "canceled" }, + // }); + break; + } + + default: + console.log(`Unhandled event type: ${event.type}`); + } + + return NextResponse.json({ received: true }); + } catch (err: any) { + console.error("Webhook handler failed:", err); + return NextResponse.json({ error: err.message }, { status: 500 }); + } +} diff --git a/apps/web/components/landingPage/PricingSection.tsx b/apps/web/components/landingPage/PricingSection.tsx index 9f599f4..99c9ccc 100644 --- a/apps/web/components/landingPage/PricingSection.tsx +++ b/apps/web/components/landingPage/PricingSection.tsx @@ -4,11 +4,13 @@ import { Link } from "lucide-react" import { Button } from "../ui/button" import { WobbleCard } from "../ui/wobble-card" import { usePathname, useRouter } from "next/navigation" +import { toast } from "sonner" +import { useState } from "react" export default function PricingSection() { const router = useRouter() - const pathname = usePathname() + const [loading, setLoading] = useState(false) const pricing = [ { @@ -35,15 +37,36 @@ export default function PricingSection() { }, ] + const handlePlan = async (e: React.MouseEvent) => { + e.preventDefault() + console.log("clicked") + // if (loading) return + setLoading(true) + try { + const response = await fetch("/api/stripe", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ priceId: "price_1SH1sSALTcE9tQgYn5aBB7NT", customerEmail: "test@gmail.com" }) + }) + if (!response.ok) throw new Error("Failed to generate referral code") + const data = await response.json() + if (data.url) window.location.href = data.url; + } catch (error: any) { + toast.error("Error generating referral code", { description: error.message }) + } finally { + setLoading(false) + } + } + return (
{ pathname === "/" ?

- Pricing -

:

- Recommended plan for you -

+ Pricing + :

+ Recommended plan for you +

}

Choose the plan that works best for your content creation needs. @@ -73,7 +96,7 @@ export default function PricingSection() { /mo

- {pathname !== "/" && } + {pathname !== "/" && }

{option.description}

diff --git a/apps/web/components/ui/wobble-card.tsx b/apps/web/components/ui/wobble-card.tsx index 2018f3d..bf81973 100644 --- a/apps/web/components/ui/wobble-card.tsx +++ b/apps/web/components/ui/wobble-card.tsx @@ -68,7 +68,7 @@ export const WobbleCard = ({ const Noise = () => { return (
=0.6'} dependencies: side-channel: 1.1.0 - dev: true /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -9772,6 +9773,19 @@ packages: engines: {node: '>=8'} dev: true + /stripe@19.1.0(@types/node@22.17.0): + resolution: {integrity: sha512-FjgIiE98dMMTNssfdjMvFdD4eZyEzdWAOwPYqzhPRNZeg9ggFWlPXmX1iJKD5pPIwZBaPlC3SayQQkwsPo6/YQ==} + engines: {node: '>=16'} + peerDependencies: + '@types/node': '>=16' + peerDependenciesMeta: + '@types/node': + optional: true + dependencies: + '@types/node': 22.17.0 + qs: 6.14.0 + dev: false + /strtok3@10.3.4: resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==} engines: {node: '>=18'} From e5eac3ada291b980a54070ac00674e8646dd9439 Mon Sep 17 00:00:00 2001 From: semz-ui Date: Sat, 18 Oct 2025 08:37:44 +0100 Subject: [PATCH 03/10] stripe continuation --- apps/web/app/api/stripe/route.ts | 106 +++--- apps/web/app/api/stripe/webhook/route.ts | 49 ++- .../components/landingPage/PricingSection.tsx | 67 +++- apps/web/lib/supabase/webhook.ts | 6 + apps/web/supabase/.gitignore | 8 + apps/web/supabase/config.toml | 349 ++++++++++++++++++ .../20251017234915_remote_schema.sql | 165 +++++++++ .../20251018002813_remote_schema.sql | 163 ++++++++ apps/web/supabase/migrations/schema.sql | 43 +++ apps/web/types/subscription.ts | 1 + 10 files changed, 873 insertions(+), 84 deletions(-) create mode 100644 apps/web/lib/supabase/webhook.ts create mode 100644 apps/web/supabase/.gitignore create mode 100644 apps/web/supabase/config.toml create mode 100644 apps/web/supabase/migrations/20251017234915_remote_schema.sql create mode 100644 apps/web/supabase/migrations/20251018002813_remote_schema.sql create mode 100644 apps/web/supabase/migrations/schema.sql create mode 100644 apps/web/types/subscription.ts diff --git a/apps/web/app/api/stripe/route.ts b/apps/web/app/api/stripe/route.ts index 0995623..9164e4d 100644 --- a/apps/web/app/api/stripe/route.ts +++ b/apps/web/app/api/stripe/route.ts @@ -4,78 +4,64 @@ import { createClient } from "@/lib/supabase/server"; export async function POST(req: Request) { - const supabase = await createClient(); + const supabase = await createClient(); try { const { data: { user }, error: userError } = await supabase.auth.getUser(); - if (userError || !user) { - return NextResponse.json({ message: 'Unauthorized' }, { status: 401 }); - } - - const { priceId, customerEmail } = await req.json(); + if (userError || !user) { + return NextResponse.json({ message: 'Unauthorized' }, { status: 401 }); + } - const { data: existing } = await supabase - .from("subscriptions") - .select("stripe_customer_id") - .eq("user_id", user.id) - .single() - const { data: profile } = await supabase - .from("profiles") - .select("full_name") - .eq("user_id", user.id) - .single() - - console.log(user.id,"profile") - - let customerId = existing?.stripe_customer_id - - if (!customerId) { - const customer = await stripe.customers.create({ - name: profile?.full_name, - metadata: { user_id: user.id }, - email: user.email - }) - customerId = customer.id - } + const { price_id, sub_type } = await req.json(); - console.log(customerId, "customerId") + const { data: existing } = await supabase + .from("subscriptions") + .select("stripe_customer_id") + .eq("user_id", user.id) + .single() + // const { data: profile } = await supabase + // .from("profiles") + // .select("full_name") + // .eq("user_id", user.id) + // .single() - const session = await stripe.checkout.sessions.create({ - mode: "subscription", - payment_method_types: ["card"], - customer: customerId, - line_items: [ - { - price: priceId, // e.g. "price_12345" - quantity: 1, - }, - ], - success_url: `${process.env.NEXT_PUBLIC_BASE_URL}/dashboard?session_id={CHECKOUT_SESSION_ID}`, - cancel_url: `${process.env.NEXT_PUBLIC_BASE_URL}/cancel`, - metadata:{ - user_id: user.id - } - }); + + // let customerId = existing?.stripe_customer_id + // console.log(existing, "profile") + // if (!customerId) { + // const customer = await stripe.customers.create({ + // name: profile?.full_name, + // metadata: { user_id: user.id, sub_type: sub_type }, + // email: user.email + // }) + // customerId = customer.id + // } - // const { error } = await supabase.from("subscriptions").upsert({ - // user_id: user.id, - // payment_details: session, - // stripe_customer_id: customerId, - // stripe_subscription_id: session.id, - // subscription_type: "active", - // }) + // const session = await stripe.checkout.sessions.create({ + // mode: "subscription", + // payment_method_types: ["card"], + // customer: customerId, + // line_items: [ + // { + // price: price_id, // e.g. "price_12345" + // quantity: 1, + // }, + // ], + // success_url: `${process.env.NEXT_PUBLIC_BASE_URL}/dashboard?session_id={CHECKOUT_SESSION_ID}`, + // cancel_url: `${process.env.NEXT_PUBLIC_BASE_URL}/cancel`, + // metadata: { + // user_id: user.id + // } + // }); - // if (error) { - // console.log(error) - // return NextResponse.json({ message: 'Something happened', error }, { status: 401 }); - // } + return NextResponse.json({ - url: session.url - // url: "Done" + // url: session.url + url: "Done" - }); + }); } catch (error: any) { console.error("Stripe Error:", error); return NextResponse.json({ error: error.message }, { status: 500 }); diff --git a/apps/web/app/api/stripe/webhook/route.ts b/apps/web/app/api/stripe/webhook/route.ts index dcbfc59..10181d8 100644 --- a/apps/web/app/api/stripe/webhook/route.ts +++ b/apps/web/app/api/stripe/webhook/route.ts @@ -1,10 +1,12 @@ import { NextResponse } from "next/server"; import { stripe } from "@/lib/stripe"; import { headers } from "next/headers"; -import { createClient } from "@/lib/supabase/server"; +import { supabaseAdmin } from "@/lib/supabase/webhook"; +import { SubType } from "@/types/subscription"; + + export async function POST(req: Request) { - const supabase = await createClient(); const body = await req.text(); const headersList = await headers(); const sig = headersList.get("stripe-signature"); @@ -28,7 +30,9 @@ export async function POST(req: Request) { const session = event.data.object as any; const subscriptionId = session.subscription; const customerId = session.customer; - let userId = session.metadata?.user_id ?? session.metadata?.userId; + let userId = session.metadata?.user_id; + let sub_type: SubType = session.metadata?.sub_type; + console.log("customerId: ", customerId) if (!userId) { try { @@ -44,11 +48,11 @@ export async function POST(req: Request) { break; } - console.log("subscription created", session); + // console.log("subscription created", userId); // Read current credits and update try { - const { data: userRow, error: fetchError } = await supabase + const { data: userRow, error: fetchError } = await supabaseAdmin .from("profiles") .select("credits") .eq("user_id", userId) @@ -58,12 +62,11 @@ export async function POST(req: Request) { console.error("Failed to fetch user for credit update:", fetchError); break; } - + const amount_to_add = sub_type === "Pro" ? 300 : 1000; const currentCredits = (userRow as any)?.credits ?? 0; - console.log("currentCredits: ", currentCredits) - const newCredits = currentCredits + 60; + const newCredits = currentCredits + amount_to_add - const { error: updateError } = await supabase + const { error: updateError } = await supabaseAdmin .from("profiles") .update({ credits: newCredits }) .eq("user_id", userId); @@ -73,7 +76,29 @@ export async function POST(req: Request) { break; } - console.log(`Added 60 credits to user ${userId}. New total: ${newCredits}`); + const { data: existing, error } = await supabaseAdmin + .from("subscriptions") + .select("stripe_customer_id") + .eq("user_id", userId) + .single() + + let ExistingCustomerId = existing?.stripe_customer_id + + if(ExistingCustomerId !== customerId) { + const { error } = await supabaseAdmin.from("subscriptions").upsert({ + user_id: userId, + payment_details: session, + stripe_customer_id: customerId, + stripe_subscription_id: session.id, + subscription_type: "active", + }) + + if (error) { + console.log(error, "errorrr") + return NextResponse.json({ message: 'Something happened', error }, { status: 401 }); + } + + } } catch (err) { console.error("Error updating credits:", err); } @@ -93,7 +118,7 @@ export async function POST(req: Request) { const customerId = invoice.customer; const customer = await stripe.customers.retrieve(customerId); const userId = (customer as any).metadata.userId; - console.log(invoice, "invoice") + // console.log(invoice, "invoice") // await db.user.update({ // where: { id: userId }, @@ -107,7 +132,7 @@ export async function POST(req: Request) { const customerId = invoice.customer; const customer = await stripe.customers.retrieve(customerId); const userId = (customer as any).metadata.userId; - console.log(invoice, "invoice") + // console.log(invoice, "invoice") // await db.user.update({ // where: { id: userId }, diff --git a/apps/web/components/landingPage/PricingSection.tsx b/apps/web/components/landingPage/PricingSection.tsx index 832dae2..a11215c 100644 --- a/apps/web/components/landingPage/PricingSection.tsx +++ b/apps/web/components/landingPage/PricingSection.tsx @@ -6,9 +6,49 @@ import { WobbleCard } from "../ui/wobble-card" import { usePathname, useRouter } from "next/navigation" import { toast } from "sonner" import { useState } from "react" +import { useSupabase } from "../supabase-provider" export default function PricingSection() { + const [loading, setLoading] = useState(false) + const { user } = useSupabase(); + const pathname = usePathname() + const router = useRouter() + + const pro__plan = process.env.NEXT_PUBLIC_PRO_PRICE + const enterprice_plan = process.env.NEXT_PUBLIC__ENTERPRICE_PLAN + + const handleStripeCheckout = async (price_id: string, sub_type: string) => { + if (loading) return + setLoading(true) + try { + const response = await fetch('/api/stripe', { + method: "POST", + body: JSON.stringify({ price_id,sub_type }) + }) + + if (!response.ok) { + toast.error("Something happened, please try again") + return + } + const data = await response.json() + // if (data.url) window.location.href = data.url; + } catch (error: any) { + toast.error("Failed to send issue report", { + description: error?.message || "Something went wrong. Please try again.", + }); + } finally { + setLoading(false) + } + } + + const handlePricingRoute = (price_id: string, sub_type: string) => { + if (user) { + handleStripeCheckout(price_id, sub_type) + } else { + router.push("/login") + } + } const pricing = [ { price: 0, @@ -23,14 +63,17 @@ export default function PricingSection() { isPopular: true, confess: "Get Started", description: "For serious content creators.", - features: ["300 credits", "Custom fine tuned model", "All script customization options", "Thumbnail & title generation", "Course module creaton"] + features: ["300 credits", "Custom fine tuned model", "All script customization options", "Thumbnail & title generation", "Course module creaton"], + stripe_pricing: pro__plan, + credit_to_be_added: 300 }, { price: 49, heading: "Enterprise", confess: "Contact Sales", description: "For professional YouTubers and teams.", - features: ["Everything in Pro", "Team collaboration", "Advanced analytics", "Priority support", "Custom integrations"] + features: ["Everything in Pro", "Team collaboration", "Advanced analytics", "Priority support", "Custom integrations"], + stripe_pricing: enterprice_plan }, ] @@ -100,24 +143,24 @@ export default function PricingSection() { ))} - + - - ) - })} + + + ) + )}
) diff --git a/apps/web/lib/supabase/webhook.ts b/apps/web/lib/supabase/webhook.ts new file mode 100644 index 0000000..9075fc9 --- /dev/null +++ b/apps/web/lib/supabase/webhook.ts @@ -0,0 +1,6 @@ +import { createClient } from '@supabase/supabase-js'; + +export const supabaseAdmin = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY! +); diff --git a/apps/web/supabase/.gitignore b/apps/web/supabase/.gitignore new file mode 100644 index 0000000..ad9264f --- /dev/null +++ b/apps/web/supabase/.gitignore @@ -0,0 +1,8 @@ +# Supabase +.branches +.temp + +# dotenvx +.env.keys +.env.local +.env.*.local diff --git a/apps/web/supabase/config.toml b/apps/web/supabase/config.toml new file mode 100644 index 0000000..9f0dc21 --- /dev/null +++ b/apps/web/supabase/config.toml @@ -0,0 +1,349 @@ +# For detailed configuration reference documentation, visit: +# https://supabase.com/docs/guides/local-development/cli/config +# A string used to distinguish different Supabase projects on the same host. Defaults to the +# working directory name when running `supabase init`. +project_id = "web" + +[api] +enabled = true +# Port to use for the API URL. +port = 54321 +# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API +# endpoints. `public` and `graphql_public` schemas are included by default. +schemas = ["public", "graphql_public"] +# Extra schemas to add to the search_path of every request. +extra_search_path = ["public", "extensions"] +# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size +# for accidental or malicious requests. +max_rows = 1000 + +[api.tls] +# Enable HTTPS endpoints locally using a self-signed certificate. +enabled = false +# Paths to self-signed certificate pair. +# cert_path = "../certs/my-cert.pem" +# key_path = "../certs/my-key.pem" + +[db] +# Port to use for the local database URL. +port = 54322 +# Port used by db diff command to initialize the shadow database. +shadow_port = 54320 +# The database major version to use. This has to be the same as your remote database's. Run `SHOW +# server_version;` on the remote database to check. +major_version = 17 + +[db.pooler] +enabled = false +# Port to use for the local connection pooler. +port = 54329 +# Specifies when a server connection can be reused by other clients. +# Configure one of the supported pooler modes: `transaction`, `session`. +pool_mode = "transaction" +# How many server connections to allow per user/database pair. +default_pool_size = 20 +# Maximum number of client connections allowed. +max_client_conn = 100 + +# [db.vault] +# secret_key = "env(SECRET_VALUE)" + +[db.migrations] +# If disabled, migrations will be skipped during a db push or reset. +enabled = true +# Specifies an ordered list of schema files that describe your database. +# Supports glob patterns relative to supabase directory: "./schemas/*.sql" +schema_paths = [] + +[db.seed] +# If enabled, seeds the database after migrations during a db reset. +enabled = true +# Specifies an ordered list of seed files to load during db reset. +# Supports glob patterns relative to supabase directory: "./seeds/*.sql" +sql_paths = ["./seed.sql"] + +[db.network_restrictions] +# Enable management of network restrictions. +enabled = false +# List of IPv4 CIDR blocks allowed to connect to the database. +# Defaults to allow all IPv4 connections. Set empty array to block all IPs. +allowed_cidrs = ["0.0.0.0/0"] +# List of IPv6 CIDR blocks allowed to connect to the database. +# Defaults to allow all IPv6 connections. Set empty array to block all IPs. +allowed_cidrs_v6 = ["::/0"] + +[realtime] +enabled = true +# Bind realtime via either IPv4 or IPv6. (default: IPv4) +# ip_version = "IPv6" +# The maximum length in bytes of HTTP request headers. (default: 4096) +# max_header_length = 4096 + +[studio] +enabled = true +# Port to use for Supabase Studio. +port = 54323 +# External URL of the API server that frontend connects to. +api_url = "http://127.0.0.1" +# OpenAI API Key to use for Supabase AI in the Supabase Studio. +openai_api_key = "env(OPENAI_API_KEY)" + +# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they +# are monitored, and you can view the emails that would have been sent from the web interface. +[inbucket] +enabled = true +# Port to use for the email testing server web interface. +port = 54324 +# Uncomment to expose additional ports for testing user applications that send emails. +# smtp_port = 54325 +# pop3_port = 54326 +# admin_email = "admin@email.com" +# sender_name = "Admin" + +[storage] +enabled = true +# The maximum file size allowed (e.g. "5MB", "500KB"). +file_size_limit = "50MiB" + +# Image transformation API is available to Supabase Pro plan. +# [storage.image_transformation] +# enabled = true + +# Uncomment to configure local storage buckets +# [storage.buckets.images] +# public = false +# file_size_limit = "50MiB" +# allowed_mime_types = ["image/png", "image/jpeg"] +# objects_path = "./images" + +[auth] +enabled = true +# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used +# in emails. +site_url = "http://127.0.0.1:3000" +# A list of *exact* URLs that auth providers are permitted to redirect to post authentication. +additional_redirect_urls = ["https://127.0.0.1:3000"] +# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week). +jwt_expiry = 3600 +# Path to JWT signing key. DO NOT commit your signing keys file to git. +# signing_keys_path = "./signing_keys.json" +# If disabled, the refresh token will never expire. +enable_refresh_token_rotation = true +# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds. +# Requires enable_refresh_token_rotation = true. +refresh_token_reuse_interval = 10 +# Allow/disallow new user signups to your project. +enable_signup = true +# Allow/disallow anonymous sign-ins to your project. +enable_anonymous_sign_ins = false +# Allow/disallow testing manual linking of accounts +enable_manual_linking = false +# Passwords shorter than this value will be rejected as weak. Minimum 6, recommended 8 or more. +minimum_password_length = 6 +# Passwords that do not meet the following requirements will be rejected as weak. Supported values +# are: `letters_digits`, `lower_upper_letters_digits`, `lower_upper_letters_digits_symbols` +password_requirements = "" + +[auth.rate_limit] +# Number of emails that can be sent per hour. Requires auth.email.smtp to be enabled. +email_sent = 2 +# Number of SMS messages that can be sent per hour. Requires auth.sms to be enabled. +sms_sent = 30 +# Number of anonymous sign-ins that can be made per hour per IP address. Requires enable_anonymous_sign_ins = true. +anonymous_users = 30 +# Number of sessions that can be refreshed in a 5 minute interval per IP address. +token_refresh = 150 +# Number of sign up and sign-in requests that can be made in a 5 minute interval per IP address (excludes anonymous users). +sign_in_sign_ups = 30 +# Number of OTP / Magic link verifications that can be made in a 5 minute interval per IP address. +token_verifications = 30 +# Number of Web3 logins that can be made in a 5 minute interval per IP address. +web3 = 30 + +# Configure one of the supported captcha providers: `hcaptcha`, `turnstile`. +# [auth.captcha] +# enabled = true +# provider = "hcaptcha" +# secret = "" + +[auth.email] +# Allow/disallow new user signups via email to your project. +enable_signup = true +# If enabled, a user will be required to confirm any email change on both the old, and new email +# addresses. If disabled, only the new email is required to confirm. +double_confirm_changes = true +# If enabled, users need to confirm their email address before signing in. +enable_confirmations = false +# If enabled, users will need to reauthenticate or have logged in recently to change their password. +secure_password_change = false +# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email. +max_frequency = "1s" +# Number of characters used in the email OTP. +otp_length = 6 +# Number of seconds before the email OTP expires (defaults to 1 hour). +otp_expiry = 3600 + +# Use a production-ready SMTP server +# [auth.email.smtp] +# enabled = true +# host = "smtp.sendgrid.net" +# port = 587 +# user = "apikey" +# pass = "env(SENDGRID_API_KEY)" +# admin_email = "admin@email.com" +# sender_name = "Admin" + +# Uncomment to customize email template +# [auth.email.template.invite] +# subject = "You have been invited" +# content_path = "./supabase/templates/invite.html" + +[auth.sms] +# Allow/disallow new user signups via SMS to your project. +enable_signup = false +# If enabled, users need to confirm their phone number before signing in. +enable_confirmations = false +# Template for sending OTP to users +template = "Your code is {{ .Code }}" +# Controls the minimum amount of time that must pass before sending another sms otp. +max_frequency = "5s" + +# Use pre-defined map of phone number to OTP for testing. +# [auth.sms.test_otp] +# 4152127777 = "123456" + +# Configure logged in session timeouts. +# [auth.sessions] +# Force log out after the specified duration. +# timebox = "24h" +# Force log out if the user has been inactive longer than the specified duration. +# inactivity_timeout = "8h" + +# This hook runs before a new user is created and allows developers to reject the request based on the incoming user object. +# [auth.hook.before_user_created] +# enabled = true +# uri = "pg-functions://postgres/auth/before-user-created-hook" + +# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used. +# [auth.hook.custom_access_token] +# enabled = true +# uri = "pg-functions:////" + +# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`. +[auth.sms.twilio] +enabled = false +account_sid = "" +message_service_sid = "" +# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead: +auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)" + +# Multi-factor-authentication is available to Supabase Pro plan. +[auth.mfa] +# Control how many MFA factors can be enrolled at once per user. +max_enrolled_factors = 10 + +# Control MFA via App Authenticator (TOTP) +[auth.mfa.totp] +enroll_enabled = false +verify_enabled = false + +# Configure MFA via Phone Messaging +[auth.mfa.phone] +enroll_enabled = false +verify_enabled = false +otp_length = 6 +template = "Your code is {{ .Code }}" +max_frequency = "5s" + +# Configure MFA via WebAuthn +# [auth.mfa.web_authn] +# enroll_enabled = true +# verify_enabled = true + +# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`, +# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`, +# `twitter`, `slack`, `spotify`, `workos`, `zoom`. +[auth.external.apple] +enabled = false +client_id = "" +# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead: +secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)" +# Overrides the default auth redirectUrl. +redirect_uri = "" +# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure, +# or any other third-party OIDC providers. +url = "" +# If enabled, the nonce check will be skipped. Required for local sign in with Google auth. +skip_nonce_check = false +# If enabled, it will allow the user to successfully authenticate when the provider does not return an email address. +email_optional = false + +# Allow Solana wallet holders to sign in to your project via the Sign in with Solana (SIWS, EIP-4361) standard. +# You can configure "web3" rate limit in the [auth.rate_limit] section and set up [auth.captcha] if self-hosting. +[auth.web3.solana] +enabled = false + +# Use Firebase Auth as a third-party provider alongside Supabase Auth. +[auth.third_party.firebase] +enabled = false +# project_id = "my-firebase-project" + +# Use Auth0 as a third-party provider alongside Supabase Auth. +[auth.third_party.auth0] +enabled = false +# tenant = "my-auth0-tenant" +# tenant_region = "us" + +# Use AWS Cognito (Amplify) as a third-party provider alongside Supabase Auth. +[auth.third_party.aws_cognito] +enabled = false +# user_pool_id = "my-user-pool-id" +# user_pool_region = "us-east-1" + +# Use Clerk as a third-party provider alongside Supabase Auth. +[auth.third_party.clerk] +enabled = false +# Obtain from https://clerk.com/setup/supabase +# domain = "example.clerk.accounts.dev" + +# OAuth server configuration +[auth.oauth_server] +# Enable OAuth server functionality +enabled = false +# Path for OAuth consent flow UI +authorization_url_path = "/oauth/consent" +# Allow dynamic client registration +allow_dynamic_registration = false + +[edge_runtime] +enabled = true +# Supported request policies: `oneshot`, `per_worker`. +# `per_worker` (default) — enables hot reload during local development. +# `oneshot` — fallback mode if hot reload causes issues (e.g. in large repos or with symlinks). +policy = "per_worker" +# Port to attach the Chrome inspector for debugging edge functions. +inspector_port = 8083 +# The Deno major version to use. +deno_version = 2 + +# [edge_runtime.secrets] +# secret_key = "env(SECRET_VALUE)" + +[analytics] +enabled = true +port = 54327 +# Configure one of the supported backends: `postgres`, `bigquery`. +backend = "postgres" + +# Experimental features may be deprecated any time +[experimental] +# Configures Postgres storage engine to use OrioleDB (S3) +orioledb_version = "" +# Configures S3 bucket URL, eg. .s3-.amazonaws.com +s3_host = "env(S3_HOST)" +# Configures S3 bucket region, eg. us-east-1 +s3_region = "env(S3_REGION)" +# Configures AWS_ACCESS_KEY_ID for S3 bucket +s3_access_key = "env(S3_ACCESS_KEY)" +# Configures AWS_SECRET_ACCESS_KEY for S3 bucket +s3_secret_key = "env(S3_SECRET_KEY)" diff --git a/apps/web/supabase/migrations/20251017234915_remote_schema.sql b/apps/web/supabase/migrations/20251017234915_remote_schema.sql new file mode 100644 index 0000000..fa9f5fe --- /dev/null +++ b/apps/web/supabase/migrations/20251017234915_remote_schema.sql @@ -0,0 +1,165 @@ + + +-- Create profiles table +CREATE TABLE IF NOT EXISTS profiles ( + id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE, + user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE, + name TEXT, + email TEXT, + credits INTEGER DEFAULT 10, + ai_trained BOOLEAN DEFAULT FALSE, + referral_code VARCHAR(10) UNIQUE, + referred_by VARCHAR(10), + total_referrals INTEGER DEFAULT 0, + referral_credits INTEGER DEFAULT 0, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Create user_style table +CREATE TABLE IF NOT EXISTS user_style ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE, + tone TEXT, + vocabulary_level TEXT, + pacing TEXT, + themes TEXT, + humor_style TEXT, + structure TEXT, + video_urls TEXT[], + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Create scripts table +CREATE TABLE IF NOT EXISTS scripts ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE, + title TEXT NOT NULL, + content TEXT NOT NULL, + prompt TEXT, + context TEXT, + tone TEXT, + include_storytelling BOOLEAN DEFAULT FALSE, + script_references TEXT, + language TEXT DEFAULT 'english', + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Create referrals table +CREATE TABLE IF NOT EXISTS referrals ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + referrer_id UUID REFERENCES auth.users(id) ON DELETE CASCADE, + referred_user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE, + referral_code VARCHAR(10) NOT NULL, + status VARCHAR(20) DEFAULT 'pending', + credits_awarded INTEGER DEFAULT 0, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + completed_at TIMESTAMP WITH TIME ZONE, + UNIQUE(referrer_id, referred_user_id) +); + +-- Add foreign key constraint for referral_code +ALTER TABLE profiles + ADD CONSTRAINT fk_referred_by + FOREIGN KEY (referred_by) + REFERENCES profiles(referral_code); + +-- Create indexes for better query performance +CREATE INDEX IF NOT EXISTS idx_profiles_user_id ON profiles(user_id); +CREATE INDEX IF NOT EXISTS idx_profiles_referral_code ON profiles(referral_code); +CREATE INDEX IF NOT EXISTS idx_user_style_user_id ON user_style(user_id); +CREATE INDEX IF NOT EXISTS idx_scripts_user_id ON scripts(user_id); +CREATE INDEX IF NOT EXISTS idx_referrals_referrer_id ON referrals(referrer_id); +CREATE INDEX IF NOT EXISTS idx_referrals_referred_user_id ON referrals(referred_user_id); + +-- Enable Row Level Security (RLS) +ALTER TABLE profiles ENABLE ROW LEVEL SECURITY; +ALTER TABLE user_style ENABLE ROW LEVEL SECURITY; +ALTER TABLE scripts ENABLE ROW LEVEL SECURITY; +ALTER TABLE referrals ENABLE ROW LEVEL SECURITY; + +-- Create RLS policies for profiles +CREATE POLICY "Users can view own profile" ON profiles + FOR SELECT USING (auth.uid() = id); + +CREATE POLICY "Users can update own profile" ON profiles + FOR UPDATE USING (auth.uid() = id); + +CREATE POLICY "Users can insert own profile" ON profiles + FOR INSERT WITH CHECK (auth.uid() = id); + +-- Create RLS policies for user_style +CREATE POLICY "Users can view own style" ON user_style + FOR SELECT USING (auth.uid() = user_id); + +CREATE POLICY "Users can insert own style" ON user_style + FOR INSERT WITH CHECK (auth.uid() = user_id); + +CREATE POLICY "Users can update own style" ON user_style + FOR UPDATE USING (auth.uid() = user_id); + +CREATE POLICY "Users can delete own style" ON user_style + FOR DELETE USING (auth.uid() = user_id); + +-- Create RLS policies for scripts +CREATE POLICY "Users can view own scripts" ON scripts + FOR SELECT USING (auth.uid() = user_id); + +CREATE POLICY "Users can insert own scripts" ON scripts + FOR INSERT WITH CHECK (auth.uid() = user_id); + +CREATE POLICY "Users can update own scripts" ON scripts + FOR UPDATE USING (auth.uid() = user_id); + +CREATE POLICY "Users can delete own scripts" ON scripts + FOR DELETE USING (auth.uid() = user_id); + +-- Create RLS policies for referrals +CREATE POLICY "Users can view referrals they made" ON referrals + FOR SELECT USING (auth.uid() = referrer_id); + +CREATE POLICY "Users can view referrals they received" ON referrals + FOR SELECT USING (auth.uid() = referred_user_id); + +CREATE POLICY "Users can insert referrals" ON referrals + FOR INSERT WITH CHECK (auth.uid() = referrer_id); + +-- Create function to automatically create profile on user signup +CREATE OR REPLACE FUNCTION public.handle_new_user() +RETURNS TRIGGER AS $$ +BEGIN + INSERT INTO public.profiles (id, user_id, email) + VALUES (NEW.id, NEW.id, NEW.email); + RETURN NEW; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +-- Create trigger for new user signup +DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users; +CREATE TRIGGER on_auth_user_created + AFTER INSERT ON auth.users + FOR EACH ROW EXECUTE FUNCTION public.handle_new_user(); + +-- Create function to update updated_at timestamp +CREATE OR REPLACE FUNCTION public.handle_updated_at() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Create triggers for updated_at +CREATE TRIGGER set_updated_at_profiles + BEFORE UPDATE ON profiles + FOR EACH ROW EXECUTE FUNCTION handle_updated_at(); + +CREATE TRIGGER set_updated_at_user_style + BEFORE UPDATE ON user_style + FOR EACH ROW EXECUTE FUNCTION handle_updated_at(); + +CREATE TRIGGER set_updated_at_scripts + BEFORE UPDATE ON scripts + FOR EACH ROW EXECUTE FUNCTION handle_updated_at(); \ No newline at end of file diff --git a/apps/web/supabase/migrations/20251018002813_remote_schema.sql b/apps/web/supabase/migrations/20251018002813_remote_schema.sql new file mode 100644 index 0000000..019d497 --- /dev/null +++ b/apps/web/supabase/migrations/20251018002813_remote_schema.sql @@ -0,0 +1,163 @@ +create table "public"."subscriptions" ( + "id" uuid not null default gen_random_uuid(), + "created_at" timestamp with time zone not null default now(), + "user_id" uuid default auth.uid(), + "stripe_customer_id" text, + "stripe_subscription_id" text, + "subscription_type" text default ''::text, + "payment_details" json +); + + +alter table "public"."subscriptions" enable row level security; + +create table "public"."youtube_channels" ( + "id" uuid not null default uuid_generate_v4(), + "user_id" uuid not null, + "channel_id" text not null, + "channel_name" text, + "channel_description" text, + "custom_url" text, + "published_at" timestamp with time zone, + "country" text, + "thumbnail" text, + "default_language" text, + "view_count" bigint, + "subscriber_count" bigint, + "video_count" bigint, + "is_linked" boolean, + "text_color" text, + "background_color" text, + "topic_details" jsonb, + "updated_at" timestamp with time zone default now() +); + + +alter table "public"."youtube_channels" enable row level security; + +CREATE UNIQUE INDEX subscriptions_pkey ON public.subscriptions USING btree (id); + +CREATE UNIQUE INDEX unique_user_channel ON public.youtube_channels USING btree (user_id, channel_id); + +CREATE UNIQUE INDEX youtube_channels_pkey ON public.youtube_channels USING btree (id); + +alter table "public"."subscriptions" add constraint "subscriptions_pkey" PRIMARY KEY using index "subscriptions_pkey"; + +alter table "public"."youtube_channels" add constraint "youtube_channels_pkey" PRIMARY KEY using index "youtube_channels_pkey"; + +alter table "public"."youtube_channels" add constraint "unique_user_channel" UNIQUE using index "unique_user_channel"; + +alter table "public"."youtube_channels" add constraint "youtube_channels_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) not valid; + +alter table "public"."youtube_channels" validate constraint "youtube_channels_user_id_fkey"; + +grant delete on table "public"."subscriptions" to "anon"; + +grant insert on table "public"."subscriptions" to "anon"; + +grant references on table "public"."subscriptions" to "anon"; + +grant select on table "public"."subscriptions" to "anon"; + +grant trigger on table "public"."subscriptions" to "anon"; + +grant truncate on table "public"."subscriptions" to "anon"; + +grant update on table "public"."subscriptions" to "anon"; + +grant delete on table "public"."subscriptions" to "authenticated"; + +grant insert on table "public"."subscriptions" to "authenticated"; + +grant references on table "public"."subscriptions" to "authenticated"; + +grant select on table "public"."subscriptions" to "authenticated"; + +grant trigger on table "public"."subscriptions" to "authenticated"; + +grant truncate on table "public"."subscriptions" to "authenticated"; + +grant update on table "public"."subscriptions" to "authenticated"; + +grant delete on table "public"."subscriptions" to "service_role"; + +grant insert on table "public"."subscriptions" to "service_role"; + +grant references on table "public"."subscriptions" to "service_role"; + +grant select on table "public"."subscriptions" to "service_role"; + +grant trigger on table "public"."subscriptions" to "service_role"; + +grant truncate on table "public"."subscriptions" to "service_role"; + +grant update on table "public"."subscriptions" to "service_role"; + +grant delete on table "public"."youtube_channels" to "anon"; + +grant insert on table "public"."youtube_channels" to "anon"; + +grant references on table "public"."youtube_channels" to "anon"; + +grant select on table "public"."youtube_channels" to "anon"; + +grant trigger on table "public"."youtube_channels" to "anon"; + +grant truncate on table "public"."youtube_channels" to "anon"; + +grant update on table "public"."youtube_channels" to "anon"; + +grant delete on table "public"."youtube_channels" to "authenticated"; + +grant insert on table "public"."youtube_channels" to "authenticated"; + +grant references on table "public"."youtube_channels" to "authenticated"; + +grant select on table "public"."youtube_channels" to "authenticated"; + +grant trigger on table "public"."youtube_channels" to "authenticated"; + +grant truncate on table "public"."youtube_channels" to "authenticated"; + +grant update on table "public"."youtube_channels" to "authenticated"; + +grant delete on table "public"."youtube_channels" to "service_role"; + +grant insert on table "public"."youtube_channels" to "service_role"; + +grant references on table "public"."youtube_channels" to "service_role"; + +grant select on table "public"."youtube_channels" to "service_role"; + +grant trigger on table "public"."youtube_channels" to "service_role"; + +grant truncate on table "public"."youtube_channels" to "service_role"; + +grant update on table "public"."youtube_channels" to "service_role"; + +create policy "Users can insert their own channel data" +on "public"."youtube_channels" +as permissive +for insert +to public +with check ((auth.uid() = user_id)); + + +create policy "Users can update their own channel data" +on "public"."youtube_channels" +as permissive +for update +to public +using ((auth.uid() = user_id)); + + +create policy "Users can view their own channel data" +on "public"."youtube_channels" +as permissive +for select +to public +using ((auth.uid() = user_id)); + + + + diff --git a/apps/web/supabase/migrations/schema.sql b/apps/web/supabase/migrations/schema.sql new file mode 100644 index 0000000..124936e --- /dev/null +++ b/apps/web/supabase/migrations/schema.sql @@ -0,0 +1,43 @@ +-- Creating table for YouTube channel details +create table youtube_channels ( + id uuid primary key default uuid_generate_v4(), + user_id uuid references auth.users(id) not null, + channel_id text not null, + channel_name text, + channel_description text, + custom_url text, + published_at timestamp with time zone, + country text, + thumbnail text, + default_language text, + view_count bigint, + subscriber_count bigint, + video_count bigint, + is_linked boolean, + text_color text, + background_color text, + topic_details jsonb, + updated_at timestamp with time zone default now(), + constraint unique_user_channel unique (user_id, channel_id) +); + +-- Enabling Row-Level Security +alter table youtube_channels enable row level security; + +-- Creating RLS policy for selecting own data +create policy "Users can view their own channel data" +on youtube_channels +for select +using (auth.uid() = user_id); + +-- Creating RLS policy for inserting own data +create policy "Users can insert their own channel data" +on youtube_channels +for insert +with check (auth.uid() = user_id); + +-- Creating RLS policy for updating own data +create policy "Users can update their own channel data" +on youtube_channels +for update +using (auth.uid() = user_id); \ No newline at end of file diff --git a/apps/web/types/subscription.ts b/apps/web/types/subscription.ts new file mode 100644 index 0000000..46b0749 --- /dev/null +++ b/apps/web/types/subscription.ts @@ -0,0 +1 @@ +export type SubType = "Pro" | "Enterprise" \ No newline at end of file From 30bc0ea4a1ef69a52dd73da2753e6fbc40b37d28 Mon Sep 17 00:00:00 2001 From: semz-ui Date: Sat, 18 Oct 2025 22:10:32 +0100 Subject: [PATCH 04/10] stripe subscription integrtion --- .../api/stripe/cancel-subscription/route.ts | 26 +++++++ .../api/stripe/create-subscription/route.ts | 74 ++++++++++++++++++ apps/web/app/api/stripe/route.ts | 69 ----------------- .../api/stripe/update-subscription/route.ts | 34 +++++++++ apps/web/app/api/stripe/webhook/route.ts | 76 +++++++++++-------- .../dashboard/settings/BillingInfo.tsx | 27 +++++-- .../components/landingPage/PricingSection.tsx | 8 +- apps/web/helpers/capitalize.ts | 8 ++ apps/web/helpers/formatDate.ts | 8 ++ apps/web/hooks/useSettings.ts | 27 +++++++ apps/web/package.json | 1 + pnpm-lock.yaml | 13 +++- 12 files changed, 262 insertions(+), 109 deletions(-) create mode 100644 apps/web/app/api/stripe/cancel-subscription/route.ts create mode 100644 apps/web/app/api/stripe/create-subscription/route.ts create mode 100644 apps/web/app/api/stripe/update-subscription/route.ts create mode 100644 apps/web/helpers/capitalize.ts create mode 100644 apps/web/helpers/formatDate.ts diff --git a/apps/web/app/api/stripe/cancel-subscription/route.ts b/apps/web/app/api/stripe/cancel-subscription/route.ts new file mode 100644 index 0000000..671fba2 --- /dev/null +++ b/apps/web/app/api/stripe/cancel-subscription/route.ts @@ -0,0 +1,26 @@ +import { createClient } from "@/lib/supabase/server"; +import { stripe } from "@/lib/stripe"; +import { NextResponse } from "next/server"; + + +export async function POST(req: Request) { + const supabase = await createClient(); + const { data: { user }, error: userError } = await supabase.auth.getUser(); + + try { + const { data: existing } = await supabase + .from("subscriptions") + .select("stripe_subscription_id") + .eq("user_id", user?.id) + .single() + + const subscriptionId = existing?.stripe_subscription_id + const updatedSubscription = await stripe.subscriptions.update(subscriptionId, { + cancel_at_period_end: true, + }); + + return NextResponse.json(updatedSubscription); + } catch (error:any) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} \ No newline at end of file diff --git a/apps/web/app/api/stripe/create-subscription/route.ts b/apps/web/app/api/stripe/create-subscription/route.ts new file mode 100644 index 0000000..f3bb26d --- /dev/null +++ b/apps/web/app/api/stripe/create-subscription/route.ts @@ -0,0 +1,74 @@ +import { NextResponse } from "next/server"; +import { stripe } from "@/lib/stripe"; +import { createClient } from "@/lib/supabase/server"; + + +export async function POST(req: Request) { + const supabase = await createClient(); + try { + const { data: { user }, error: userError } = await supabase.auth.getUser(); + if (userError || !user) { + return NextResponse.json({ message: 'Unauthorized' }, { status: 401 }); + } + + const { price_id, sub_type } = await req.json(); + + const { data: existing } = await supabase + .from("subscriptions") + .select("stripe_customer_id, subscription_type") + .eq("user_id", user.id) + .single() + const { data: profile } = await supabase + .from("profiles") + .select("full_name") + .eq("user_id", user.id) + .single() + + + let customerId = existing?.stripe_customer_id + + + if(existing?.subscription_type.toLowerCase() === sub_type.toLowerCase()){ + return NextResponse.json({ error: `${sub_type} is. your. active. plan, Please select a different active. plan if you want to upgrade` }, { status: 400 }); + } + + if (!customerId) { + const customer = await stripe.customers.create({ + name: profile?.full_name, + metadata: { user_id: user.id }, + email: user.email + }) + customerId = customer.id + } + + const session = await stripe.checkout.sessions.create({ + mode: "subscription", + payment_method_types: ["card"], + customer: customerId, + line_items: [ + { + price: price_id, // e.g. "price_12345" + quantity: 1, + }, + ], + success_url: `${process.env.NEXT_PUBLIC_BASE_URL}/dashboard?session_id={CHECKOUT_SESSION_ID}`, + cancel_url: `${process.env.NEXT_PUBLIC_BASE_URL}/cancel`, + metadata: { + user_id: user.id, + sub_type: sub_type + } + }); + + + + + return NextResponse.json({ + url: session.url + // url: "Done" + + }); + } catch (error: any) { + console.error("Stripe Error:", error); + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} diff --git a/apps/web/app/api/stripe/route.ts b/apps/web/app/api/stripe/route.ts index 9164e4d..e69de29 100644 --- a/apps/web/app/api/stripe/route.ts +++ b/apps/web/app/api/stripe/route.ts @@ -1,69 +0,0 @@ -import { NextResponse } from "next/server"; -import { stripe } from "@/lib/stripe"; -import { createClient } from "@/lib/supabase/server"; - - -export async function POST(req: Request) { - const supabase = await createClient(); - try { - const { data: { user }, error: userError } = await supabase.auth.getUser(); - if (userError || !user) { - return NextResponse.json({ message: 'Unauthorized' }, { status: 401 }); - } - - const { price_id, sub_type } = await req.json(); - - const { data: existing } = await supabase - .from("subscriptions") - .select("stripe_customer_id") - .eq("user_id", user.id) - .single() - // const { data: profile } = await supabase - // .from("profiles") - // .select("full_name") - // .eq("user_id", user.id) - // .single() - - - // let customerId = existing?.stripe_customer_id - // console.log(existing, "profile") - - // if (!customerId) { - // const customer = await stripe.customers.create({ - // name: profile?.full_name, - // metadata: { user_id: user.id, sub_type: sub_type }, - // email: user.email - // }) - // customerId = customer.id - // } - - // const session = await stripe.checkout.sessions.create({ - // mode: "subscription", - // payment_method_types: ["card"], - // customer: customerId, - // line_items: [ - // { - // price: price_id, // e.g. "price_12345" - // quantity: 1, - // }, - // ], - // success_url: `${process.env.NEXT_PUBLIC_BASE_URL}/dashboard?session_id={CHECKOUT_SESSION_ID}`, - // cancel_url: `${process.env.NEXT_PUBLIC_BASE_URL}/cancel`, - // metadata: { - // user_id: user.id - // } - // }); - - - - - return NextResponse.json({ - // url: session.url - url: "Done" - - }); - } catch (error: any) { - console.error("Stripe Error:", error); - return NextResponse.json({ error: error.message }, { status: 500 }); - } -} diff --git a/apps/web/app/api/stripe/update-subscription/route.ts b/apps/web/app/api/stripe/update-subscription/route.ts new file mode 100644 index 0000000..066486e --- /dev/null +++ b/apps/web/app/api/stripe/update-subscription/route.ts @@ -0,0 +1,34 @@ +import { createClient } from "@/lib/supabase/server"; +import { stripe } from "@/lib/stripe"; +import { NextResponse } from "next/server"; + + +export async function POST(req: Request) { + const supabase = await createClient(); + const { data: { user }, error: userError } = await supabase.auth.getUser(); + + const { price_id, sub_type } = await req.json(); + + const { data: existing } = await supabase + .from("subscriptions") + .select("stripe_subscription_id") + .eq("user_id", user?.id) + .single() + + const subscriptionId = existing?.stripe_subscription_id + + const subscription: any = await stripe.subscriptions.retrieve(existing?.stripe_subscription_id); + const subscriptionItemId = subscription.items.data[0].id; + + const updatedSubscription = await stripe.subscriptions.update(subscriptionId, { + items: [ + { + id: subscriptionItemId, + price: price_id, + }, + ], + proration_behavior: "none", + }); + + return NextResponse.json({ message: "Success!, New plan will start at the end of the month", updatedSubscription, status: 200 }); +} \ No newline at end of file diff --git a/apps/web/app/api/stripe/webhook/route.ts b/apps/web/app/api/stripe/webhook/route.ts index 10181d8..31cc499 100644 --- a/apps/web/app/api/stripe/webhook/route.ts +++ b/apps/web/app/api/stripe/webhook/route.ts @@ -26,13 +26,13 @@ export async function POST(req: Request) { try { switch (event.type) { - case "customer.subscription.created": { + case "checkout.session.completed": { const session = event.data.object as any; const subscriptionId = session.subscription; const customerId = session.customer; let userId = session.metadata?.user_id; let sub_type: SubType = session.metadata?.sub_type; - console.log("customerId: ", customerId) + console.log("customerId: ", session) if (!userId) { try { @@ -78,41 +78,58 @@ export async function POST(req: Request) { const { data: existing, error } = await supabaseAdmin .from("subscriptions") - .select("stripe_customer_id") + .select("stripe_customer_id, stripe_subscription_id") .eq("user_id", userId) .single() - let ExistingCustomerId = existing?.stripe_customer_id - - if(ExistingCustomerId !== customerId) { - const { error } = await supabaseAdmin.from("subscriptions").upsert({ - user_id: userId, - payment_details: session, - stripe_customer_id: customerId, - stripe_subscription_id: session.id, - subscription_type: "active", - }) + let ExistingCustomerId = existing?.stripe_customer_id + + const subscription_data: any = await stripe.subscriptions.retrieve(subscriptionId); + const subscriptionItemEndDate = subscription_data.items.data[0].current_period_end; + const expires = new Date(subscriptionItemEndDate * 1000) + console.log("subscriptionItemEndDate: ",subscriptionItemEndDate) + + if (ExistingCustomerId !== customerId) { + const { error } = await supabaseAdmin.from("subscriptions").upsert({ + user_id: userId, + // payment_details: session, + stripe_customer_id: customerId, + stripe_subscription_id: session.subscription, + subscription_type: sub_type, + subscription_end_date: expires.toUTCString() + }) + + if (error) { + console.log(error, "errorrr") + return NextResponse.json({ message: 'Something happened', error }, { status: 401 }); + } - if (error) { - console.log(error, "errorrr") - return NextResponse.json({ message: 'Something happened', error }, { status: 401 }); } - - } + if (ExistingCustomerId === customerId) { + const { error } = await supabaseAdmin.from("subscriptions").update({ + // payment_details: session, + stripe_subscription_id: session.subscription, + subscription_type: sub_type.toLocaleLowerCase(), + subscription_end_date: expires.toUTCString() + }).eq("user_id", userId); + + if (error) { + console.log(error, "errorrr") + return NextResponse.json({ message: 'Something happened', error }, { status: 401 }); + } + } } catch (err) { console.error("Error updating credits:", err); } - - // await db.user.update({ - // where: { id: userId }, - // data: { - // stripeSubscriptionId: subscriptionId, - // status: "active", - // }, - // }); break; } + case "customer.subscription.created": { + const sub = event.data.object as any; + + console.log(sub) + break; + } case "invoice.payment_succeeded": { const invoice = event.data.object as any; const customerId = invoice.customer; @@ -147,10 +164,9 @@ export async function POST(req: Request) { const customer = await stripe.customers.retrieve(customerId); const userId = (customer as any).metadata.userId; - // await db.user.update({ - // where: { id: userId }, - // data: { status: "canceled" }, - // }); + const { error } = await supabaseAdmin.from("subscriptions").update({ + subscription_type: "cancelled", + }).eq("user_id", userId); break; } diff --git a/apps/web/components/dashboard/settings/BillingInfo.tsx b/apps/web/components/dashboard/settings/BillingInfo.tsx index 92eb157..15576fc 100644 --- a/apps/web/components/dashboard/settings/BillingInfo.tsx +++ b/apps/web/components/dashboard/settings/BillingInfo.tsx @@ -5,8 +5,12 @@ import { Skeleton } from "@/components/ui/skeleton" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { useSettings } from "@/hooks/useSettings"; import { CreditCard, Loader2 } from "lucide-react" -import { useState, useEffect } from "react" +import { useState, useEffect, useMemo } from "react" import { useRouter } from "next/navigation" +import { createClient } from "@/lib/supabase/client"; +import { useSupabase } from "@/components/supabase-provider"; +import {capitalize} from "@/helpers/capitalize" +import { formatDate } from "@/helpers/formatDate"; // A type for the component's data state interface BillingData { @@ -15,14 +19,27 @@ interface BillingData { paymentMethod: string | null } +interface BillingDetails { + subscription_end_date: string; + subscription_type: string; +} + export function BillingInfo() { - const { updateBilling, loadingBilling } = useSettings() + const { updateBilling, loadingBilling, billingDetails, fetchSubscriptionDetails } = useSettings() const router = useRouter() + const { supabase, user } = useSupabase() const [isLoadingData, setIsLoadingData] = useState(true) const [billingData, setBillingData] = useState(null) + + useEffect(() => { + if(user) { + fetchSubscriptionDetails(user?.id) + } + }, [user]) + useEffect(() => { const fetchBillingData = async () => { @@ -69,10 +86,10 @@ export function BillingInfo() { ) : ( <> -

{billingData?.currentPlan} Plan

- {billingData?.nextBillingDate && ( +

{capitalize(billingDetails?.subscription_type) || "Free"} Plan

+ {billingDetails?.subscription_end_date && (

- Next billing date: {billingData.nextBillingDate} + Next billing date: {formatDate(billingDetails.subscription_end_date) || "Please subscribe to a plan"}

)} diff --git a/apps/web/components/landingPage/PricingSection.tsx b/apps/web/components/landingPage/PricingSection.tsx index a11215c..08d5ee9 100644 --- a/apps/web/components/landingPage/PricingSection.tsx +++ b/apps/web/components/landingPage/PricingSection.tsx @@ -22,17 +22,17 @@ export default function PricingSection() { if (loading) return setLoading(true) try { - const response = await fetch('/api/stripe', { + const response = await fetch('/api/stripe/create-subscription', { method: "POST", body: JSON.stringify({ price_id,sub_type }) }) + const data = await response.json() if (!response.ok) { - toast.error("Something happened, please try again") + toast.error(data.error || "Something went wrong") return } - const data = await response.json() - // if (data.url) window.location.href = data.url; + if (data.url) window.location.href = data.url; } catch (error: any) { toast.error("Failed to send issue report", { description: error?.message || "Something went wrong. Please try again.", diff --git a/apps/web/helpers/capitalize.ts b/apps/web/helpers/capitalize.ts new file mode 100644 index 0000000..e6fb52c --- /dev/null +++ b/apps/web/helpers/capitalize.ts @@ -0,0 +1,8 @@ +export const capitalize = (value: string | undefined | null) => { + if (value) { + return value.charAt(0).toUpperCase() + value.slice(1); + } else { + return ""; + } +}; + diff --git a/apps/web/helpers/formatDate.ts b/apps/web/helpers/formatDate.ts new file mode 100644 index 0000000..85f11d2 --- /dev/null +++ b/apps/web/helpers/formatDate.ts @@ -0,0 +1,8 @@ +export function formatDate(isoString: string): string { + const date = new Date(isoString) + return date.toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric' + }) +} \ No newline at end of file diff --git a/apps/web/hooks/useSettings.ts b/apps/web/hooks/useSettings.ts index d56c732..13d39c0 100644 --- a/apps/web/hooks/useSettings.ts +++ b/apps/web/hooks/useSettings.ts @@ -5,6 +5,11 @@ import { useState } from "react"; import { useSupabase } from "@/components/supabase-provider"; import { toast } from "sonner"; +interface BillingDetails { + subscription_end_date: string; + subscription_type: string; +} + export function useSettings () { const { supabase, user } = useSupabase(); @@ -14,6 +19,7 @@ export function useSettings const [isChangingPassword, setIsChangingPassword] = useState(false); const [loadingNotifications, setLoadingNotifications] = useState(false); const [loadingBilling, setLoadingBilling] = useState(false); + const [billingDetails, setBillingDetails] = useState(null) // --- Profile update --- const updateProfile = async ({ @@ -141,6 +147,25 @@ const [isChangingPassword, setIsChangingPassword] = useState(false); } }; + const fetchSubscriptionDetails = async (userId:string): Promise => { + setLoadingBilling(true) + try { + const { data: existing } = await supabase + .from("subscriptions") + .select("subscription_end_date, subscription_type") + .eq("user_id", userId) + .single() + + if(existing) { + setBillingDetails(existing) + } + } catch (error) { + + } finally { + setLoadingBilling(false) + } + } + // --- Password reset --- const changePassword = async () => { if (!user?.email) return; @@ -174,5 +199,7 @@ const [isChangingPassword, setIsChangingPassword] = useState(false); // Billing updateBilling, loadingBilling, + fetchSubscriptionDetails, + billingDetails, }; } diff --git a/apps/web/package.json b/apps/web/package.json index 1bf8074..1f782e7 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -99,6 +99,7 @@ "@types/node": "^22", "@types/react": "^19", "@types/react-dom": "^19", + "@types/stripe": "^8.0.417", "postcss": "^8.5", "tailwindcss": "^3.4.17", "typescript": "^5" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d572825..91a7734 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -385,6 +385,9 @@ importers: '@types/react-dom': specifier: ^19 version: 19.1.7(@types/react@19.1.9) + '@types/stripe': + specifier: ^8.0.417 + version: 8.0.417(@types/node@22.17.0) postcss: specifier: ^8.5 version: 8.5.6 @@ -4504,6 +4507,15 @@ packages: resolution: {integrity: sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==} dev: false + /@types/stripe@8.0.417(@types/node@22.17.0): + resolution: {integrity: sha512-PTuqskh9YKNENnOHGVJBm4sM0zE8B1jZw1JIskuGAPkMB+OH236QeN8scclhYGPA4nG6zTtPXgwpXdp+HPDTVw==} + deprecated: This is a stub types definition. stripe provides its own type definitions, so you do not need this installed. + dependencies: + stripe: 19.1.0(@types/node@22.17.0) + transitivePeerDependencies: + - '@types/node' + dev: true + /@types/superagent@8.1.9: resolution: {integrity: sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==} dependencies: @@ -9874,7 +9886,6 @@ packages: dependencies: '@types/node': 22.17.0 qs: 6.14.0 - dev: false /strtok3@10.3.4: resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==} From 296908c857dad079c057bca7525781d238dffdc0 Mon Sep 17 00:00:00 2001 From: semz-ui Date: Sat, 18 Oct 2025 22:14:05 +0100 Subject: [PATCH 05/10] updated env key --- apps/web/.env.example | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/web/.env.example b/apps/web/.env.example index d466b3d..86265ee 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -16,4 +16,11 @@ NEXT_PUBLIC_BASE_URL=your_base_url # Google OAuth Credentials GOOGLE_CLIENT_SECRET=your_google_client_secret -GOOGLE_CLIENT_ID=your_google_client_id \ No newline at end of file +GOOGLE_CLIENT_ID=your_google_client_id + +# Stripe Api Keys +NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_**** +STRIPE_SECRET_KEY=sk_test_**** +STRIPE_WEBHOOK_SECRET=whsec_***** +NEXT_PUBLIC_PRO_PRICE = price_***** +NEXT_PUBLIC__ENTERPRICE_PLAN = price_***** From 3a12581c298b7bf3a49e52cb7175f1e7921378ba Mon Sep 17 00:00:00 2001 From: semz-ui Date: Sat, 18 Oct 2025 22:30:40 +0100 Subject: [PATCH 06/10] schema update --- .../20251017234915_remote_schema.sql | 165 ----- .../20251018002813_remote_schema.sql | 163 ----- .../20251018212434_remote_schema.sql | 610 ++++++++++++++++++ apps/web/supabase/migrations/schema.sql | 43 -- 4 files changed, 610 insertions(+), 371 deletions(-) delete mode 100644 apps/web/supabase/migrations/20251017234915_remote_schema.sql delete mode 100644 apps/web/supabase/migrations/20251018002813_remote_schema.sql create mode 100644 apps/web/supabase/migrations/20251018212434_remote_schema.sql delete mode 100644 apps/web/supabase/migrations/schema.sql diff --git a/apps/web/supabase/migrations/20251017234915_remote_schema.sql b/apps/web/supabase/migrations/20251017234915_remote_schema.sql deleted file mode 100644 index fa9f5fe..0000000 --- a/apps/web/supabase/migrations/20251017234915_remote_schema.sql +++ /dev/null @@ -1,165 +0,0 @@ - - --- Create profiles table -CREATE TABLE IF NOT EXISTS profiles ( - id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE, - user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE, - name TEXT, - email TEXT, - credits INTEGER DEFAULT 10, - ai_trained BOOLEAN DEFAULT FALSE, - referral_code VARCHAR(10) UNIQUE, - referred_by VARCHAR(10), - total_referrals INTEGER DEFAULT 0, - referral_credits INTEGER DEFAULT 0, - created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), - updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() -); - --- Create user_style table -CREATE TABLE IF NOT EXISTS user_style ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE, - tone TEXT, - vocabulary_level TEXT, - pacing TEXT, - themes TEXT, - humor_style TEXT, - structure TEXT, - video_urls TEXT[], - created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), - updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() -); - --- Create scripts table -CREATE TABLE IF NOT EXISTS scripts ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE, - title TEXT NOT NULL, - content TEXT NOT NULL, - prompt TEXT, - context TEXT, - tone TEXT, - include_storytelling BOOLEAN DEFAULT FALSE, - script_references TEXT, - language TEXT DEFAULT 'english', - created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), - updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() -); - --- Create referrals table -CREATE TABLE IF NOT EXISTS referrals ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - referrer_id UUID REFERENCES auth.users(id) ON DELETE CASCADE, - referred_user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE, - referral_code VARCHAR(10) NOT NULL, - status VARCHAR(20) DEFAULT 'pending', - credits_awarded INTEGER DEFAULT 0, - created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), - completed_at TIMESTAMP WITH TIME ZONE, - UNIQUE(referrer_id, referred_user_id) -); - --- Add foreign key constraint for referral_code -ALTER TABLE profiles - ADD CONSTRAINT fk_referred_by - FOREIGN KEY (referred_by) - REFERENCES profiles(referral_code); - --- Create indexes for better query performance -CREATE INDEX IF NOT EXISTS idx_profiles_user_id ON profiles(user_id); -CREATE INDEX IF NOT EXISTS idx_profiles_referral_code ON profiles(referral_code); -CREATE INDEX IF NOT EXISTS idx_user_style_user_id ON user_style(user_id); -CREATE INDEX IF NOT EXISTS idx_scripts_user_id ON scripts(user_id); -CREATE INDEX IF NOT EXISTS idx_referrals_referrer_id ON referrals(referrer_id); -CREATE INDEX IF NOT EXISTS idx_referrals_referred_user_id ON referrals(referred_user_id); - --- Enable Row Level Security (RLS) -ALTER TABLE profiles ENABLE ROW LEVEL SECURITY; -ALTER TABLE user_style ENABLE ROW LEVEL SECURITY; -ALTER TABLE scripts ENABLE ROW LEVEL SECURITY; -ALTER TABLE referrals ENABLE ROW LEVEL SECURITY; - --- Create RLS policies for profiles -CREATE POLICY "Users can view own profile" ON profiles - FOR SELECT USING (auth.uid() = id); - -CREATE POLICY "Users can update own profile" ON profiles - FOR UPDATE USING (auth.uid() = id); - -CREATE POLICY "Users can insert own profile" ON profiles - FOR INSERT WITH CHECK (auth.uid() = id); - --- Create RLS policies for user_style -CREATE POLICY "Users can view own style" ON user_style - FOR SELECT USING (auth.uid() = user_id); - -CREATE POLICY "Users can insert own style" ON user_style - FOR INSERT WITH CHECK (auth.uid() = user_id); - -CREATE POLICY "Users can update own style" ON user_style - FOR UPDATE USING (auth.uid() = user_id); - -CREATE POLICY "Users can delete own style" ON user_style - FOR DELETE USING (auth.uid() = user_id); - --- Create RLS policies for scripts -CREATE POLICY "Users can view own scripts" ON scripts - FOR SELECT USING (auth.uid() = user_id); - -CREATE POLICY "Users can insert own scripts" ON scripts - FOR INSERT WITH CHECK (auth.uid() = user_id); - -CREATE POLICY "Users can update own scripts" ON scripts - FOR UPDATE USING (auth.uid() = user_id); - -CREATE POLICY "Users can delete own scripts" ON scripts - FOR DELETE USING (auth.uid() = user_id); - --- Create RLS policies for referrals -CREATE POLICY "Users can view referrals they made" ON referrals - FOR SELECT USING (auth.uid() = referrer_id); - -CREATE POLICY "Users can view referrals they received" ON referrals - FOR SELECT USING (auth.uid() = referred_user_id); - -CREATE POLICY "Users can insert referrals" ON referrals - FOR INSERT WITH CHECK (auth.uid() = referrer_id); - --- Create function to automatically create profile on user signup -CREATE OR REPLACE FUNCTION public.handle_new_user() -RETURNS TRIGGER AS $$ -BEGIN - INSERT INTO public.profiles (id, user_id, email) - VALUES (NEW.id, NEW.id, NEW.email); - RETURN NEW; -END; -$$ LANGUAGE plpgsql SECURITY DEFINER; - --- Create trigger for new user signup -DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users; -CREATE TRIGGER on_auth_user_created - AFTER INSERT ON auth.users - FOR EACH ROW EXECUTE FUNCTION public.handle_new_user(); - --- Create function to update updated_at timestamp -CREATE OR REPLACE FUNCTION public.handle_updated_at() -RETURNS TRIGGER AS $$ -BEGIN - NEW.updated_at = NOW(); - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - --- Create triggers for updated_at -CREATE TRIGGER set_updated_at_profiles - BEFORE UPDATE ON profiles - FOR EACH ROW EXECUTE FUNCTION handle_updated_at(); - -CREATE TRIGGER set_updated_at_user_style - BEFORE UPDATE ON user_style - FOR EACH ROW EXECUTE FUNCTION handle_updated_at(); - -CREATE TRIGGER set_updated_at_scripts - BEFORE UPDATE ON scripts - FOR EACH ROW EXECUTE FUNCTION handle_updated_at(); \ No newline at end of file diff --git a/apps/web/supabase/migrations/20251018002813_remote_schema.sql b/apps/web/supabase/migrations/20251018002813_remote_schema.sql deleted file mode 100644 index 019d497..0000000 --- a/apps/web/supabase/migrations/20251018002813_remote_schema.sql +++ /dev/null @@ -1,163 +0,0 @@ -create table "public"."subscriptions" ( - "id" uuid not null default gen_random_uuid(), - "created_at" timestamp with time zone not null default now(), - "user_id" uuid default auth.uid(), - "stripe_customer_id" text, - "stripe_subscription_id" text, - "subscription_type" text default ''::text, - "payment_details" json -); - - -alter table "public"."subscriptions" enable row level security; - -create table "public"."youtube_channels" ( - "id" uuid not null default uuid_generate_v4(), - "user_id" uuid not null, - "channel_id" text not null, - "channel_name" text, - "channel_description" text, - "custom_url" text, - "published_at" timestamp with time zone, - "country" text, - "thumbnail" text, - "default_language" text, - "view_count" bigint, - "subscriber_count" bigint, - "video_count" bigint, - "is_linked" boolean, - "text_color" text, - "background_color" text, - "topic_details" jsonb, - "updated_at" timestamp with time zone default now() -); - - -alter table "public"."youtube_channels" enable row level security; - -CREATE UNIQUE INDEX subscriptions_pkey ON public.subscriptions USING btree (id); - -CREATE UNIQUE INDEX unique_user_channel ON public.youtube_channels USING btree (user_id, channel_id); - -CREATE UNIQUE INDEX youtube_channels_pkey ON public.youtube_channels USING btree (id); - -alter table "public"."subscriptions" add constraint "subscriptions_pkey" PRIMARY KEY using index "subscriptions_pkey"; - -alter table "public"."youtube_channels" add constraint "youtube_channels_pkey" PRIMARY KEY using index "youtube_channels_pkey"; - -alter table "public"."youtube_channels" add constraint "unique_user_channel" UNIQUE using index "unique_user_channel"; - -alter table "public"."youtube_channels" add constraint "youtube_channels_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) not valid; - -alter table "public"."youtube_channels" validate constraint "youtube_channels_user_id_fkey"; - -grant delete on table "public"."subscriptions" to "anon"; - -grant insert on table "public"."subscriptions" to "anon"; - -grant references on table "public"."subscriptions" to "anon"; - -grant select on table "public"."subscriptions" to "anon"; - -grant trigger on table "public"."subscriptions" to "anon"; - -grant truncate on table "public"."subscriptions" to "anon"; - -grant update on table "public"."subscriptions" to "anon"; - -grant delete on table "public"."subscriptions" to "authenticated"; - -grant insert on table "public"."subscriptions" to "authenticated"; - -grant references on table "public"."subscriptions" to "authenticated"; - -grant select on table "public"."subscriptions" to "authenticated"; - -grant trigger on table "public"."subscriptions" to "authenticated"; - -grant truncate on table "public"."subscriptions" to "authenticated"; - -grant update on table "public"."subscriptions" to "authenticated"; - -grant delete on table "public"."subscriptions" to "service_role"; - -grant insert on table "public"."subscriptions" to "service_role"; - -grant references on table "public"."subscriptions" to "service_role"; - -grant select on table "public"."subscriptions" to "service_role"; - -grant trigger on table "public"."subscriptions" to "service_role"; - -grant truncate on table "public"."subscriptions" to "service_role"; - -grant update on table "public"."subscriptions" to "service_role"; - -grant delete on table "public"."youtube_channels" to "anon"; - -grant insert on table "public"."youtube_channels" to "anon"; - -grant references on table "public"."youtube_channels" to "anon"; - -grant select on table "public"."youtube_channels" to "anon"; - -grant trigger on table "public"."youtube_channels" to "anon"; - -grant truncate on table "public"."youtube_channels" to "anon"; - -grant update on table "public"."youtube_channels" to "anon"; - -grant delete on table "public"."youtube_channels" to "authenticated"; - -grant insert on table "public"."youtube_channels" to "authenticated"; - -grant references on table "public"."youtube_channels" to "authenticated"; - -grant select on table "public"."youtube_channels" to "authenticated"; - -grant trigger on table "public"."youtube_channels" to "authenticated"; - -grant truncate on table "public"."youtube_channels" to "authenticated"; - -grant update on table "public"."youtube_channels" to "authenticated"; - -grant delete on table "public"."youtube_channels" to "service_role"; - -grant insert on table "public"."youtube_channels" to "service_role"; - -grant references on table "public"."youtube_channels" to "service_role"; - -grant select on table "public"."youtube_channels" to "service_role"; - -grant trigger on table "public"."youtube_channels" to "service_role"; - -grant truncate on table "public"."youtube_channels" to "service_role"; - -grant update on table "public"."youtube_channels" to "service_role"; - -create policy "Users can insert their own channel data" -on "public"."youtube_channels" -as permissive -for insert -to public -with check ((auth.uid() = user_id)); - - -create policy "Users can update their own channel data" -on "public"."youtube_channels" -as permissive -for update -to public -using ((auth.uid() = user_id)); - - -create policy "Users can view their own channel data" -on "public"."youtube_channels" -as permissive -for select -to public -using ((auth.uid() = user_id)); - - - - diff --git a/apps/web/supabase/migrations/20251018212434_remote_schema.sql b/apps/web/supabase/migrations/20251018212434_remote_schema.sql new file mode 100644 index 0000000..e0dd6c6 --- /dev/null +++ b/apps/web/supabase/migrations/20251018212434_remote_schema.sql @@ -0,0 +1,610 @@ +create table "public"."profiles" ( + "id" uuid not null, + "user_id" uuid, + "name" text, + "email" text, + "credits" integer default 10, + "ai_trained" boolean default false, + "referral_code" character varying(10), + "referred_by" character varying(10), + "total_referrals" integer default 0, + "referral_credits" integer default 0, + "created_at" timestamp with time zone default now(), + "updated_at" timestamp with time zone default now(), + "avatar_url" text, + "full_name" text, + "youtube_connected" boolean, + "language" text +); + + +alter table "public"."profiles" enable row level security; + +create table "public"."referrals" ( + "id" uuid not null default uuid_generate_v4(), + "referrer_id" uuid, + "referred_user_id" uuid, + "referral_code" character varying(10) not null, + "status" character varying(20) default 'pending'::character varying, + "credits_awarded" integer default 0, + "created_at" timestamp with time zone default now(), + "completed_at" timestamp with time zone +); + + +alter table "public"."referrals" enable row level security; + +create table "public"."scripts" ( + "id" uuid not null default uuid_generate_v4(), + "user_id" uuid, + "title" text not null, + "content" text not null, + "prompt" text, + "context" text, + "tone" text, + "include_storytelling" boolean default false, + "script_references" text, + "language" text default 'english'::text, + "created_at" timestamp with time zone default now(), + "updated_at" timestamp with time zone default now() +); + + +alter table "public"."scripts" enable row level security; + +create table "public"."subscriptions" ( + "id" uuid not null default gen_random_uuid(), + "created_at" timestamp with time zone not null default now(), + "user_id" uuid default auth.uid(), + "stripe_customer_id" text, + "stripe_subscription_id" text, + "subscription_type" text default ''::text, + "subscription_end_date" timestamp with time zone +); + + +create table "public"."user_style" ( + "id" uuid not null default uuid_generate_v4(), + "user_id" uuid, + "tone" text, + "vocabulary_level" text, + "pacing" text, + "themes" text, + "humor_style" text, + "structure" text, + "video_urls" text[], + "created_at" timestamp with time zone default now(), + "updated_at" timestamp with time zone default now() +); + + +alter table "public"."user_style" enable row level security; + +create table "public"."youtube_channels" ( + "id" uuid not null default uuid_generate_v4(), + "user_id" uuid not null, + "channel_id" text not null, + "channel_name" text, + "channel_description" text, + "custom_url" text, + "published_at" timestamp with time zone, + "country" text, + "thumbnail" text, + "default_language" text, + "view_count" bigint, + "subscriber_count" bigint, + "video_count" bigint, + "is_linked" boolean, + "text_color" text, + "background_color" text, + "topic_details" jsonb, + "updated_at" timestamp with time zone default now() +); + + +alter table "public"."youtube_channels" enable row level security; + +CREATE INDEX idx_profiles_referral_code ON public.profiles USING btree (referral_code); + +CREATE INDEX idx_profiles_user_id ON public.profiles USING btree (user_id); + +CREATE INDEX idx_referrals_referred_user_id ON public.referrals USING btree (referred_user_id); + +CREATE INDEX idx_referrals_referrer_id ON public.referrals USING btree (referrer_id); + +CREATE INDEX idx_scripts_user_id ON public.scripts USING btree (user_id); + +CREATE INDEX idx_user_style_user_id ON public.user_style USING btree (user_id); + +CREATE UNIQUE INDEX profiles_pkey ON public.profiles USING btree (id); + +CREATE UNIQUE INDEX profiles_referral_code_key ON public.profiles USING btree (referral_code); + +CREATE UNIQUE INDEX referrals_pkey ON public.referrals USING btree (id); + +CREATE UNIQUE INDEX referrals_referrer_id_referred_user_id_key ON public.referrals USING btree (referrer_id, referred_user_id); + +CREATE UNIQUE INDEX scripts_pkey ON public.scripts USING btree (id); + +CREATE UNIQUE INDEX subscriptions_pkey ON public.subscriptions USING btree (id); + +CREATE UNIQUE INDEX unique_user_channel ON public.youtube_channels USING btree (user_id, channel_id); + +CREATE UNIQUE INDEX user_style_pkey ON public.user_style USING btree (id); + +CREATE UNIQUE INDEX youtube_channels_pkey ON public.youtube_channels USING btree (id); + +alter table "public"."profiles" add constraint "profiles_pkey" PRIMARY KEY using index "profiles_pkey"; + +alter table "public"."referrals" add constraint "referrals_pkey" PRIMARY KEY using index "referrals_pkey"; + +alter table "public"."scripts" add constraint "scripts_pkey" PRIMARY KEY using index "scripts_pkey"; + +alter table "public"."subscriptions" add constraint "subscriptions_pkey" PRIMARY KEY using index "subscriptions_pkey"; + +alter table "public"."user_style" add constraint "user_style_pkey" PRIMARY KEY using index "user_style_pkey"; + +alter table "public"."youtube_channels" add constraint "youtube_channels_pkey" PRIMARY KEY using index "youtube_channels_pkey"; + +alter table "public"."profiles" add constraint "fk_referred_by" FOREIGN KEY (referred_by) REFERENCES profiles(referral_code) not valid; + +alter table "public"."profiles" validate constraint "fk_referred_by"; + +alter table "public"."profiles" add constraint "profiles_id_fkey" FOREIGN KEY (id) REFERENCES auth.users(id) ON DELETE CASCADE not valid; + +alter table "public"."profiles" validate constraint "profiles_id_fkey"; + +alter table "public"."profiles" add constraint "profiles_referral_code_key" UNIQUE using index "profiles_referral_code_key"; + +alter table "public"."profiles" add constraint "profiles_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE not valid; + +alter table "public"."profiles" validate constraint "profiles_user_id_fkey"; + +alter table "public"."referrals" add constraint "referrals_referred_user_id_fkey" FOREIGN KEY (referred_user_id) REFERENCES auth.users(id) ON DELETE CASCADE not valid; + +alter table "public"."referrals" validate constraint "referrals_referred_user_id_fkey"; + +alter table "public"."referrals" add constraint "referrals_referrer_id_fkey" FOREIGN KEY (referrer_id) REFERENCES auth.users(id) ON DELETE CASCADE not valid; + +alter table "public"."referrals" validate constraint "referrals_referrer_id_fkey"; + +alter table "public"."referrals" add constraint "referrals_referrer_id_referred_user_id_key" UNIQUE using index "referrals_referrer_id_referred_user_id_key"; + +alter table "public"."scripts" add constraint "scripts_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE not valid; + +alter table "public"."scripts" validate constraint "scripts_user_id_fkey"; + +alter table "public"."user_style" add constraint "user_style_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE not valid; + +alter table "public"."user_style" validate constraint "user_style_user_id_fkey"; + +alter table "public"."youtube_channels" add constraint "unique_user_channel" UNIQUE using index "unique_user_channel"; + +alter table "public"."youtube_channels" add constraint "youtube_channels_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) not valid; + +alter table "public"."youtube_channels" validate constraint "youtube_channels_user_id_fkey"; + +set check_function_bodies = off; + +CREATE OR REPLACE FUNCTION public.handle_new_user() + RETURNS trigger + LANGUAGE plpgsql + SECURITY DEFINER +AS $function$ +BEGIN + INSERT INTO public.profiles (id, user_id, email) + VALUES (NEW.id, NEW.id, NEW.email); + RETURN NEW; +END; +$function$ +; + +CREATE OR REPLACE FUNCTION public.handle_updated_at() + RETURNS trigger + LANGUAGE plpgsql +AS $function$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$function$ +; + +grant delete on table "public"."profiles" to "anon"; + +grant insert on table "public"."profiles" to "anon"; + +grant references on table "public"."profiles" to "anon"; + +grant select on table "public"."profiles" to "anon"; + +grant trigger on table "public"."profiles" to "anon"; + +grant truncate on table "public"."profiles" to "anon"; + +grant update on table "public"."profiles" to "anon"; + +grant delete on table "public"."profiles" to "authenticated"; + +grant insert on table "public"."profiles" to "authenticated"; + +grant references on table "public"."profiles" to "authenticated"; + +grant select on table "public"."profiles" to "authenticated"; + +grant trigger on table "public"."profiles" to "authenticated"; + +grant truncate on table "public"."profiles" to "authenticated"; + +grant update on table "public"."profiles" to "authenticated"; + +grant delete on table "public"."profiles" to "service_role"; + +grant insert on table "public"."profiles" to "service_role"; + +grant references on table "public"."profiles" to "service_role"; + +grant select on table "public"."profiles" to "service_role"; + +grant trigger on table "public"."profiles" to "service_role"; + +grant truncate on table "public"."profiles" to "service_role"; + +grant update on table "public"."profiles" to "service_role"; + +grant delete on table "public"."referrals" to "anon"; + +grant insert on table "public"."referrals" to "anon"; + +grant references on table "public"."referrals" to "anon"; + +grant select on table "public"."referrals" to "anon"; + +grant trigger on table "public"."referrals" to "anon"; + +grant truncate on table "public"."referrals" to "anon"; + +grant update on table "public"."referrals" to "anon"; + +grant delete on table "public"."referrals" to "authenticated"; + +grant insert on table "public"."referrals" to "authenticated"; + +grant references on table "public"."referrals" to "authenticated"; + +grant select on table "public"."referrals" to "authenticated"; + +grant trigger on table "public"."referrals" to "authenticated"; + +grant truncate on table "public"."referrals" to "authenticated"; + +grant update on table "public"."referrals" to "authenticated"; + +grant delete on table "public"."referrals" to "service_role"; + +grant insert on table "public"."referrals" to "service_role"; + +grant references on table "public"."referrals" to "service_role"; + +grant select on table "public"."referrals" to "service_role"; + +grant trigger on table "public"."referrals" to "service_role"; + +grant truncate on table "public"."referrals" to "service_role"; + +grant update on table "public"."referrals" to "service_role"; + +grant delete on table "public"."scripts" to "anon"; + +grant insert on table "public"."scripts" to "anon"; + +grant references on table "public"."scripts" to "anon"; + +grant select on table "public"."scripts" to "anon"; + +grant trigger on table "public"."scripts" to "anon"; + +grant truncate on table "public"."scripts" to "anon"; + +grant update on table "public"."scripts" to "anon"; + +grant delete on table "public"."scripts" to "authenticated"; + +grant insert on table "public"."scripts" to "authenticated"; + +grant references on table "public"."scripts" to "authenticated"; + +grant select on table "public"."scripts" to "authenticated"; + +grant trigger on table "public"."scripts" to "authenticated"; + +grant truncate on table "public"."scripts" to "authenticated"; + +grant update on table "public"."scripts" to "authenticated"; + +grant delete on table "public"."scripts" to "service_role"; + +grant insert on table "public"."scripts" to "service_role"; + +grant references on table "public"."scripts" to "service_role"; + +grant select on table "public"."scripts" to "service_role"; + +grant trigger on table "public"."scripts" to "service_role"; + +grant truncate on table "public"."scripts" to "service_role"; + +grant update on table "public"."scripts" to "service_role"; + +grant delete on table "public"."subscriptions" to "anon"; + +grant insert on table "public"."subscriptions" to "anon"; + +grant references on table "public"."subscriptions" to "anon"; + +grant select on table "public"."subscriptions" to "anon"; + +grant trigger on table "public"."subscriptions" to "anon"; + +grant truncate on table "public"."subscriptions" to "anon"; + +grant update on table "public"."subscriptions" to "anon"; + +grant delete on table "public"."subscriptions" to "authenticated"; + +grant insert on table "public"."subscriptions" to "authenticated"; + +grant references on table "public"."subscriptions" to "authenticated"; + +grant select on table "public"."subscriptions" to "authenticated"; + +grant trigger on table "public"."subscriptions" to "authenticated"; + +grant truncate on table "public"."subscriptions" to "authenticated"; + +grant update on table "public"."subscriptions" to "authenticated"; + +grant delete on table "public"."subscriptions" to "service_role"; + +grant insert on table "public"."subscriptions" to "service_role"; + +grant references on table "public"."subscriptions" to "service_role"; + +grant select on table "public"."subscriptions" to "service_role"; + +grant trigger on table "public"."subscriptions" to "service_role"; + +grant truncate on table "public"."subscriptions" to "service_role"; + +grant update on table "public"."subscriptions" to "service_role"; + +grant delete on table "public"."user_style" to "anon"; + +grant insert on table "public"."user_style" to "anon"; + +grant references on table "public"."user_style" to "anon"; + +grant select on table "public"."user_style" to "anon"; + +grant trigger on table "public"."user_style" to "anon"; + +grant truncate on table "public"."user_style" to "anon"; + +grant update on table "public"."user_style" to "anon"; + +grant delete on table "public"."user_style" to "authenticated"; + +grant insert on table "public"."user_style" to "authenticated"; + +grant references on table "public"."user_style" to "authenticated"; + +grant select on table "public"."user_style" to "authenticated"; + +grant trigger on table "public"."user_style" to "authenticated"; + +grant truncate on table "public"."user_style" to "authenticated"; + +grant update on table "public"."user_style" to "authenticated"; + +grant delete on table "public"."user_style" to "service_role"; + +grant insert on table "public"."user_style" to "service_role"; + +grant references on table "public"."user_style" to "service_role"; + +grant select on table "public"."user_style" to "service_role"; + +grant trigger on table "public"."user_style" to "service_role"; + +grant truncate on table "public"."user_style" to "service_role"; + +grant update on table "public"."user_style" to "service_role"; + +grant delete on table "public"."youtube_channels" to "anon"; + +grant insert on table "public"."youtube_channels" to "anon"; + +grant references on table "public"."youtube_channels" to "anon"; + +grant select on table "public"."youtube_channels" to "anon"; + +grant trigger on table "public"."youtube_channels" to "anon"; + +grant truncate on table "public"."youtube_channels" to "anon"; + +grant update on table "public"."youtube_channels" to "anon"; + +grant delete on table "public"."youtube_channels" to "authenticated"; + +grant insert on table "public"."youtube_channels" to "authenticated"; + +grant references on table "public"."youtube_channels" to "authenticated"; + +grant select on table "public"."youtube_channels" to "authenticated"; + +grant trigger on table "public"."youtube_channels" to "authenticated"; + +grant truncate on table "public"."youtube_channels" to "authenticated"; + +grant update on table "public"."youtube_channels" to "authenticated"; + +grant delete on table "public"."youtube_channels" to "service_role"; + +grant insert on table "public"."youtube_channels" to "service_role"; + +grant references on table "public"."youtube_channels" to "service_role"; + +grant select on table "public"."youtube_channels" to "service_role"; + +grant trigger on table "public"."youtube_channels" to "service_role"; + +grant truncate on table "public"."youtube_channels" to "service_role"; + +grant update on table "public"."youtube_channels" to "service_role"; + +create policy "Users can insert own profile" +on "public"."profiles" +as permissive +for insert +to public +with check ((auth.uid() = id)); + + +create policy "Users can update own profile" +on "public"."profiles" +as permissive +for update +to public +using ((auth.uid() = id)); + + +create policy "Users can view own profile" +on "public"."profiles" +as permissive +for select +to public +using ((auth.uid() = id)); + + +create policy "Users can insert referrals" +on "public"."referrals" +as permissive +for insert +to public +with check ((auth.uid() = referrer_id)); + + +create policy "Users can view referrals they made" +on "public"."referrals" +as permissive +for select +to public +using ((auth.uid() = referrer_id)); + + +create policy "Users can view referrals they received" +on "public"."referrals" +as permissive +for select +to public +using ((auth.uid() = referred_user_id)); + + +create policy "Users can delete own scripts" +on "public"."scripts" +as permissive +for delete +to public +using ((auth.uid() = user_id)); + + +create policy "Users can insert own scripts" +on "public"."scripts" +as permissive +for insert +to public +with check ((auth.uid() = user_id)); + + +create policy "Users can update own scripts" +on "public"."scripts" +as permissive +for update +to public +using ((auth.uid() = user_id)); + + +create policy "Users can view own scripts" +on "public"."scripts" +as permissive +for select +to public +using ((auth.uid() = user_id)); + + +create policy "Users can delete own style" +on "public"."user_style" +as permissive +for delete +to public +using ((auth.uid() = user_id)); + + +create policy "Users can insert own style" +on "public"."user_style" +as permissive +for insert +to public +with check ((auth.uid() = user_id)); + + +create policy "Users can update own style" +on "public"."user_style" +as permissive +for update +to public +using ((auth.uid() = user_id)); + + +create policy "Users can view own style" +on "public"."user_style" +as permissive +for select +to public +using ((auth.uid() = user_id)); + + +create policy "Users can insert their own channel data" +on "public"."youtube_channels" +as permissive +for insert +to public +with check ((auth.uid() = user_id)); + + +create policy "Users can update their own channel data" +on "public"."youtube_channels" +as permissive +for update +to public +using ((auth.uid() = user_id)); + + +create policy "Users can view their own channel data" +on "public"."youtube_channels" +as permissive +for select +to public +using ((auth.uid() = user_id)); + + +CREATE TRIGGER set_updated_at_profiles BEFORE UPDATE ON public.profiles FOR EACH ROW EXECUTE FUNCTION handle_updated_at(); + +CREATE TRIGGER set_updated_at_scripts BEFORE UPDATE ON public.scripts FOR EACH ROW EXECUTE FUNCTION handle_updated_at(); + +CREATE TRIGGER set_updated_at_user_style BEFORE UPDATE ON public.user_style FOR EACH ROW EXECUTE FUNCTION handle_updated_at(); + + +CREATE TRIGGER on_auth_user_created AFTER INSERT ON auth.users FOR EACH ROW EXECUTE FUNCTION handle_new_user(); + + diff --git a/apps/web/supabase/migrations/schema.sql b/apps/web/supabase/migrations/schema.sql deleted file mode 100644 index 124936e..0000000 --- a/apps/web/supabase/migrations/schema.sql +++ /dev/null @@ -1,43 +0,0 @@ --- Creating table for YouTube channel details -create table youtube_channels ( - id uuid primary key default uuid_generate_v4(), - user_id uuid references auth.users(id) not null, - channel_id text not null, - channel_name text, - channel_description text, - custom_url text, - published_at timestamp with time zone, - country text, - thumbnail text, - default_language text, - view_count bigint, - subscriber_count bigint, - video_count bigint, - is_linked boolean, - text_color text, - background_color text, - topic_details jsonb, - updated_at timestamp with time zone default now(), - constraint unique_user_channel unique (user_id, channel_id) -); - --- Enabling Row-Level Security -alter table youtube_channels enable row level security; - --- Creating RLS policy for selecting own data -create policy "Users can view their own channel data" -on youtube_channels -for select -using (auth.uid() = user_id); - --- Creating RLS policy for inserting own data -create policy "Users can insert their own channel data" -on youtube_channels -for insert -with check (auth.uid() = user_id); - --- Creating RLS policy for updating own data -create policy "Users can update their own channel data" -on youtube_channels -for update -using (auth.uid() = user_id); \ No newline at end of file From 3fb08130b5a1d7b733952aad9504e6ca2ba08e5f Mon Sep 17 00:00:00 2001 From: semz-ui Date: Mon, 20 Oct 2025 19:12:25 +0100 Subject: [PATCH 07/10] stripe payment integration update --- .../api/stripe/create-subscription/route.ts | 26 +- apps/web/app/api/stripe/webhook/route.ts | 39 +- .../dashboard/main/ReturningUserHub.tsx | 2 +- .../dashboard/settings/BillingInfo.tsx | 11 +- .../components/landingPage/PricingSection.tsx | 137 ++-- apps/web/components/supabase-provider.tsx | 33 + apps/web/hooks/useSettings.ts | 8 +- apps/web/supabase/.gitignore | 8 - apps/web/supabase/config.toml | 349 ---------- .../20251018212434_remote_schema.sql | 610 ------------------ apps/web/types/plans.ts | 15 + apps/web/types/subscription.ts | 14 +- 12 files changed, 182 insertions(+), 1070 deletions(-) delete mode 100644 apps/web/supabase/.gitignore delete mode 100644 apps/web/supabase/config.toml delete mode 100644 apps/web/supabase/migrations/20251018212434_remote_schema.sql create mode 100644 apps/web/types/plans.ts diff --git a/apps/web/app/api/stripe/create-subscription/route.ts b/apps/web/app/api/stripe/create-subscription/route.ts index f3bb26d..b354e65 100644 --- a/apps/web/app/api/stripe/create-subscription/route.ts +++ b/apps/web/app/api/stripe/create-subscription/route.ts @@ -5,17 +5,19 @@ import { createClient } from "@/lib/supabase/server"; export async function POST(req: Request) { const supabase = await createClient(); + const pro__plan = process.env.NEXT_PUBLIC_PRO_PRICE + const enterprice_plan = process.env.NEXT_PUBLIC__ENTERPRICE_PLAN try { const { data: { user }, error: userError } = await supabase.auth.getUser(); if (userError || !user) { return NextResponse.json({ message: 'Unauthorized' }, { status: 401 }); } - const { price_id, sub_type } = await req.json(); + const { sub_type, plan_id } = await req.json(); const { data: existing } = await supabase .from("subscriptions") - .select("stripe_customer_id, subscription_type") + .select("stripe_subscription_id, plan_id") .eq("user_id", user.id) .single() const { data: profile } = await supabase @@ -23,31 +25,25 @@ export async function POST(req: Request) { .select("full_name") .eq("user_id", user.id) .single() - - - let customerId = existing?.stripe_customer_id - - if(existing?.subscription_type.toLowerCase() === sub_type.toLowerCase()){ - return NextResponse.json({ error: `${sub_type} is. your. active. plan, Please select a different active. plan if you want to upgrade` }, { status: 400 }); + if(existing?.plan_id === plan_id){ + return NextResponse.json({ error: `${sub_type} is your active plan, Please select a different active. plan if you want to upgrade` }, { status: 400 }); } - if (!customerId) { const customer = await stripe.customers.create({ name: profile?.full_name, metadata: { user_id: user.id }, email: user.email }) - customerId = customer.id - } + const session = await stripe.checkout.sessions.create({ mode: "subscription", payment_method_types: ["card"], - customer: customerId, + customer: customer.id, line_items: [ { - price: price_id, // e.g. "price_12345" + price: sub_type === "Pro" ? pro__plan : enterprice_plan, quantity: 1, }, ], @@ -55,7 +51,8 @@ export async function POST(req: Request) { cancel_url: `${process.env.NEXT_PUBLIC_BASE_URL}/cancel`, metadata: { user_id: user.id, - sub_type: sub_type + sub_type: sub_type, + plan_id: plan_id } }); @@ -64,7 +61,6 @@ export async function POST(req: Request) { return NextResponse.json({ url: session.url - // url: "Done" }); } catch (error: any) { diff --git a/apps/web/app/api/stripe/webhook/route.ts b/apps/web/app/api/stripe/webhook/route.ts index 31cc499..68fb41a 100644 --- a/apps/web/app/api/stripe/webhook/route.ts +++ b/apps/web/app/api/stripe/webhook/route.ts @@ -32,7 +32,7 @@ export async function POST(req: Request) { const customerId = session.customer; let userId = session.metadata?.user_id; let sub_type: SubType = session.metadata?.sub_type; - console.log("customerId: ", session) + let plan_id: SubType = session.metadata?.plan_id; if (!userId) { try { @@ -62,7 +62,7 @@ export async function POST(req: Request) { console.error("Failed to fetch user for credit update:", fetchError); break; } - const amount_to_add = sub_type === "Pro" ? 300 : 1000; + const amount_to_add = sub_type === "Pro" ? 5000 : 100000; const currentCredits = (userRow as any)?.credits ?? 0; const newCredits = currentCredits + amount_to_add @@ -82,42 +82,29 @@ export async function POST(req: Request) { .eq("user_id", userId) .single() - let ExistingCustomerId = existing?.stripe_customer_id const subscription_data: any = await stripe.subscriptions.retrieve(subscriptionId); const subscriptionItemEndDate = subscription_data.items.data[0].current_period_end; + const subscriptionItemStartDate = subscription_data.items.data[0].current_period_start; + const starts = new Date(subscriptionItemStartDate * 1000) const expires = new Date(subscriptionItemEndDate * 1000) - console.log("subscriptionItemEndDate: ",subscriptionItemEndDate) - if (ExistingCustomerId !== customerId) { - const { error } = await supabaseAdmin.from("subscriptions").upsert({ + const { error:subError } = await supabaseAdmin.from("subscriptions").upsert({ user_id: userId, - // payment_details: session, - stripe_customer_id: customerId, + plan_id: plan_id, stripe_subscription_id: session.subscription, - subscription_type: sub_type, - subscription_end_date: expires.toUTCString() + current_period_end: expires.toUTCString(), + current_period_start: starts.toUTCString(), + status: "active", + updated_at: new Date().toISOString(), }) - if (error) { + if (subError) { console.log(error, "errorrr") return NextResponse.json({ message: 'Something happened', error }, { status: 401 }); } - } - if (ExistingCustomerId === customerId) { - const { error } = await supabaseAdmin.from("subscriptions").update({ - // payment_details: session, - stripe_subscription_id: session.subscription, - subscription_type: sub_type.toLocaleLowerCase(), - subscription_end_date: expires.toUTCString() - }).eq("user_id", userId); - - if (error) { - console.log(error, "errorrr") - return NextResponse.json({ message: 'Something happened', error }, { status: 401 }); - } - } + } catch (err) { console.error("Error updating credits:", err); } @@ -165,7 +152,7 @@ export async function POST(req: Request) { const userId = (customer as any).metadata.userId; const { error } = await supabaseAdmin.from("subscriptions").update({ - subscription_type: "cancelled", + status: "canceled", }).eq("user_id", userId); break; } diff --git a/apps/web/components/dashboard/main/ReturningUserHub.tsx b/apps/web/components/dashboard/main/ReturningUserHub.tsx index 93c24d3..585b827 100644 --- a/apps/web/components/dashboard/main/ReturningUserHub.tsx +++ b/apps/web/components/dashboard/main/ReturningUserHub.tsx @@ -194,7 +194,7 @@ export function ReturningUserHub({
Free Plan
- + diff --git a/apps/web/components/dashboard/settings/BillingInfo.tsx b/apps/web/components/dashboard/settings/BillingInfo.tsx index 15576fc..a5c8b4f 100644 --- a/apps/web/components/dashboard/settings/BillingInfo.tsx +++ b/apps/web/components/dashboard/settings/BillingInfo.tsx @@ -28,7 +28,7 @@ export function BillingInfo() { const { updateBilling, loadingBilling, billingDetails, fetchSubscriptionDetails } = useSettings() const router = useRouter() - const { supabase, user } = useSupabase() + const { supabase, user, fetchPlan, plans, planLoading } = useSupabase() const [isLoadingData, setIsLoadingData] = useState(true) @@ -37,9 +37,12 @@ export function BillingInfo() { useEffect(() => { if(user) { fetchSubscriptionDetails(user?.id) + fetchPlan() } }, [user]) + const findUserPlan = plans?.find(item => item.id === billingDetails?.plan_id) + useEffect(() => { const fetchBillingData = async () => { @@ -86,10 +89,10 @@ export function BillingInfo() { ) : ( <> -

{capitalize(billingDetails?.subscription_type) || "Free"} Plan

- {billingDetails?.subscription_end_date && ( +

{capitalize(findUserPlan?.name) || "Free"} Plan

+ {billingDetails?.current_period_end && (

- Next billing date: {formatDate(billingDetails.subscription_end_date) || "Please subscribe to a plan"} + Next billing date: {formatDate(billingDetails.current_period_end) || "Please subscribe to a plan"}

)} diff --git a/apps/web/components/landingPage/PricingSection.tsx b/apps/web/components/landingPage/PricingSection.tsx index 08d5ee9..6d9ccb8 100644 --- a/apps/web/components/landingPage/PricingSection.tsx +++ b/apps/web/components/landingPage/PricingSection.tsx @@ -5,12 +5,17 @@ import { Button } from "../ui/button" import { WobbleCard } from "../ui/wobble-card" import { usePathname, useRouter } from "next/navigation" import { toast } from "sonner" -import { useState } from "react" +import { useEffect, useState } from "react" import { useSupabase } from "../supabase-provider" +import { Skeleton } from "../ui/skeleton" +import { Features, Plans } from "@/types/plans" +import { capitalize } from "@/helpers/capitalize" +import { useSettings } from "@/hooks/useSettings" export default function PricingSection() { + const { loadingBilling, billingDetails, fetchSubscriptionDetails } = useSettings() const [loading, setLoading] = useState(false) - const { user } = useSupabase(); + const { user, supabase, fetchPlan, plans, planLoading } = useSupabase(); const pathname = usePathname() const router = useRouter() @@ -18,13 +23,21 @@ export default function PricingSection() { const pro__plan = process.env.NEXT_PUBLIC_PRO_PRICE const enterprice_plan = process.env.NEXT_PUBLIC__ENTERPRICE_PLAN - const handleStripeCheckout = async (price_id: string, sub_type: string) => { + + useEffect(() => { + if(user) { + fetchPlan() + fetchSubscriptionDetails(user?.id) + } + }, []) + + const handleStripeCheckout = async (plan_id: string, sub_type: string) => { if (loading) return setLoading(true) try { const response = await fetch('/api/stripe/create-subscription', { method: "POST", - body: JSON.stringify({ price_id,sub_type }) + body: JSON.stringify({ plan_id, sub_type }) }) const data = await response.json() @@ -42,9 +55,9 @@ export default function PricingSection() { } } - const handlePricingRoute = (price_id: string, sub_type: string) => { + const handlePricingRoute = (plan_id: string, sub_type: string) => { if (user) { - handleStripeCheckout(price_id, sub_type) + handleStripeCheckout(plan_id, sub_type) } else { router.push("/login") } @@ -93,71 +106,91 @@ export default function PricingSection() {
- {pricing.map((option, key) => ( + {plans && plans.map((option:Plans, key) => ( - {option?.isPopular && ( + {option?.name === "Pro" && (
POPULAR
)} -

- {option.heading} -

-
- ${option.price} - - /mo - -
-

- {option.description} -

-
    - {option.features.map((feature, featureKey) => ( -
  • - :

    + {option.name} +

    + } + { + planLoading || loadingBilling ? :
    + ${option.price_monthly} + + /mo + +
    + } + { + planLoading || loadingBilling ? :

    + {option.name === "Enterprise" + ? "For professional YouTubers and teams." + : option.name === "Pro" + ? "For serious content creators." + : "Perfect for trying out Script AI."} +

    + } + { + planLoading || loadingBilling ? <> + + :
      + {option.features.map((f: Features, featureKey) => ( +
    • - - - {feature} -
    • - ))} -
    - -
  • + ))} +
+ } + + { + planLoading || loadingBilling ? : - + } +
) )} diff --git a/apps/web/components/supabase-provider.tsx b/apps/web/components/supabase-provider.tsx index e46adc4..f232bbe 100644 --- a/apps/web/components/supabase-provider.tsx +++ b/apps/web/components/supabase-provider.tsx @@ -13,6 +13,7 @@ import { import { type SupabaseClient, type User, type Session } from "@supabase/supabase-js" import { createClient } from "@/lib/supabase/client" import { UserProfile } from "@repo/validation" +import { Plans } from "@/types/plans" type SupabaseContext = { supabase: SupabaseClient @@ -26,6 +27,9 @@ type SupabaseContext = { setProfile: Dispatch> profileLoading: boolean fetchUserProfile: (userId: string) => Promise + fetchPlan: () => Promise + plans: Plans[] | null + planLoading: boolean } // Suspense boundary helpers @@ -45,6 +49,9 @@ export function SupabaseProvider({ children }: { children: React.ReactNode }) { const [profile, setProfile] = useState(null) const [profileLoading, setProfileLoading] = useState(true) + const [planLoading, setplanLoading] = useState(false) + const [plans, setPlans] = useState(null) + // Generate referral code function generateReferralCode(): string { const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" @@ -100,6 +107,28 @@ export function SupabaseProvider({ children }: { children: React.ReactNode }) { } } + const fetchPlan = async () => { + setplanLoading(true) + try { + const { data, error } = await supabase + .from('plans') + .select('*') + .eq('is_active', true) + + if (error) { + console.error('Error fetching data:', error) + return [] + } + + setPlans(data) + return data + } catch (error) { + + } finally { + setplanLoading(false) + } + } + // Initial session loader const getInitialSession = async (): Promise => { const { data, error } = await supabase.auth.getSession() @@ -148,6 +177,7 @@ export function SupabaseProvider({ children }: { children: React.ReactNode }) { useEffect(() => { if (user) { fetchUserProfile(user.id) + fetchPlan() } else { setProfile(null) setProfileLoading(false) @@ -166,6 +196,9 @@ export function SupabaseProvider({ children }: { children: React.ReactNode }) { setProfile, profileLoading, fetchUserProfile, + plans, + fetchPlan, + planLoading } return {children} diff --git a/apps/web/hooks/useSettings.ts b/apps/web/hooks/useSettings.ts index 13d39c0..d001a7e 100644 --- a/apps/web/hooks/useSettings.ts +++ b/apps/web/hooks/useSettings.ts @@ -6,8 +6,8 @@ import { useSupabase } from "@/components/supabase-provider"; import { toast } from "sonner"; interface BillingDetails { - subscription_end_date: string; - subscription_type: string; + current_period_end: string; + plan_id: string; } export function useSettings @@ -152,12 +152,12 @@ const [isChangingPassword, setIsChangingPassword] = useState(false); try { const { data: existing } = await supabase .from("subscriptions") - .select("subscription_end_date, subscription_type") + .select("*") .eq("user_id", userId) .single() if(existing) { - setBillingDetails(existing) + setBillingDetails(existing as BillingDetails) } } catch (error) { diff --git a/apps/web/supabase/.gitignore b/apps/web/supabase/.gitignore deleted file mode 100644 index ad9264f..0000000 --- a/apps/web/supabase/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Supabase -.branches -.temp - -# dotenvx -.env.keys -.env.local -.env.*.local diff --git a/apps/web/supabase/config.toml b/apps/web/supabase/config.toml deleted file mode 100644 index 9f0dc21..0000000 --- a/apps/web/supabase/config.toml +++ /dev/null @@ -1,349 +0,0 @@ -# For detailed configuration reference documentation, visit: -# https://supabase.com/docs/guides/local-development/cli/config -# A string used to distinguish different Supabase projects on the same host. Defaults to the -# working directory name when running `supabase init`. -project_id = "web" - -[api] -enabled = true -# Port to use for the API URL. -port = 54321 -# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API -# endpoints. `public` and `graphql_public` schemas are included by default. -schemas = ["public", "graphql_public"] -# Extra schemas to add to the search_path of every request. -extra_search_path = ["public", "extensions"] -# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size -# for accidental or malicious requests. -max_rows = 1000 - -[api.tls] -# Enable HTTPS endpoints locally using a self-signed certificate. -enabled = false -# Paths to self-signed certificate pair. -# cert_path = "../certs/my-cert.pem" -# key_path = "../certs/my-key.pem" - -[db] -# Port to use for the local database URL. -port = 54322 -# Port used by db diff command to initialize the shadow database. -shadow_port = 54320 -# The database major version to use. This has to be the same as your remote database's. Run `SHOW -# server_version;` on the remote database to check. -major_version = 17 - -[db.pooler] -enabled = false -# Port to use for the local connection pooler. -port = 54329 -# Specifies when a server connection can be reused by other clients. -# Configure one of the supported pooler modes: `transaction`, `session`. -pool_mode = "transaction" -# How many server connections to allow per user/database pair. -default_pool_size = 20 -# Maximum number of client connections allowed. -max_client_conn = 100 - -# [db.vault] -# secret_key = "env(SECRET_VALUE)" - -[db.migrations] -# If disabled, migrations will be skipped during a db push or reset. -enabled = true -# Specifies an ordered list of schema files that describe your database. -# Supports glob patterns relative to supabase directory: "./schemas/*.sql" -schema_paths = [] - -[db.seed] -# If enabled, seeds the database after migrations during a db reset. -enabled = true -# Specifies an ordered list of seed files to load during db reset. -# Supports glob patterns relative to supabase directory: "./seeds/*.sql" -sql_paths = ["./seed.sql"] - -[db.network_restrictions] -# Enable management of network restrictions. -enabled = false -# List of IPv4 CIDR blocks allowed to connect to the database. -# Defaults to allow all IPv4 connections. Set empty array to block all IPs. -allowed_cidrs = ["0.0.0.0/0"] -# List of IPv6 CIDR blocks allowed to connect to the database. -# Defaults to allow all IPv6 connections. Set empty array to block all IPs. -allowed_cidrs_v6 = ["::/0"] - -[realtime] -enabled = true -# Bind realtime via either IPv4 or IPv6. (default: IPv4) -# ip_version = "IPv6" -# The maximum length in bytes of HTTP request headers. (default: 4096) -# max_header_length = 4096 - -[studio] -enabled = true -# Port to use for Supabase Studio. -port = 54323 -# External URL of the API server that frontend connects to. -api_url = "http://127.0.0.1" -# OpenAI API Key to use for Supabase AI in the Supabase Studio. -openai_api_key = "env(OPENAI_API_KEY)" - -# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they -# are monitored, and you can view the emails that would have been sent from the web interface. -[inbucket] -enabled = true -# Port to use for the email testing server web interface. -port = 54324 -# Uncomment to expose additional ports for testing user applications that send emails. -# smtp_port = 54325 -# pop3_port = 54326 -# admin_email = "admin@email.com" -# sender_name = "Admin" - -[storage] -enabled = true -# The maximum file size allowed (e.g. "5MB", "500KB"). -file_size_limit = "50MiB" - -# Image transformation API is available to Supabase Pro plan. -# [storage.image_transformation] -# enabled = true - -# Uncomment to configure local storage buckets -# [storage.buckets.images] -# public = false -# file_size_limit = "50MiB" -# allowed_mime_types = ["image/png", "image/jpeg"] -# objects_path = "./images" - -[auth] -enabled = true -# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used -# in emails. -site_url = "http://127.0.0.1:3000" -# A list of *exact* URLs that auth providers are permitted to redirect to post authentication. -additional_redirect_urls = ["https://127.0.0.1:3000"] -# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week). -jwt_expiry = 3600 -# Path to JWT signing key. DO NOT commit your signing keys file to git. -# signing_keys_path = "./signing_keys.json" -# If disabled, the refresh token will never expire. -enable_refresh_token_rotation = true -# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds. -# Requires enable_refresh_token_rotation = true. -refresh_token_reuse_interval = 10 -# Allow/disallow new user signups to your project. -enable_signup = true -# Allow/disallow anonymous sign-ins to your project. -enable_anonymous_sign_ins = false -# Allow/disallow testing manual linking of accounts -enable_manual_linking = false -# Passwords shorter than this value will be rejected as weak. Minimum 6, recommended 8 or more. -minimum_password_length = 6 -# Passwords that do not meet the following requirements will be rejected as weak. Supported values -# are: `letters_digits`, `lower_upper_letters_digits`, `lower_upper_letters_digits_symbols` -password_requirements = "" - -[auth.rate_limit] -# Number of emails that can be sent per hour. Requires auth.email.smtp to be enabled. -email_sent = 2 -# Number of SMS messages that can be sent per hour. Requires auth.sms to be enabled. -sms_sent = 30 -# Number of anonymous sign-ins that can be made per hour per IP address. Requires enable_anonymous_sign_ins = true. -anonymous_users = 30 -# Number of sessions that can be refreshed in a 5 minute interval per IP address. -token_refresh = 150 -# Number of sign up and sign-in requests that can be made in a 5 minute interval per IP address (excludes anonymous users). -sign_in_sign_ups = 30 -# Number of OTP / Magic link verifications that can be made in a 5 minute interval per IP address. -token_verifications = 30 -# Number of Web3 logins that can be made in a 5 minute interval per IP address. -web3 = 30 - -# Configure one of the supported captcha providers: `hcaptcha`, `turnstile`. -# [auth.captcha] -# enabled = true -# provider = "hcaptcha" -# secret = "" - -[auth.email] -# Allow/disallow new user signups via email to your project. -enable_signup = true -# If enabled, a user will be required to confirm any email change on both the old, and new email -# addresses. If disabled, only the new email is required to confirm. -double_confirm_changes = true -# If enabled, users need to confirm their email address before signing in. -enable_confirmations = false -# If enabled, users will need to reauthenticate or have logged in recently to change their password. -secure_password_change = false -# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email. -max_frequency = "1s" -# Number of characters used in the email OTP. -otp_length = 6 -# Number of seconds before the email OTP expires (defaults to 1 hour). -otp_expiry = 3600 - -# Use a production-ready SMTP server -# [auth.email.smtp] -# enabled = true -# host = "smtp.sendgrid.net" -# port = 587 -# user = "apikey" -# pass = "env(SENDGRID_API_KEY)" -# admin_email = "admin@email.com" -# sender_name = "Admin" - -# Uncomment to customize email template -# [auth.email.template.invite] -# subject = "You have been invited" -# content_path = "./supabase/templates/invite.html" - -[auth.sms] -# Allow/disallow new user signups via SMS to your project. -enable_signup = false -# If enabled, users need to confirm their phone number before signing in. -enable_confirmations = false -# Template for sending OTP to users -template = "Your code is {{ .Code }}" -# Controls the minimum amount of time that must pass before sending another sms otp. -max_frequency = "5s" - -# Use pre-defined map of phone number to OTP for testing. -# [auth.sms.test_otp] -# 4152127777 = "123456" - -# Configure logged in session timeouts. -# [auth.sessions] -# Force log out after the specified duration. -# timebox = "24h" -# Force log out if the user has been inactive longer than the specified duration. -# inactivity_timeout = "8h" - -# This hook runs before a new user is created and allows developers to reject the request based on the incoming user object. -# [auth.hook.before_user_created] -# enabled = true -# uri = "pg-functions://postgres/auth/before-user-created-hook" - -# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used. -# [auth.hook.custom_access_token] -# enabled = true -# uri = "pg-functions:////" - -# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`. -[auth.sms.twilio] -enabled = false -account_sid = "" -message_service_sid = "" -# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead: -auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)" - -# Multi-factor-authentication is available to Supabase Pro plan. -[auth.mfa] -# Control how many MFA factors can be enrolled at once per user. -max_enrolled_factors = 10 - -# Control MFA via App Authenticator (TOTP) -[auth.mfa.totp] -enroll_enabled = false -verify_enabled = false - -# Configure MFA via Phone Messaging -[auth.mfa.phone] -enroll_enabled = false -verify_enabled = false -otp_length = 6 -template = "Your code is {{ .Code }}" -max_frequency = "5s" - -# Configure MFA via WebAuthn -# [auth.mfa.web_authn] -# enroll_enabled = true -# verify_enabled = true - -# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`, -# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`, -# `twitter`, `slack`, `spotify`, `workos`, `zoom`. -[auth.external.apple] -enabled = false -client_id = "" -# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead: -secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)" -# Overrides the default auth redirectUrl. -redirect_uri = "" -# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure, -# or any other third-party OIDC providers. -url = "" -# If enabled, the nonce check will be skipped. Required for local sign in with Google auth. -skip_nonce_check = false -# If enabled, it will allow the user to successfully authenticate when the provider does not return an email address. -email_optional = false - -# Allow Solana wallet holders to sign in to your project via the Sign in with Solana (SIWS, EIP-4361) standard. -# You can configure "web3" rate limit in the [auth.rate_limit] section and set up [auth.captcha] if self-hosting. -[auth.web3.solana] -enabled = false - -# Use Firebase Auth as a third-party provider alongside Supabase Auth. -[auth.third_party.firebase] -enabled = false -# project_id = "my-firebase-project" - -# Use Auth0 as a third-party provider alongside Supabase Auth. -[auth.third_party.auth0] -enabled = false -# tenant = "my-auth0-tenant" -# tenant_region = "us" - -# Use AWS Cognito (Amplify) as a third-party provider alongside Supabase Auth. -[auth.third_party.aws_cognito] -enabled = false -# user_pool_id = "my-user-pool-id" -# user_pool_region = "us-east-1" - -# Use Clerk as a third-party provider alongside Supabase Auth. -[auth.third_party.clerk] -enabled = false -# Obtain from https://clerk.com/setup/supabase -# domain = "example.clerk.accounts.dev" - -# OAuth server configuration -[auth.oauth_server] -# Enable OAuth server functionality -enabled = false -# Path for OAuth consent flow UI -authorization_url_path = "/oauth/consent" -# Allow dynamic client registration -allow_dynamic_registration = false - -[edge_runtime] -enabled = true -# Supported request policies: `oneshot`, `per_worker`. -# `per_worker` (default) — enables hot reload during local development. -# `oneshot` — fallback mode if hot reload causes issues (e.g. in large repos or with symlinks). -policy = "per_worker" -# Port to attach the Chrome inspector for debugging edge functions. -inspector_port = 8083 -# The Deno major version to use. -deno_version = 2 - -# [edge_runtime.secrets] -# secret_key = "env(SECRET_VALUE)" - -[analytics] -enabled = true -port = 54327 -# Configure one of the supported backends: `postgres`, `bigquery`. -backend = "postgres" - -# Experimental features may be deprecated any time -[experimental] -# Configures Postgres storage engine to use OrioleDB (S3) -orioledb_version = "" -# Configures S3 bucket URL, eg. .s3-.amazonaws.com -s3_host = "env(S3_HOST)" -# Configures S3 bucket region, eg. us-east-1 -s3_region = "env(S3_REGION)" -# Configures AWS_ACCESS_KEY_ID for S3 bucket -s3_access_key = "env(S3_ACCESS_KEY)" -# Configures AWS_SECRET_ACCESS_KEY for S3 bucket -s3_secret_key = "env(S3_SECRET_KEY)" diff --git a/apps/web/supabase/migrations/20251018212434_remote_schema.sql b/apps/web/supabase/migrations/20251018212434_remote_schema.sql deleted file mode 100644 index e0dd6c6..0000000 --- a/apps/web/supabase/migrations/20251018212434_remote_schema.sql +++ /dev/null @@ -1,610 +0,0 @@ -create table "public"."profiles" ( - "id" uuid not null, - "user_id" uuid, - "name" text, - "email" text, - "credits" integer default 10, - "ai_trained" boolean default false, - "referral_code" character varying(10), - "referred_by" character varying(10), - "total_referrals" integer default 0, - "referral_credits" integer default 0, - "created_at" timestamp with time zone default now(), - "updated_at" timestamp with time zone default now(), - "avatar_url" text, - "full_name" text, - "youtube_connected" boolean, - "language" text -); - - -alter table "public"."profiles" enable row level security; - -create table "public"."referrals" ( - "id" uuid not null default uuid_generate_v4(), - "referrer_id" uuid, - "referred_user_id" uuid, - "referral_code" character varying(10) not null, - "status" character varying(20) default 'pending'::character varying, - "credits_awarded" integer default 0, - "created_at" timestamp with time zone default now(), - "completed_at" timestamp with time zone -); - - -alter table "public"."referrals" enable row level security; - -create table "public"."scripts" ( - "id" uuid not null default uuid_generate_v4(), - "user_id" uuid, - "title" text not null, - "content" text not null, - "prompt" text, - "context" text, - "tone" text, - "include_storytelling" boolean default false, - "script_references" text, - "language" text default 'english'::text, - "created_at" timestamp with time zone default now(), - "updated_at" timestamp with time zone default now() -); - - -alter table "public"."scripts" enable row level security; - -create table "public"."subscriptions" ( - "id" uuid not null default gen_random_uuid(), - "created_at" timestamp with time zone not null default now(), - "user_id" uuid default auth.uid(), - "stripe_customer_id" text, - "stripe_subscription_id" text, - "subscription_type" text default ''::text, - "subscription_end_date" timestamp with time zone -); - - -create table "public"."user_style" ( - "id" uuid not null default uuid_generate_v4(), - "user_id" uuid, - "tone" text, - "vocabulary_level" text, - "pacing" text, - "themes" text, - "humor_style" text, - "structure" text, - "video_urls" text[], - "created_at" timestamp with time zone default now(), - "updated_at" timestamp with time zone default now() -); - - -alter table "public"."user_style" enable row level security; - -create table "public"."youtube_channels" ( - "id" uuid not null default uuid_generate_v4(), - "user_id" uuid not null, - "channel_id" text not null, - "channel_name" text, - "channel_description" text, - "custom_url" text, - "published_at" timestamp with time zone, - "country" text, - "thumbnail" text, - "default_language" text, - "view_count" bigint, - "subscriber_count" bigint, - "video_count" bigint, - "is_linked" boolean, - "text_color" text, - "background_color" text, - "topic_details" jsonb, - "updated_at" timestamp with time zone default now() -); - - -alter table "public"."youtube_channels" enable row level security; - -CREATE INDEX idx_profiles_referral_code ON public.profiles USING btree (referral_code); - -CREATE INDEX idx_profiles_user_id ON public.profiles USING btree (user_id); - -CREATE INDEX idx_referrals_referred_user_id ON public.referrals USING btree (referred_user_id); - -CREATE INDEX idx_referrals_referrer_id ON public.referrals USING btree (referrer_id); - -CREATE INDEX idx_scripts_user_id ON public.scripts USING btree (user_id); - -CREATE INDEX idx_user_style_user_id ON public.user_style USING btree (user_id); - -CREATE UNIQUE INDEX profiles_pkey ON public.profiles USING btree (id); - -CREATE UNIQUE INDEX profiles_referral_code_key ON public.profiles USING btree (referral_code); - -CREATE UNIQUE INDEX referrals_pkey ON public.referrals USING btree (id); - -CREATE UNIQUE INDEX referrals_referrer_id_referred_user_id_key ON public.referrals USING btree (referrer_id, referred_user_id); - -CREATE UNIQUE INDEX scripts_pkey ON public.scripts USING btree (id); - -CREATE UNIQUE INDEX subscriptions_pkey ON public.subscriptions USING btree (id); - -CREATE UNIQUE INDEX unique_user_channel ON public.youtube_channels USING btree (user_id, channel_id); - -CREATE UNIQUE INDEX user_style_pkey ON public.user_style USING btree (id); - -CREATE UNIQUE INDEX youtube_channels_pkey ON public.youtube_channels USING btree (id); - -alter table "public"."profiles" add constraint "profiles_pkey" PRIMARY KEY using index "profiles_pkey"; - -alter table "public"."referrals" add constraint "referrals_pkey" PRIMARY KEY using index "referrals_pkey"; - -alter table "public"."scripts" add constraint "scripts_pkey" PRIMARY KEY using index "scripts_pkey"; - -alter table "public"."subscriptions" add constraint "subscriptions_pkey" PRIMARY KEY using index "subscriptions_pkey"; - -alter table "public"."user_style" add constraint "user_style_pkey" PRIMARY KEY using index "user_style_pkey"; - -alter table "public"."youtube_channels" add constraint "youtube_channels_pkey" PRIMARY KEY using index "youtube_channels_pkey"; - -alter table "public"."profiles" add constraint "fk_referred_by" FOREIGN KEY (referred_by) REFERENCES profiles(referral_code) not valid; - -alter table "public"."profiles" validate constraint "fk_referred_by"; - -alter table "public"."profiles" add constraint "profiles_id_fkey" FOREIGN KEY (id) REFERENCES auth.users(id) ON DELETE CASCADE not valid; - -alter table "public"."profiles" validate constraint "profiles_id_fkey"; - -alter table "public"."profiles" add constraint "profiles_referral_code_key" UNIQUE using index "profiles_referral_code_key"; - -alter table "public"."profiles" add constraint "profiles_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE not valid; - -alter table "public"."profiles" validate constraint "profiles_user_id_fkey"; - -alter table "public"."referrals" add constraint "referrals_referred_user_id_fkey" FOREIGN KEY (referred_user_id) REFERENCES auth.users(id) ON DELETE CASCADE not valid; - -alter table "public"."referrals" validate constraint "referrals_referred_user_id_fkey"; - -alter table "public"."referrals" add constraint "referrals_referrer_id_fkey" FOREIGN KEY (referrer_id) REFERENCES auth.users(id) ON DELETE CASCADE not valid; - -alter table "public"."referrals" validate constraint "referrals_referrer_id_fkey"; - -alter table "public"."referrals" add constraint "referrals_referrer_id_referred_user_id_key" UNIQUE using index "referrals_referrer_id_referred_user_id_key"; - -alter table "public"."scripts" add constraint "scripts_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE not valid; - -alter table "public"."scripts" validate constraint "scripts_user_id_fkey"; - -alter table "public"."user_style" add constraint "user_style_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE not valid; - -alter table "public"."user_style" validate constraint "user_style_user_id_fkey"; - -alter table "public"."youtube_channels" add constraint "unique_user_channel" UNIQUE using index "unique_user_channel"; - -alter table "public"."youtube_channels" add constraint "youtube_channels_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) not valid; - -alter table "public"."youtube_channels" validate constraint "youtube_channels_user_id_fkey"; - -set check_function_bodies = off; - -CREATE OR REPLACE FUNCTION public.handle_new_user() - RETURNS trigger - LANGUAGE plpgsql - SECURITY DEFINER -AS $function$ -BEGIN - INSERT INTO public.profiles (id, user_id, email) - VALUES (NEW.id, NEW.id, NEW.email); - RETURN NEW; -END; -$function$ -; - -CREATE OR REPLACE FUNCTION public.handle_updated_at() - RETURNS trigger - LANGUAGE plpgsql -AS $function$ -BEGIN - NEW.updated_at = NOW(); - RETURN NEW; -END; -$function$ -; - -grant delete on table "public"."profiles" to "anon"; - -grant insert on table "public"."profiles" to "anon"; - -grant references on table "public"."profiles" to "anon"; - -grant select on table "public"."profiles" to "anon"; - -grant trigger on table "public"."profiles" to "anon"; - -grant truncate on table "public"."profiles" to "anon"; - -grant update on table "public"."profiles" to "anon"; - -grant delete on table "public"."profiles" to "authenticated"; - -grant insert on table "public"."profiles" to "authenticated"; - -grant references on table "public"."profiles" to "authenticated"; - -grant select on table "public"."profiles" to "authenticated"; - -grant trigger on table "public"."profiles" to "authenticated"; - -grant truncate on table "public"."profiles" to "authenticated"; - -grant update on table "public"."profiles" to "authenticated"; - -grant delete on table "public"."profiles" to "service_role"; - -grant insert on table "public"."profiles" to "service_role"; - -grant references on table "public"."profiles" to "service_role"; - -grant select on table "public"."profiles" to "service_role"; - -grant trigger on table "public"."profiles" to "service_role"; - -grant truncate on table "public"."profiles" to "service_role"; - -grant update on table "public"."profiles" to "service_role"; - -grant delete on table "public"."referrals" to "anon"; - -grant insert on table "public"."referrals" to "anon"; - -grant references on table "public"."referrals" to "anon"; - -grant select on table "public"."referrals" to "anon"; - -grant trigger on table "public"."referrals" to "anon"; - -grant truncate on table "public"."referrals" to "anon"; - -grant update on table "public"."referrals" to "anon"; - -grant delete on table "public"."referrals" to "authenticated"; - -grant insert on table "public"."referrals" to "authenticated"; - -grant references on table "public"."referrals" to "authenticated"; - -grant select on table "public"."referrals" to "authenticated"; - -grant trigger on table "public"."referrals" to "authenticated"; - -grant truncate on table "public"."referrals" to "authenticated"; - -grant update on table "public"."referrals" to "authenticated"; - -grant delete on table "public"."referrals" to "service_role"; - -grant insert on table "public"."referrals" to "service_role"; - -grant references on table "public"."referrals" to "service_role"; - -grant select on table "public"."referrals" to "service_role"; - -grant trigger on table "public"."referrals" to "service_role"; - -grant truncate on table "public"."referrals" to "service_role"; - -grant update on table "public"."referrals" to "service_role"; - -grant delete on table "public"."scripts" to "anon"; - -grant insert on table "public"."scripts" to "anon"; - -grant references on table "public"."scripts" to "anon"; - -grant select on table "public"."scripts" to "anon"; - -grant trigger on table "public"."scripts" to "anon"; - -grant truncate on table "public"."scripts" to "anon"; - -grant update on table "public"."scripts" to "anon"; - -grant delete on table "public"."scripts" to "authenticated"; - -grant insert on table "public"."scripts" to "authenticated"; - -grant references on table "public"."scripts" to "authenticated"; - -grant select on table "public"."scripts" to "authenticated"; - -grant trigger on table "public"."scripts" to "authenticated"; - -grant truncate on table "public"."scripts" to "authenticated"; - -grant update on table "public"."scripts" to "authenticated"; - -grant delete on table "public"."scripts" to "service_role"; - -grant insert on table "public"."scripts" to "service_role"; - -grant references on table "public"."scripts" to "service_role"; - -grant select on table "public"."scripts" to "service_role"; - -grant trigger on table "public"."scripts" to "service_role"; - -grant truncate on table "public"."scripts" to "service_role"; - -grant update on table "public"."scripts" to "service_role"; - -grant delete on table "public"."subscriptions" to "anon"; - -grant insert on table "public"."subscriptions" to "anon"; - -grant references on table "public"."subscriptions" to "anon"; - -grant select on table "public"."subscriptions" to "anon"; - -grant trigger on table "public"."subscriptions" to "anon"; - -grant truncate on table "public"."subscriptions" to "anon"; - -grant update on table "public"."subscriptions" to "anon"; - -grant delete on table "public"."subscriptions" to "authenticated"; - -grant insert on table "public"."subscriptions" to "authenticated"; - -grant references on table "public"."subscriptions" to "authenticated"; - -grant select on table "public"."subscriptions" to "authenticated"; - -grant trigger on table "public"."subscriptions" to "authenticated"; - -grant truncate on table "public"."subscriptions" to "authenticated"; - -grant update on table "public"."subscriptions" to "authenticated"; - -grant delete on table "public"."subscriptions" to "service_role"; - -grant insert on table "public"."subscriptions" to "service_role"; - -grant references on table "public"."subscriptions" to "service_role"; - -grant select on table "public"."subscriptions" to "service_role"; - -grant trigger on table "public"."subscriptions" to "service_role"; - -grant truncate on table "public"."subscriptions" to "service_role"; - -grant update on table "public"."subscriptions" to "service_role"; - -grant delete on table "public"."user_style" to "anon"; - -grant insert on table "public"."user_style" to "anon"; - -grant references on table "public"."user_style" to "anon"; - -grant select on table "public"."user_style" to "anon"; - -grant trigger on table "public"."user_style" to "anon"; - -grant truncate on table "public"."user_style" to "anon"; - -grant update on table "public"."user_style" to "anon"; - -grant delete on table "public"."user_style" to "authenticated"; - -grant insert on table "public"."user_style" to "authenticated"; - -grant references on table "public"."user_style" to "authenticated"; - -grant select on table "public"."user_style" to "authenticated"; - -grant trigger on table "public"."user_style" to "authenticated"; - -grant truncate on table "public"."user_style" to "authenticated"; - -grant update on table "public"."user_style" to "authenticated"; - -grant delete on table "public"."user_style" to "service_role"; - -grant insert on table "public"."user_style" to "service_role"; - -grant references on table "public"."user_style" to "service_role"; - -grant select on table "public"."user_style" to "service_role"; - -grant trigger on table "public"."user_style" to "service_role"; - -grant truncate on table "public"."user_style" to "service_role"; - -grant update on table "public"."user_style" to "service_role"; - -grant delete on table "public"."youtube_channels" to "anon"; - -grant insert on table "public"."youtube_channels" to "anon"; - -grant references on table "public"."youtube_channels" to "anon"; - -grant select on table "public"."youtube_channels" to "anon"; - -grant trigger on table "public"."youtube_channels" to "anon"; - -grant truncate on table "public"."youtube_channels" to "anon"; - -grant update on table "public"."youtube_channels" to "anon"; - -grant delete on table "public"."youtube_channels" to "authenticated"; - -grant insert on table "public"."youtube_channels" to "authenticated"; - -grant references on table "public"."youtube_channels" to "authenticated"; - -grant select on table "public"."youtube_channels" to "authenticated"; - -grant trigger on table "public"."youtube_channels" to "authenticated"; - -grant truncate on table "public"."youtube_channels" to "authenticated"; - -grant update on table "public"."youtube_channels" to "authenticated"; - -grant delete on table "public"."youtube_channels" to "service_role"; - -grant insert on table "public"."youtube_channels" to "service_role"; - -grant references on table "public"."youtube_channels" to "service_role"; - -grant select on table "public"."youtube_channels" to "service_role"; - -grant trigger on table "public"."youtube_channels" to "service_role"; - -grant truncate on table "public"."youtube_channels" to "service_role"; - -grant update on table "public"."youtube_channels" to "service_role"; - -create policy "Users can insert own profile" -on "public"."profiles" -as permissive -for insert -to public -with check ((auth.uid() = id)); - - -create policy "Users can update own profile" -on "public"."profiles" -as permissive -for update -to public -using ((auth.uid() = id)); - - -create policy "Users can view own profile" -on "public"."profiles" -as permissive -for select -to public -using ((auth.uid() = id)); - - -create policy "Users can insert referrals" -on "public"."referrals" -as permissive -for insert -to public -with check ((auth.uid() = referrer_id)); - - -create policy "Users can view referrals they made" -on "public"."referrals" -as permissive -for select -to public -using ((auth.uid() = referrer_id)); - - -create policy "Users can view referrals they received" -on "public"."referrals" -as permissive -for select -to public -using ((auth.uid() = referred_user_id)); - - -create policy "Users can delete own scripts" -on "public"."scripts" -as permissive -for delete -to public -using ((auth.uid() = user_id)); - - -create policy "Users can insert own scripts" -on "public"."scripts" -as permissive -for insert -to public -with check ((auth.uid() = user_id)); - - -create policy "Users can update own scripts" -on "public"."scripts" -as permissive -for update -to public -using ((auth.uid() = user_id)); - - -create policy "Users can view own scripts" -on "public"."scripts" -as permissive -for select -to public -using ((auth.uid() = user_id)); - - -create policy "Users can delete own style" -on "public"."user_style" -as permissive -for delete -to public -using ((auth.uid() = user_id)); - - -create policy "Users can insert own style" -on "public"."user_style" -as permissive -for insert -to public -with check ((auth.uid() = user_id)); - - -create policy "Users can update own style" -on "public"."user_style" -as permissive -for update -to public -using ((auth.uid() = user_id)); - - -create policy "Users can view own style" -on "public"."user_style" -as permissive -for select -to public -using ((auth.uid() = user_id)); - - -create policy "Users can insert their own channel data" -on "public"."youtube_channels" -as permissive -for insert -to public -with check ((auth.uid() = user_id)); - - -create policy "Users can update their own channel data" -on "public"."youtube_channels" -as permissive -for update -to public -using ((auth.uid() = user_id)); - - -create policy "Users can view their own channel data" -on "public"."youtube_channels" -as permissive -for select -to public -using ((auth.uid() = user_id)); - - -CREATE TRIGGER set_updated_at_profiles BEFORE UPDATE ON public.profiles FOR EACH ROW EXECUTE FUNCTION handle_updated_at(); - -CREATE TRIGGER set_updated_at_scripts BEFORE UPDATE ON public.scripts FOR EACH ROW EXECUTE FUNCTION handle_updated_at(); - -CREATE TRIGGER set_updated_at_user_style BEFORE UPDATE ON public.user_style FOR EACH ROW EXECUTE FUNCTION handle_updated_at(); - - -CREATE TRIGGER on_auth_user_created AFTER INSERT ON auth.users FOR EACH ROW EXECUTE FUNCTION handle_new_user(); - - diff --git a/apps/web/types/plans.ts b/apps/web/types/plans.ts new file mode 100644 index 0000000..16a42e1 --- /dev/null +++ b/apps/web/types/plans.ts @@ -0,0 +1,15 @@ + +export interface Features { + feature: string; + limit: string +} + +export interface Plans { + id: string; + name: string; + price_monthly: number; + is_active: boolean; + credits_monthly: boolean; + created_at: string; + features: Features[] +} \ No newline at end of file diff --git a/apps/web/types/subscription.ts b/apps/web/types/subscription.ts index 46b0749..bab1f96 100644 --- a/apps/web/types/subscription.ts +++ b/apps/web/types/subscription.ts @@ -1 +1,13 @@ -export type SubType = "Pro" | "Enterprise" \ No newline at end of file +export type SubType = "Pro" | "Enterprise" | "Free" + +export interface Subscription { + id: string + user_id: string + plan_id: string + stripe_subscription_id: string + status: string + current_period_start: string + current_period_end: string + created_at: string + updated_at: string +} \ No newline at end of file From b0220a2d1ef6b85a9588c86f8faf9b9b8565061b Mon Sep 17 00:00:00 2001 From: semz-ui Date: Thu, 23 Oct 2025 19:05:34 +0100 Subject: [PATCH 08/10] stripe key update --- README.md | 20 +- .../api/stripe/create-subscription/route.ts | 32 ++- apps/web/app/api/stripe/route.ts | 38 ++++ apps/web/app/api/stripe/webhook/route.ts | 184 ++++++++++++++---- .../dashboard/settings/BillingInfo.tsx | 28 ++- .../components/landingPage/PricingSection.tsx | 13 +- apps/web/hooks/useSettings.ts | 9 +- 7 files changed, 258 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index d23f073..bb0c78a 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,21 @@ ``` - Note: This uses the schema from `packages/supabase/migrations/` and won't add any seed data. -4. **Set up environment variables** +4. **Set up Stripe Integration (Local Setup)** +- Get pk_test_***** and sk_test_**** from your stripe dashboard [https://dashboard.stripe.com/acct_*****/test/apikeys](https://dashboard.stripe.com/) +- In another terminal, run Stripe CLI to forward events to your local webhook endpoint + ```bash + stripe listen --forward-to localhost:3000/api/stripe/webhook +- webhook secret will be generate in your terminal whsec_****** + ``` + - Edit `apps/web/.env` and `apps/api/.env` to include your stripe credentials: + ``` + NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=whsec_****** + STRIPE_SECRET_KEY=pk_test_****** + STRIPE_WEBHOOK_SECRET=sk_test_****** + ``` + +5. **Set up environment variables** ```bash # Copy example environment files cp apps/web/.env.example apps/web/.env @@ -70,12 +84,12 @@ ``` (Get these from your Supabase dashboard under Settings > API.) -5. **Start development servers** +6. **Start development servers** ```bash pnpm run dev ``` -6. **Open your browser** +7. **Open your browser** - Frontend: [http://localhost:3000](http://localhost:3000) - Backend: [http://localhost:8000](http://localhost:8000) diff --git a/apps/web/app/api/stripe/create-subscription/route.ts b/apps/web/app/api/stripe/create-subscription/route.ts index b354e65..f293638 100644 --- a/apps/web/app/api/stripe/create-subscription/route.ts +++ b/apps/web/app/api/stripe/create-subscription/route.ts @@ -13,11 +13,11 @@ export async function POST(req: Request) { return NextResponse.json({ message: 'Unauthorized' }, { status: 401 }); } - const { sub_type, plan_id } = await req.json(); + const { sub_type, plan_id, stripe_customer_id } = await req.json(); const { data: existing } = await supabase .from("subscriptions") - .select("stripe_subscription_id, plan_id") + .select("stripe_subscription_id, plan_id, stripe_customer_id") .eq("user_id", user.id) .single() const { data: profile } = await supabase @@ -26,21 +26,30 @@ export async function POST(req: Request) { .eq("user_id", user.id) .single() - if(existing?.plan_id === plan_id){ - return NextResponse.json({ error: `${sub_type} is your active plan, Please select a different active. plan if you want to upgrade` }, { status: 400 }); - } + // if(existing?.plan_id === plan_id){ + // return NextResponse.json({ error: `${sub_type} is your active plan, Please select a different active. plan if you want to upgrade` }, { status: 400 }); + // } + + let customerId; - const customer = await stripe.customers.create({ + if(stripe_customer_id) { + customerId = stripe_customer_id + }else { + const customer = await stripe.customers.create({ name: profile?.full_name, metadata: { user_id: user.id }, email: user.email }) + customerId = customer.id + } + + const session = await stripe.checkout.sessions.create({ mode: "subscription", payment_method_types: ["card"], - customer: customer.id, + customer: customerId, line_items: [ { price: sub_type === "Pro" ? pro__plan : enterprice_plan, @@ -53,7 +62,14 @@ export async function POST(req: Request) { user_id: user.id, sub_type: sub_type, plan_id: plan_id - } + }, + subscription_data: { + metadata: { + user_id: user.id, + sub_type, + plan_id, + } + } }); diff --git a/apps/web/app/api/stripe/route.ts b/apps/web/app/api/stripe/route.ts index e69de29..d823fa5 100644 --- a/apps/web/app/api/stripe/route.ts +++ b/apps/web/app/api/stripe/route.ts @@ -0,0 +1,38 @@ +import { stripe } from "@/lib/stripe"; +import { createClient } from "@/lib/supabase/server"; +import { NextResponse } from "next/server"; + +export async function GET(request: Request) { + try { + const supabase = await createClient(); + const { data: { user }, error: userError } = await supabase.auth.getUser(); + + if (userError || !user) { + return NextResponse.json({ message: 'Unauthorized' }, { status: 401 }); + } + + const { data: existing } = await supabase + .from("subscriptions") + .select("stripe_customer_id") + .eq("user_id", user.id) + .single() + + const charges = await stripe.charges.list({ + customer: existing?.stripe_customer_id, // Customer ID + limit: 100, + }); + + // Or using PaymentIntents (recommended for newer integrations) + const paymentIntents = await stripe.paymentIntents.list({ + customer: existing?.stripe_customer_id, + limit: 100, + }); + return NextResponse.json({ + charges, + paymentIntents + + }); + } catch (error:any) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} \ No newline at end of file diff --git a/apps/web/app/api/stripe/webhook/route.ts b/apps/web/app/api/stripe/webhook/route.ts index 68fb41a..ca4b8a6 100644 --- a/apps/web/app/api/stripe/webhook/route.ts +++ b/apps/web/app/api/stripe/webhook/route.ts @@ -34,6 +34,105 @@ export async function POST(req: Request) { let sub_type: SubType = session.metadata?.sub_type; let plan_id: SubType = session.metadata?.plan_id; + // if (!userId) { + // try { + // const customer = await stripe.customers.retrieve(customerId); + // userId = (customer as any).metadata?.user_id ?? (customer as any).metadata?.userId; + // } catch (err) { + // console.error("Failed to retrieve Stripe customer:", err); + // } + // } + + // if (!userId) { + // console.warn("No userId found for subscription.created event, skipping credit increment."); + // break; + // } + + // console.log("subscription created", userId); + + // Read current credits and update + // try { + // const { data: userRow, error: fetchError } = await supabaseAdmin + // .from("profiles") + // .select("credits") + // .eq("user_id", userId) + // .single(); + + // if (fetchError) { + // console.error("Failed to fetch user for credit update:", fetchError); + // break; + // } + // const amount_to_add = sub_type === "Pro" ? 5000 : 100000; + // const currentCredits = (userRow as any)?.credits ?? 0; + // const newCredits = currentCredits + amount_to_add + + // const { error: updateError } = await supabaseAdmin + // .from("profiles") + // .update({ credits: newCredits }) + // .eq("user_id", userId); + + // if (updateError) { + // console.error("Failed to update user credits:", updateError); + // break; + // } + + // const { data: existing, error } = await supabaseAdmin + // .from("subscriptions") + // .select("stripe_customer_id, stripe_subscription_id") + // .eq("user_id", userId) + // .single() + + + // const subscription_data: any = await stripe.subscriptions.retrieve(subscriptionId); + // const subscriptionItemEndDate = subscription_data.items.data[0].current_period_end; + // const subscriptionItemStartDate = subscription_data.items.data[0].current_period_start; + // const starts = new Date(subscriptionItemStartDate * 1000) + // const expires = new Date(subscriptionItemEndDate * 1000) + + // const { error: subError } = await supabaseAdmin.from("subscriptions").upsert({ + // user_id: userId, + // plan_id: plan_id, + // stripe_subscription_id: session.subscription, + // current_period_end: expires.toUTCString(), + // current_period_start: starts.toUTCString(), + // status: "active", + // updated_at: new Date().toISOString(), + // stripe_customer_id: session.customer + // }) + + // if (subError) { + // console.log(error, "errorrr") + // return NextResponse.json({ message: 'Something happened', error }, { status: 401 }); + // } + + + // } catch (err) { + // console.error("Error updating credits:", err); + // } + break; + } + + case "customer.subscription.created": { + const sub = event.data.object as any; + + // console.log(sub) + break; + } + case "invoice.payment_succeeded": { + let userId; + const invoice = event.data.object as any; + const customerId = invoice.customer; + + const subscription = await stripe.subscriptions.retrieve( + invoice.parent.subscription_details.subscription + ); + + console.log(customerId, "customerId") + + const metadata = subscription.metadata + userId = metadata?.user_id + const plan_id = metadata?.plan_id + if (!userId) { try { const customer = await stripe.customers.retrieve(customerId); @@ -43,15 +142,15 @@ export async function POST(req: Request) { } } + if (!userId) { console.warn("No userId found for subscription.created event, skipping credit increment."); break; } - // console.log("subscription created", userId); + try { - // Read current credits and update - try { + const { data: userRow, error: fetchError } = await supabaseAdmin .from("profiles") .select("credits") @@ -62,7 +161,7 @@ export async function POST(req: Request) { console.error("Failed to fetch user for credit update:", fetchError); break; } - const amount_to_add = sub_type === "Pro" ? 5000 : 100000; + const amount_to_add = 5000; const currentCredits = (userRow as any)?.credits ?? 0; const newCredits = currentCredits + amount_to_add @@ -83,46 +182,50 @@ export async function POST(req: Request) { .single() - const subscription_data: any = await stripe.subscriptions.retrieve(subscriptionId); - const subscriptionItemEndDate = subscription_data.items.data[0].current_period_end; - const subscriptionItemStartDate = subscription_data.items.data[0].current_period_start; - const starts = new Date(subscriptionItemStartDate * 1000) - const expires = new Date(subscriptionItemEndDate * 1000) - - const { error:subError } = await supabaseAdmin.from("subscriptions").upsert({ - user_id: userId, - plan_id: plan_id, - stripe_subscription_id: session.subscription, - current_period_end: expires.toUTCString(), - current_period_start: starts.toUTCString(), - status: "active", - updated_at: new Date().toISOString(), - }) - - if (subError) { - console.log(error, "errorrr") - return NextResponse.json({ message: 'Something happened', error }, { status: 401 }); - } + const subscription_data: any = await stripe.subscriptions.retrieve(subscription.id); + const subscriptionItemEndDate = subscription_data.items.data[0].current_period_end; + const subscriptionItemStartDate = subscription_data.items.data[0].current_period_start; + const starts = new Date(subscriptionItemStartDate * 1000) + const expires = new Date(subscriptionItemEndDate * 1000) + let sub_error; + + if(existing) { + const { error: subError } = await supabaseAdmin.from("subscriptions").update({ + user_id: userId, + plan_id: plan_id, + stripe_subscription_id: subscription.id, + current_period_end: expires.toUTCString(), + current_period_start: starts.toUTCString(), + status: "active", + updated_at: new Date().toISOString(), + stripe_customer_id: customerId + }).eq("stripe_customer_id",customerId) + sub_error = subError + } else { + const { error: subError } = await supabaseAdmin.from("subscriptions").upsert({ + user_id: userId, + plan_id: plan_id, + stripe_subscription_id: subscription.id, + current_period_end: expires.toUTCString(), + current_period_start: starts.toUTCString(), + status: "active", + updated_at: new Date().toISOString(), + stripe_customer_id: customerId + }) + sub_error = subError + } + + if (sub_error) { + console.log(error, "errorrr") + return NextResponse.json({ message: 'Something happened', error }, { status: 401 }); + } + - } catch (err) { console.error("Error updating credits:", err); } - break; - } - case "customer.subscription.created": { - const sub = event.data.object as any; - - console.log(sub) - break; - } - case "invoice.payment_succeeded": { - const invoice = event.data.object as any; - const customerId = invoice.customer; - const customer = await stripe.customers.retrieve(customerId); - const userId = (customer as any).metadata.userId; - // console.log(invoice, "invoice") + // await db.user.update({ // where: { id: userId }, @@ -149,10 +252,11 @@ export async function POST(req: Request) { const subscription = event.data.object as any; const customerId = subscription.customer; const customer = await stripe.customers.retrieve(customerId); - const userId = (customer as any).metadata.userId; + const userId = (customer as any).metadata.user_id; const { error } = await supabaseAdmin.from("subscriptions").update({ status: "canceled", + plan_id: "51cb98f9-192a-4464-9a15-e3114aaf0c20" }).eq("user_id", userId); break; } diff --git a/apps/web/components/dashboard/settings/BillingInfo.tsx b/apps/web/components/dashboard/settings/BillingInfo.tsx index a5c8b4f..8f761c8 100644 --- a/apps/web/components/dashboard/settings/BillingInfo.tsx +++ b/apps/web/components/dashboard/settings/BillingInfo.tsx @@ -19,14 +19,14 @@ interface BillingData { paymentMethod: string | null } -interface BillingDetails { +interface SubscriptionDetails { subscription_end_date: string; subscription_type: string; } export function BillingInfo() { - const { updateBilling, loadingBilling, billingDetails, fetchSubscriptionDetails } = useSettings() + const { updateBilling, loadingBilling, subscriptionDetails, fetchSubscriptionDetails } = useSettings() const router = useRouter() const { supabase, user, fetchPlan, plans, planLoading } = useSupabase() @@ -41,7 +41,25 @@ export function BillingInfo() { } }, [user]) - const findUserPlan = plans?.find(item => item.id === billingDetails?.plan_id) + const findUserPlan = plans?.find(item => item.id === subscriptionDetails?.plan_id) + + const fetchTransactionHistory = async() => { + const res = await fetch("/api/stripe", { + method: "GET" + }) + + if(!res.ok) throw new Error("Failed to fetch transaction histpry") + + const data = await res.json() + console.log(data) + return + } + + useEffect(() => { + if(subscriptionDetails) { + fetchTransactionHistory() + } + }, [subscriptionDetails]) useEffect(() => { @@ -90,9 +108,9 @@ export function BillingInfo() { ) : ( <>

{capitalize(findUserPlan?.name) || "Free"} Plan

- {billingDetails?.current_period_end && ( + {subscriptionDetails?.current_period_end && (

- Next billing date: {formatDate(billingDetails.current_period_end) || "Please subscribe to a plan"} + Next billing date: {formatDate(subscriptionDetails.current_period_end) || "Please subscribe to a plan"}

)} diff --git a/apps/web/components/landingPage/PricingSection.tsx b/apps/web/components/landingPage/PricingSection.tsx index 6d9ccb8..4a4ad6c 100644 --- a/apps/web/components/landingPage/PricingSection.tsx +++ b/apps/web/components/landingPage/PricingSection.tsx @@ -13,7 +13,7 @@ import { capitalize } from "@/helpers/capitalize" import { useSettings } from "@/hooks/useSettings" export default function PricingSection() { - const { loadingBilling, billingDetails, fetchSubscriptionDetails } = useSettings() + const { loadingBilling, subscriptionDetails, fetchSubscriptionDetails } = useSettings() const [loading, setLoading] = useState(false) const { user, supabase, fetchPlan, plans, planLoading } = useSupabase(); @@ -31,13 +31,13 @@ export default function PricingSection() { } }, []) - const handleStripeCheckout = async (plan_id: string, sub_type: string) => { + const handleStripeCheckout = async (plan_id: string, sub_type: string, ) => { if (loading) return setLoading(true) try { const response = await fetch('/api/stripe/create-subscription', { method: "POST", - body: JSON.stringify({ plan_id, sub_type }) + body: JSON.stringify({ plan_id, sub_type, stripe_customer_id: subscriptionDetails?.stripe_customer_id }) }) const data = await response.json() @@ -54,10 +54,11 @@ export default function PricingSection() { setLoading(false) } } + console.log(subscriptionDetails, "subscriptionDetails") const handlePricingRoute = (plan_id: string, sub_type: string) => { if (user) { - handleStripeCheckout(plan_id, sub_type) + sub_type === "Starter" ? router.push("/dashboard") : sub_type === "Pro" ? handleStripeCheckout( plan_id,sub_type): router.push("/sales") } else { router.push("/login") } @@ -177,8 +178,8 @@ export default function PricingSection() { { planLoading || loadingBilling ? :