=> {
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/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 (
{
+ 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..34ec257 100644
--- a/apps/web/hooks/useSettings.ts
+++ b/apps/web/hooks/useSettings.ts
@@ -5,6 +5,12 @@ import { useState } from "react";
import { useSupabase } from "@/components/supabase-provider";
import { toast } from "sonner";
+interface SubscriptionDetails {
+ current_period_end: string;
+ plan_id: string;
+ stripe_customer_id: string;
+}
+
export function useSettings
() {
const { supabase, user } = useSupabase();
@@ -14,6 +20,7 @@ export function useSettings
const [isChangingPassword, setIsChangingPassword] = useState(false);
const [loadingNotifications, setLoadingNotifications] = useState(false);
const [loadingBilling, setLoadingBilling] = useState(false);
+ const [subscriptionDetails, setSubscriptionDetails] = useState(null)
// --- Profile update ---
const updateProfile = async ({
@@ -141,6 +148,25 @@ const [isChangingPassword, setIsChangingPassword] = useState(false);
}
};
+ const fetchSubscriptionDetails = async (userId:string): Promise => {
+ setLoadingBilling(true)
+ try {
+ const { data: existing } = await supabase
+ .from("subscriptions")
+ .select("*")
+ .eq("user_id", userId)
+ .single()
+
+ if(existing) {
+ setSubscriptionDetails(existing as SubscriptionDetails)
+ }
+ } catch (error) {
+
+ } finally {
+ setLoadingBilling(false)
+ }
+ }
+
// --- Password reset ---
const changePassword = async () => {
if (!user?.email) return;
@@ -174,5 +200,7 @@ const [isChangingPassword, setIsChangingPassword] = useState(false);
// Billing
updateBilling,
loadingBilling,
+ fetchSubscriptionDetails,
+ subscriptionDetails,
};
}
diff --git a/apps/web/lib/stripe.ts b/apps/web/lib/stripe.ts
new file mode 100644
index 0000000..6a68cf7
--- /dev/null
+++ b/apps/web/lib/stripe.ts
@@ -0,0 +1,5 @@
+import Stripe from "stripe";
+
+export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
+ apiVersion: "2025-09-30.clover",
+});
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/middleware.ts b/apps/web/middleware.ts
index 7715f7e..5b68baa 100644
--- a/apps/web/middleware.ts
+++ b/apps/web/middleware.ts
@@ -6,6 +6,7 @@ export async function middleware(request: NextRequest) {
const response = NextResponse.next()
if (request.nextUrl.pathname === "/api/auth/callback" ||
+ request.nextUrl.pathname === "/api/stripe/webhook" ||
request.nextUrl.pathname === "/api/track-referral"
) {
return response
diff --git a/apps/web/package.json b/apps/web/package.json
index 8748e4f..1f782e7 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -84,6 +84,7 @@
"resend": "^6.0.1",
"simplex-noise": "^4.0.3",
"sonner": "^1.7.1",
+ "stripe": "^19.1.0",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
"three": "^0.178.0",
@@ -98,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/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
new file mode 100644
index 0000000..bab1f96
--- /dev/null
+++ b/apps/web/types/subscription.ts
@@ -0,0 +1,13 @@
+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
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 76b2fb0..91a7734 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -348,6 +348,9 @@ importers:
sonner:
specifier: ^1.7.1
version: 1.7.4(react-dom@19.1.1)(react@19.1.1)
+ stripe:
+ specifier: ^19.1.0
+ version: 19.1.0(@types/node@22.17.0)
tailwind-merge:
specifier: ^2.6.0
version: 2.6.0
@@ -382,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
@@ -4418,7 +4424,6 @@ packages:
resolution: {integrity: sha512-bbAKTCqX5aNVryi7qXVMi+OkB3w/OyblodicMbvE38blyAz7GxXf6XYhklokijuPwwVg9sDLKRxt0ZHXQwZVfQ==}
dependencies:
undici-types: 6.21.0
- dev: true
/@types/phoenix@1.6.6:
resolution: {integrity: sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==}
@@ -4502,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:
@@ -9037,7 +9051,6 @@ packages:
engines: {node: '>=0.6'}
dependencies:
side-channel: 1.1.0
- dev: true
/queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
@@ -9862,6 +9875,18 @@ 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
+
/strtok3@10.3.4:
resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==}
engines: {node: '>=18'}