diff --git a/apps/desktop/src-tauri/src/general_settings.rs b/apps/desktop/src-tauri/src/general_settings.rs index 0f941d71..47de3574 100644 --- a/apps/desktop/src-tauri/src/general_settings.rs +++ b/apps/desktop/src-tauri/src/general_settings.rs @@ -20,7 +20,8 @@ pub struct GeneralSettingsStore { pub enable_notifications: bool, #[serde(default)] pub disable_auto_open_links: bool, - #[serde(default)] + // first launch: store won't exist so show startup + #[serde(default = "true_b")] pub has_completed_startup: bool, } diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 5adf5361..6f680f28 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -2521,8 +2521,14 @@ pub async fn run() { let permissions = permissions::do_permissions_check(false); println!("Permissions check result: {:?}", permissions); - if !permissions.screen_recording.permitted() || !permissions.accessibility.permitted() { - println!("Required permissions not granted, showing permissions window"); + if !permissions.screen_recording.permitted() + || !permissions.accessibility.permitted() + || GeneralSettingsStore::get(app.handle()) + .ok() + .flatten() + .map(|s| !s.has_completed_startup) + .unwrap_or(false) + { CapWindow::Setup.show(&app_handle).ok(); } else { println!("Permissions granted, showing main window"); diff --git a/apps/desktop/src/routes/(window-chrome).tsx b/apps/desktop/src/routes/(window-chrome).tsx index fd700f5a..626b481e 100644 --- a/apps/desktop/src/routes/(window-chrome).tsx +++ b/apps/desktop/src/routes/(window-chrome).tsx @@ -46,7 +46,10 @@ export default function (props: RouteSectionProps) { exitToClass="opacity-0" > */} }> - {props.children} + + {/* prevents flicker idk */} + {props.children} + {/* */} diff --git a/apps/desktop/src/routes/(window-chrome)/(main).tsx b/apps/desktop/src/routes/(window-chrome)/(main).tsx index ed128d0c..1775e776 100644 --- a/apps/desktop/src/routes/(window-chrome)/(main).tsx +++ b/apps/desktop/src/routes/(window-chrome)/(main).tsx @@ -48,12 +48,12 @@ import { const getAuth = cache(async () => { const value = await authStore.get(); - if (!value && !import.meta.env.TAURI_ENV_DEBUG) return redirect("/signin"); + const local = import.meta.env.VITE_LOCAL_MODE === "true"; + if (!value && !local) return redirect("/signin"); const res = await fetch(`${clientEnv.VITE_SERVER_URL}/api/desktop/plan`, { headers: { authorization: `Bearer ${value?.token}` }, }); - if (res.status !== 200 && !import.meta.env.TAURI_ENV_DEBUG) - return redirect("/signin"); + if (res.status !== 200 && !local) return redirect("/signin"); return value; }, "getAuth"); diff --git a/apps/desktop/src/routes/editor/Timeline.tsx b/apps/desktop/src/routes/editor/Timeline.tsx index a3a13557..2bddee74 100644 --- a/apps/desktop/src/routes/editor/Timeline.tsx +++ b/apps/desktop/src/routes/editor/Timeline.tsx @@ -234,7 +234,7 @@ export function Timeline() { }); }} /> - + {formatTime(segment.start)} diff --git a/apps/web/app/api/settings/billing/subscribe/route.ts b/apps/web/app/api/settings/billing/subscribe/route.ts index 52da04f9..bc56d8d4 100644 --- a/apps/web/app/api/settings/billing/subscribe/route.ts +++ b/apps/web/app/api/settings/billing/subscribe/route.ts @@ -6,90 +6,89 @@ import { db } from "@cap/database"; import { users } from "@cap/database/schema"; export async function POST(request: NextRequest) { + console.log("Starting subscription process"); const user = await getCurrentUser(); let customerId = user?.stripeCustomerId; const { priceId } = await request.json(); + console.log("Received request with priceId:", priceId); + console.log("Current user:", user?.id); + if (!priceId) { console.error("Price ID not found"); - return new Response(JSON.stringify({ error: true }), { status: 400, - headers: { - "Content-Type": "application/json", - }, + headers: { "Content-Type": "application/json" }, }); } if (!user) { console.error("User not found"); - return new Response(JSON.stringify({ error: true, auth: false }), { status: 401, - headers: { - "Content-Type": "application/json", - }, + headers: { "Content-Type": "application/json" }, }); } - - if ( - isUserOnProPlan({ - subscriptionStatus: user.stripeSubscriptionStatus as string, - }) - ) { + + if (isUserOnProPlan({ subscriptionStatus: user.stripeSubscriptionStatus as string })) { + console.error("User already has pro plan"); return new Response(JSON.stringify({ error: true, subscription: true }), { status: 400, - headers: { - "Content-Type": "application/json", - }, + headers: { "Content-Type": "application/json" }, }); } - if (!user.stripeCustomerId) { - const customer = await stripe.customers.create({ - email: user.email, - metadata: { - userId: user.id, - }, - }); + try { + if (!user.stripeCustomerId) { + console.log("Creating new Stripe customer for user:", user.id); + const customer = await stripe.customers.create({ + email: user.email, + metadata: { + userId: user.id, + }, + }); - await db - .update(users) - .set({ - stripeCustomerId: customer.id, - }) - .where(eq(users.id, user.id)); + console.log("Created Stripe customer:", customer.id); - customerId = customer.id; - } + await db + .update(users) + .set({ + stripeCustomerId: customer.id, + }) + .where(eq(users.id, user.id)); - const checkoutSession = await stripe.checkout.sessions.create({ - customer: customerId as string, - line_items: [ - { - price: priceId, - quantity: 1, - }, - ], - mode: "subscription", - success_url: `${process.env.NEXT_PUBLIC_URL}/dashboard/caps?upgrade=true`, - cancel_url: `${process.env.NEXT_PUBLIC_URL}/pricing`, - allow_promotion_codes: true, - }); + console.log("Updated user with Stripe customer ID"); + customerId = customer.id; + } - if (checkoutSession.url) { - return new Response(JSON.stringify({ url: checkoutSession.url }), { - status: 200, - headers: { - "Content-Type": "application/json", - }, + console.log("Creating checkout session for customer:", customerId); + const checkoutSession = await stripe.checkout.sessions.create({ + customer: customerId as string, + line_items: [{ price: priceId, quantity: 1 }], + mode: "subscription", + success_url: `${process.env.NEXT_PUBLIC_URL}/dashboard/caps?upgrade=true`, + cancel_url: `${process.env.NEXT_PUBLIC_URL}/pricing`, + allow_promotion_codes: true, }); - } - return new Response(JSON.stringify({ error: true }), { - status: 400, - headers: { - "Content-Type": "application/json", - }, - }); + if (checkoutSession.url) { + console.log("Successfully created checkout session"); + return new Response(JSON.stringify({ url: checkoutSession.url }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } + + console.error("Checkout session created but no URL returned"); + return new Response(JSON.stringify({ error: true }), { + status: 400, + headers: { "Content-Type": "application/json" }, + }); + } catch (error) { + console.error("Error creating checkout session:", error); + return new Response(JSON.stringify({ error: true }), { + status: 500, + headers: { "Content-Type": "application/json" }, + }); + } } diff --git a/apps/web/app/api/webhooks/stripe/route.ts b/apps/web/app/api/webhooks/stripe/route.ts index 3b497c4f..33403d7d 100644 --- a/apps/web/app/api/webhooks/stripe/route.ts +++ b/apps/web/app/api/webhooks/stripe/route.ts @@ -11,173 +11,255 @@ const relevantEvents = new Set([ "customer.subscription.deleted", ]); +// Helper function to find user with retries +async function findUserWithRetry(email: string, userId?: string, maxRetries = 5): Promise { + for (let i = 0; i < maxRetries; i++) { + console.log(`[Attempt ${i + 1}/${maxRetries}] Looking for user:`, { email, userId }); + + try { + // Try finding by userId first if available + if (userId) { + console.log(`Attempting to find user by ID: ${userId}`); + const userById = await db + .select() + .from(users) + .where(eq(users.id, userId)) + .limit(1) + .then(rows => rows[0] ?? null); + + if (userById) { + console.log(`Found user by ID: ${userId}`); + return userById; + } + console.log(`No user found by ID: ${userId}`); + } + + // If not found by ID or no ID provided, try email + if (email) { + console.log(`Attempting to find user by email: ${email}`); + const userByEmail = await db + .select() + .from(users) + .where(eq(users.email, email)) + .limit(1) + .then(rows => rows[0] ?? null); + + if (userByEmail) { + console.log(`Found user by email: ${email}`); + return userByEmail; + } + console.log(`No user found by email: ${email}`); + } + + // If we reach here, no user was found on this attempt + if (i < maxRetries - 1) { + const delay = Math.pow(2, i) * 3000; // 3s, 6s, 12s, 24s, 48s + console.log(`No user found on attempt ${i + 1}. Waiting ${delay}ms before retry...`); + await new Promise(resolve => setTimeout(resolve, delay)); + } + } catch (error) { + console.error(`Error during attempt ${i + 1}:`, error); + // If this is not the last attempt, continue to next retry + if (i < maxRetries - 1) { + const delay = Math.pow(2, i) * 3000; + await new Promise(resolve => setTimeout(resolve, delay)); + continue; + } + } + } + + console.log('All attempts exhausted. No user found.'); + return null; +} + export const POST = async (req: Request) => { + console.log('Webhook received'); const buf = await req.text(); const sig = req.headers.get("Stripe-Signature") as string; const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET; let event: Stripe.Event; + try { - if (!sig || !webhookSecret) return; + if (!sig || !webhookSecret) { + console.log("❌ Missing webhook secret or signature"); + return new Response("Missing webhook secret or signature", { status: 400 }); + } event = stripe.webhooks.constructEvent(buf, sig, webhookSecret); console.log(`✅ Event received: ${event.type}`); } catch (err: any) { console.log(`❌ Error message: ${err.message}`); - return new Response(`Webhook Error: ${err.message}`, { - status: 400, - }); + return new Response(`Webhook Error: ${err.message}`, { status: 400 }); } + if (relevantEvents.has(event.type)) { try { if (event.type === "checkout.session.completed") { - console.log("Processing checkout.session.completed event"); + console.log('Processing checkout.session.completed event'); + const session = event.data.object as Stripe.Checkout.Session; + console.log('Session data:', { + id: session.id, + customerId: session.customer, + subscriptionId: session.subscription + }); + const customer = await stripe.customers.retrieve( - event.data.object.customer as string + session.customer as string ); + console.log('Retrieved customer:', { + id: customer.id, + email: 'email' in customer ? customer.email : undefined, + metadata: 'metadata' in customer ? customer.metadata : undefined + }); + let foundUserId; + let customerEmail; + if ("metadata" in customer) { foundUserId = customer.metadata.userId; } - if (!foundUserId) { - console.log("No user found in metadata, checking customer email"); - if ("email" in customer && customer.email) { - const userByEmail = await db - .select() - .from(users) - .where(eq(users.email, customer.email)) - .limit(1); - - if (userByEmail && userByEmail.length > 0 && userByEmail[0]) { - foundUserId = userByEmail[0].id; - console.log(`User found by email: ${foundUserId}`); - // Update customer metadata with userId - await stripe.customers.update(customer.id, { - metadata: { userId: foundUserId }, - }); - } else { - console.log("No user found by email"); - return new Response("No user found", { - status: 400, - }); - } - } else { - console.log("No email found for customer"); - return new Response("No user found", { - status: 400, - }); - } + if ("email" in customer) { + customerEmail = customer.email; } - const user = await db - .select() - .from(users) - .where(eq(users.id, foundUserId)); - - if (!user) { - console.log( - "No user found in database for checkout.session.completed event" - ); - return new Response("No user found", { - status: 400, + console.log("Starting user lookup with:", { foundUserId, customerEmail }); + + // Try to find user with retries + const dbUser = await findUserWithRetry(customerEmail as string, foundUserId); + + if (!dbUser) { + console.log("No user found after all retries. Returning 202 to allow retry."); + return new Response("User not found, webhook will be retried", { + status: 202, }); } + console.log("Successfully found user:", { + userId: dbUser.id, + email: dbUser.email, + name: dbUser.name + }); + const subscription = await stripe.subscriptions.retrieve( - event.data.object.subscription as string + session.subscription as string ); + console.log('Retrieved subscription:', { + id: subscription.id, + status: subscription.status + }); + const inviteQuota = subscription.items.data.reduce( (total, item) => total + (item.quantity || 1), 0 ); + console.log('Updating user in database with:', { + subscriptionId: session.subscription, + status: subscription.status, + customerId: customer.id, + inviteQuota + }); + await db .update(users) .set({ - stripeSubscriptionId: event.data.object.subscription as string, - stripeSubscriptionStatus: event.data.object.status, + stripeSubscriptionId: session.subscription as string, + stripeSubscriptionStatus: subscription.status, + stripeCustomerId: customer.id, inviteQuota: inviteQuota, }) - .where(eq(users.id, foundUserId)); - console.log( - "User updated successfully for checkout.session.completed event" - ); + .where(eq(users.id, dbUser.id)); + + console.log("Successfully updated user in database"); } if (event.type === "customer.subscription.updated") { - console.log("Processing customer.subscription.updated event"); + console.log('Processing customer.subscription.updated event'); + const subscription = event.data.object as Stripe.Subscription; + console.log('Subscription data:', { + id: subscription.id, + status: subscription.status, + customerId: subscription.customer + }); + const customer = await stripe.customers.retrieve( - event.data.object.customer as string + subscription.customer as string ); + console.log('Retrieved customer:', { + id: customer.id, + email: 'email' in customer ? customer.email : undefined, + metadata: 'metadata' in customer ? customer.metadata : undefined + }); + let foundUserId; + let customerEmail; + if ("metadata" in customer) { foundUserId = customer.metadata.userId; } - if (!foundUserId) { - console.log("No user found in metadata, checking customer email"); - if ("email" in customer && customer.email) { - const userByEmail = await db - .select() - .from(users) - .where(eq(users.email, customer.email)) - .limit(1); + if ("email" in customer) { + customerEmail = customer.email; + } - if (userByEmail && userByEmail.length > 0 && userByEmail[0]) { - foundUserId = userByEmail[0].id; - console.log(`User found by email: ${foundUserId}`); - // Update customer metadata with userId - await stripe.customers.update(customer.id, { - metadata: { userId: foundUserId }, - }); - } else { - console.log("No user found by email"); - return new Response("No user found", { - status: 400, - }); - } - } else { - console.log("No email found for customer"); - return new Response("No user found", { - status: 400, - }); - } + console.log("Starting user lookup with:", { foundUserId, customerEmail }); + + // Try to find user with retries + const dbUser = await findUserWithRetry(customerEmail as string, foundUserId); + + if (!dbUser) { + console.log("No user found after all retries. Returning 202 to allow retry."); + return new Response("User not found, webhook will be retried", { + status: 202, + }); } - const user = await db - .select() - .from(users) - .where(eq(users.id, foundUserId)); + console.log("Successfully found user:", { + userId: dbUser.id, + email: dbUser.email, + name: dbUser.name + }); + + // Get all active subscriptions for this customer + const subscriptions = await stripe.subscriptions.list({ + customer: customer.id, + status: 'active', + }); - if (!user) { - console.log( - "No user found in database for customer.subscription.updated event" + console.log('Retrieved all active subscriptions:', { + count: subscriptions.data.length + }); + + // Calculate total invite quota based on all active subscriptions + const inviteQuota = subscriptions.data.reduce((total, sub) => { + return total + sub.items.data.reduce( + (subTotal, item) => subTotal + (item.quantity || 1), + 0 ); - return new Response("No user found", { - status: 400, - }); - } + }, 0); - const subscription = event.data.object as Stripe.Subscription; - const inviteQuota = subscription.items.data.reduce( - (total, item) => total + (item.quantity || 1), - 0 - ); + console.log('Updating user in database with:', { + subscriptionId: subscription.id, + status: subscription.status, + customerId: customer.id, + inviteQuota + }); await db .update(users) .set({ - stripeSubscriptionId: event.data.object.id, - stripeSubscriptionStatus: event.data.object.status, + stripeSubscriptionId: subscription.id, + stripeSubscriptionStatus: subscription.status, + stripeCustomerId: customer.id, inviteQuota: inviteQuota, }) - .where(eq(users.id, foundUserId)); - console.log( - "User updated successfully for customer.subscription.updated event" - ); + .where(eq(users.id, dbUser.id)); + + console.log("Successfully updated user in database with new invite quota:", inviteQuota); } if (event.type === "customer.subscription.deleted") { - console.log("Processing customer.subscription.deleted event"); + const subscription = event.data.object as Stripe.Subscription; const customer = await stripe.customers.retrieve( - event.data.object.customer as string + subscription.customer as string ); let foundUserId; if ("metadata" in customer) { @@ -213,48 +295,38 @@ export const POST = async (req: Request) => { } } - const user = await db + const userResult = await db .select() .from(users) .where(eq(users.id, foundUserId)); - if (!user) { - console.log( - "No user found in database for customer.subscription.deleted event" - ); - return new Response("No user found", { - status: 400, - }); + if (!userResult || userResult.length === 0) { + console.log("No user found in database"); + return new Response("No user found", { status: 400 }); } await db .update(users) .set({ - stripeSubscriptionId: event.data.object.id, - stripeSubscriptionStatus: event.data.object.status, - inviteQuota: 1, // Reset to default quota when subscription is deleted + stripeSubscriptionId: subscription.id, + stripeSubscriptionStatus: subscription.status, + inviteQuota: 1, // Reset to default quota }) .where(eq(users.id, foundUserId)); - console.log( - "User updated successfully for customer.subscription.deleted event" - ); + + console.log("User updated successfully", { foundUserId, inviteQuota: 1 }); } + + return NextResponse.json({ received: true }); } catch (error) { - console.log("❌ Webhook handler failed. View logs."); + console.error("❌ Webhook handler failed:", error); return new Response( 'Webhook error: "Webhook handler failed. View logs."', - { - status: 400, - } + { status: 400 } ); } - } else { - console.log(`Unrecognised event: ${event.type}`); - return new Response(`Unrecognised event: ${event.type}`, { - status: 400, - }); } - console.log("✅ Webhook processed successfully"); - return NextResponse.json({ received: true }); + console.log(`Unrecognised event: ${event.type}`); + return new Response(`Unrecognised event: ${event.type}`, { status: 400 }); }; diff --git a/apps/web/app/dashboard/caps/Caps.tsx b/apps/web/app/dashboard/caps/Caps.tsx index f4605f63..5c482ce3 100644 --- a/apps/web/app/dashboard/caps/Caps.tsx +++ b/apps/web/app/dashboard/caps/Caps.tsx @@ -35,10 +35,6 @@ export const Caps = ({ const limit = 15; const totalPages = Math.ceil(count / limit); - if (!activeSpace && (user?.name === undefined || user?.name === "")) { - replace("/onboarding"); - } - useEffect(() => { const fetchAnalytics = async () => { const analyticsData: Record = {}; diff --git a/apps/web/app/dashboard/caps/page.tsx b/apps/web/app/dashboard/caps/page.tsx index 79851250..272bec77 100644 --- a/apps/web/app/dashboard/caps/page.tsx +++ b/apps/web/app/dashboard/caps/page.tsx @@ -11,6 +11,7 @@ import { import { desc, eq, sql, count, or } from "drizzle-orm"; import { getCurrentUser } from "@cap/database/auth/session"; import { Metadata } from "next"; +import { redirect } from "next/navigation"; export const metadata: Metadata = { title: "My Caps — Cap", @@ -23,10 +24,19 @@ export default async function CapsPage({ }: { searchParams: { [key: string]: string | string[] | undefined }; }) { + const user = await getCurrentUser(); + + if (!user || !user.id) { + redirect("/login"); + } + + if (!user.name || user.name.length <= 1) { + redirect("/onboarding"); + } + + const userId = user.id; const page = Number(searchParams.page) || 1; const limit = Number(searchParams.limit) || 15; - const user = await getCurrentUser(); - const userId = user?.id as string; const offset = (page - 1) * limit; const totalCountResult = await db @@ -86,6 +96,7 @@ export default async function CapsPage({ const processedVideoData = videoData.map((video) => ({ ...video, sharedSpaces: video.sharedSpaces.filter((space) => space.id !== null), + ownerName: video.ownerName ?? "", })); return ( diff --git a/apps/web/app/dashboard/layout.tsx b/apps/web/app/dashboard/layout.tsx index 942776f5..ccd898d1 100644 --- a/apps/web/app/dashboard/layout.tsx +++ b/apps/web/app/dashboard/layout.tsx @@ -32,6 +32,10 @@ export default async function DashboardLayout({ redirect("/login"); } + if (!user.name || user.name.length <= 1) { + redirect("/onboarding"); + } + const spacesWithMembers = await db .select({ space: spaces, diff --git a/apps/web/app/login/form.tsx b/apps/web/app/login/form.tsx index 886e97b6..40023264 100644 --- a/apps/web/app/login/form.tsx +++ b/apps/web/app/login/form.tsx @@ -18,6 +18,30 @@ export function LoginForm() { error && toast.error(error); }, [searchParams]); + useEffect(() => { + const pendingPriceId = localStorage.getItem("pendingPriceId"); + if (emailSent && pendingPriceId) { + // Clear the pending price ID + localStorage.removeItem("pendingPriceId"); + + // Wait a bit to ensure the user is created + setTimeout(async () => { + const response = await fetch(`/api/settings/billing/subscribe`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ priceId: pendingPriceId }), + }); + const data = await response.json(); + + if (data.url) { + window.location.href = data.url; + } + }, 2000); // Wait 2 seconds after email is sent + } + }, [emailSent]); + return ( <>
{ const data = await response.json(); if (data.auth === false) { - push(`/login?next=/pricing?plan=${planId}`); + localStorage.setItem("pendingPriceId", planId); + push(`/login?next=/pricing`); + return; } if (data.subscription === true) { diff --git a/turbo.json b/turbo.json index 0e581d33..0790655d 100644 --- a/turbo.json +++ b/turbo.json @@ -4,7 +4,7 @@ "globalDotEnv": [".env"], "pipeline": { "build": { - "inputs": ["**/*.ts", "!src-tauri/**", "!node_modules/**"], + "inputs": ["**/*.ts", "**/*.tsx", "!src-tauri/**", "!node_modules/**"], "dependsOn": ["^build"], "outputs": [".next/**", "!.next/cache/**"], "dotEnv": [