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 (
<>