From eca13359292cdcdf472258d8c5f169fd9b746bdd Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 10:24:53 +0000 Subject: [PATCH 1/2] feat: add Convex authentication option to sidebar - Add new AuthSidebarPanel component with two states: - When cloud is disabled: shows a message with a button to navigate to the Cloud panel - When cloud is enabled: shows setup checklist and a "Setup Authentication" button that triggers the AI to configure Convex Auth with email/password - Add "Authentication" menu item (KeyRound icon) in the sidebar below "Cloud" on both desktop and mobile - Update CloudSidebarPanel to show an "Authentication" shortcut section when cloud is enabled - Update mobile-burger-menu to include Authentication in navigation items - Wire up handleSetupAuth in page.tsx that sends a detailed Convex Auth setup prompt to the AI via append() - Add mobile sheet for the auth panel - Covers programmatic setup: @convex-dev/auth installation, convex/auth.ts, schema.ts, http.ts, ConvexAuthProvider, sign-in/sign-up screens Co-authored-by: Rodrigo Figueroa --- apps/web/app/(app)/p/[id]/page.tsx | 37 ++++++ apps/web/components/app-sidebar.tsx | 30 ++++- apps/web/components/auth-sidebar-panel.tsx | 139 ++++++++++++++++++++ apps/web/components/cloud-sidebar-panel.tsx | 23 +++- apps/web/components/mobile-burger-menu.tsx | 2 + 5 files changed, 228 insertions(+), 3 deletions(-) create mode 100644 apps/web/components/auth-sidebar-panel.tsx diff --git a/apps/web/app/(app)/p/[id]/page.tsx b/apps/web/app/(app)/p/[id]/page.tsx index f52dbc62..7894d2f4 100644 --- a/apps/web/app/(app)/p/[id]/page.tsx +++ b/apps/web/app/(app)/p/[id]/page.tsx @@ -18,6 +18,7 @@ import { AssetsPanel } from '@/components/assets-panel' import { ProjectsPanel } from '@/components/projects-panel' import { BackendPanel } from '@/components/backend-panel' import { CloudSidebarPanel } from '@/components/cloud-sidebar-panel' +import { AuthSidebarPanel } from '@/components/auth-sidebar-panel' import { Sheet, SheetContent, SheetTitle } from '@/components/ui/sheet' import * as VisuallyHidden from "@radix-ui/react-visually-hidden" // ResizableHandle and ResizablePanel/Group removed - using fixed 500px width for chat panel @@ -595,6 +596,25 @@ function ProjectPageInternal() { fetchCloudStatus() }, [fetchCloudStatus]) + const handleSetupAuth = useCallback(() => { + setDesktopSidebarPanel(null) + setMobileSidebarPanel(null) + append({ + role: 'user', + content: `Please set up Convex authentication with email/password for my Expo React Native app. Follow these steps: + +1. Install the required packages: @convex-dev/auth and @auth/core +2. Create convex/auth.ts with the Password provider from @convex-dev/auth/providers/Password +3. Update convex/schema.ts to spread authTables from @convex-dev/auth/server inside defineSchema +4. Create or update convex/http.ts to add authentication HTTP routes using auth.addHttpRoutes(http) +5. Run npx @convex-dev/auth to generate JWT private key and JWKS, then configure SITE_URL as an environment variable +6. Update the root app layout (_layout.tsx) to wrap with ConvexAuthProvider from @convex-dev/auth/react-native, using expo-secure-store as the storage option +7. Create a sign-in screen and sign-up screen with email and password input fields using the useAuthActions hook + +Important: Use @convex-dev/auth/react-native (not /react) for Expo compatibility and expo-secure-store for secure token persistence.`, + }) + }, [append, setDesktopSidebarPanel, setMobileSidebarPanel]) + // Legacy compatibility const isDesktopMode = viewMode === 'desktop' @@ -2428,6 +2448,7 @@ function ProjectPageInternal() { cloudEnabled={cloudEnabled} cloudDeploymentUrl={cloudDeploymentUrl} onCloudEnabled={handleCloudEnabled} + onSetupAuth={handleSetupAuth} >
{/* Fixed width Chat/History panel - 500px */} @@ -2610,6 +2631,22 @@ function ProjectPageInternal() { cloudEnabled={cloudEnabled} deploymentUrl={cloudDeploymentUrl} onCloudEnabled={handleCloudEnabled} + onNavigateToAuth={() => setMobileSidebarPanel('auth')} + onClose={() => setMobileSidebarPanel(null)} + /> + + + + !open && setMobileSidebarPanel(null)}> + + + Authentication + + setMobileSidebarPanel('cloud')} + onSetupAuth={handleSetupAuth} onClose={() => setMobileSidebarPanel(null)} /> diff --git a/apps/web/components/app-sidebar.tsx b/apps/web/components/app-sidebar.tsx index 2ebd8d03..13ee0e5a 100644 --- a/apps/web/components/app-sidebar.tsx +++ b/apps/web/components/app-sidebar.tsx @@ -1,7 +1,7 @@ "use client" import { useState, useEffect, useRef } from "react" -import { Image, FolderOpen, Database, ChevronLeft, ChevronRight, X, MessageSquare, Cloud } from "lucide-react" +import { Image, FolderOpen, Database, ChevronLeft, ChevronRight, X, MessageSquare, Cloud, KeyRound } from "lucide-react" import { Sidebar, SidebarContent, @@ -19,6 +19,7 @@ import { AssetsPanel } from "@/components/assets-panel" import { ProjectsPanel } from "@/components/projects-panel" import { BackendPanel } from "@/components/backend-panel" import { CloudSidebarPanel } from "@/components/cloud-sidebar-panel" +import { AuthSidebarPanel } from "@/components/auth-sidebar-panel" import { UserMenu } from "@/components/user-menu" import { Session } from "@/lib/auth" import { cn } from "@/lib/utils" @@ -39,6 +40,7 @@ interface AppSidebarProps { cloudEnabled?: boolean cloudDeploymentUrl?: string onCloudEnabled?: () => void + onSetupAuth?: () => void } function SidebarToggle() { @@ -93,6 +95,12 @@ function SidebarNav({ icon: Cloud, spacer: false, }, + { + id: "auth", + label: "Authentication", + icon: KeyRound, + spacer: false, + }, { id: "backend", label: "Backend", @@ -182,7 +190,7 @@ function PanelContent({ children, onClose, }: { - isBottomOption: boolean + isBottomOption?: boolean isOpen: boolean isFirstOpen: boolean isSwitching: boolean @@ -296,6 +304,7 @@ export function AppSidebar({ cloudEnabled, cloudDeploymentUrl, onCloudEnabled, + onSetupAuth, }: AppSidebarProps) { const [internalActivePanel, setInternalActivePanel] = useState(null) const [isFirstOpen, setIsFirstOpen] = useState(true) @@ -396,6 +405,23 @@ export function AppSidebar({ cloudEnabled={cloudEnabled || false} deploymentUrl={cloudDeploymentUrl} onCloudEnabled={onCloudEnabled} + onNavigateToAuth={() => handlePanelChange('auth')} + onClose={() => handlePanelChange(null)} + /> + + + handlePanelChange(null)} + isBottomOption + > + handlePanelChange('cloud')} + onSetupAuth={onSetupAuth || (() => {})} onClose={() => handlePanelChange(null)} /> diff --git a/apps/web/components/auth-sidebar-panel.tsx b/apps/web/components/auth-sidebar-panel.tsx new file mode 100644 index 00000000..6326bf87 --- /dev/null +++ b/apps/web/components/auth-sidebar-panel.tsx @@ -0,0 +1,139 @@ +'use client' + +import { useState } from 'react' +import { Button } from '@/components/ui/button' +import { + KeyRound, + Cloud, + CheckCircle2, + Loader2, + ShieldCheck, + Package, + Database, + Globe, + Smartphone, +} from 'lucide-react' + +interface AuthSidebarPanelProps { + projectId?: string + cloudEnabled: boolean + onNavigateToCloud: () => void + onSetupAuth: () => void + onClose: () => void +} + +const SETUP_STEPS = [ + { icon: Package, text: '@convex-dev/auth package installation' }, + { icon: ShieldCheck, text: 'Password provider configuration' }, + { icon: Database, text: 'Auth tables in your database schema' }, + { icon: Globe, text: 'HTTP routes for authentication' }, + { icon: Smartphone, text: 'ConvexAuthProvider in your app layout' }, +] + +export function AuthSidebarPanel({ + projectId, + cloudEnabled, + onNavigateToCloud, + onSetupAuth, + onClose, +}: AuthSidebarPanelProps) { + const [isSettingUp, setIsSettingUp] = useState(false) + + const handleSetupAuth = () => { + setIsSettingUp(true) + onSetupAuth() + setTimeout(() => setIsSettingUp(false), 3000) + } + + return ( +
+ {/* Header */} +
+

+ + Authentication +

+
+ + {/* Content */} +
+
+

+ Add user authentication to your app +

+ + {!cloudEnabled ? ( +
+
+ +
+
+ Cloud Required +
+
+ Authentication via Convex requires Cloud to be enabled for your project. +
+
+
+ +

+ Enable cloud to add Convex Auth to your app. Convex provides secure, + production-ready authentication with built-in support for email and password. +

+ + +
+ ) : ( +
+

+ Set up email and password authentication for your Expo app using Convex Auth. +

+ +
+

+ What will be set up +

+
+ {SETUP_STEPS.map(({ icon: Icon, text }) => ( +
+ + {text} +
+ ))} +
+
+ +
+

+ The AI will configure Convex Auth with email/password authentication + and create sign-in and sign-up screens for your app. +

+
+ + +
+ )} +
+
+
+ ) +} diff --git a/apps/web/components/cloud-sidebar-panel.tsx b/apps/web/components/cloud-sidebar-panel.tsx index 623b72c4..1b5a468d 100644 --- a/apps/web/components/cloud-sidebar-panel.tsx +++ b/apps/web/components/cloud-sidebar-panel.tsx @@ -12,7 +12,7 @@ import { AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog' -import { Database, Zap, Cloud, HardDrive, CheckCircle2, Loader2, ExternalLink } from 'lucide-react' +import { Database, Zap, Cloud, HardDrive, CheckCircle2, Loader2, ExternalLink, KeyRound } from 'lucide-react' import { toast } from 'sonner' interface CloudSidebarPanelProps { @@ -20,6 +20,7 @@ interface CloudSidebarPanelProps { cloudEnabled: boolean deploymentUrl?: string onCloudEnabled?: () => void + onNavigateToAuth?: () => void onClose: () => void } @@ -28,6 +29,7 @@ export function CloudSidebarPanel({ cloudEnabled, deploymentUrl, onCloudEnabled, + onNavigateToAuth, onClose, }: CloudSidebarPanelProps) { const [isEnabling, setIsEnabling] = useState(false) @@ -116,6 +118,25 @@ export function CloudSidebarPanel({

Your app now has access to a real-time database. The AI will use Convex for all data persistence and backend logic.

+ +
+
+ + Authentication +
+

+ Add email/password authentication to your app with Convex Auth. +

+ +
) : (
diff --git a/apps/web/components/mobile-burger-menu.tsx b/apps/web/components/mobile-burger-menu.tsx index d9d5c5c0..3070fd74 100644 --- a/apps/web/components/mobile-burger-menu.tsx +++ b/apps/web/components/mobile-burger-menu.tsx @@ -16,6 +16,7 @@ import { FolderOpen, Database, Cloud, + KeyRound, Download, Rocket, ExternalLink, @@ -136,6 +137,7 @@ export function MobileBurgerMenu({ { id: null, label: "Chat", icon: MessageSquare }, { id: "assets", label: "Assets", icon: Image }, { id: "cloud", label: "Cloud", icon: Cloud }, + { id: "auth", label: "Authentication", icon: KeyRound }, { id: "projects", label: "Projects", icon: FolderOpen }, { id: "backend", label: "Backend", icon: Database }, ] From fa8ba30a965f6d5697be05459e8053621c3bc831 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 10:54:04 +0000 Subject: [PATCH 2/2] fix: replace append() with direct API call for Convex Auth setup Replace the chat message approach with a dedicated API route that writes Convex Auth files directly to the sandbox and commits via git: - Add /api/setup-convex-auth route that: - Installs @convex-dev/auth, @auth/core, expo-secure-store packages - Writes convex/auth.ts with Password provider - Writes convex/auth.config.ts with CONVEX_SITE_URL configuration - Smart-injects authTables into convex/schema.ts - Smart-injects auth HTTP routes into convex/http.ts - Updates app/_layout.tsx to use ConvexAuthProvider (replaces ConvexWrapper) - Creates app/(auth)/_layout.tsx, sign-in.tsx, and sign-up.tsx screens - Commits all changes via git - Update handleSetupAuth in page.tsx to call the new API - Update AuthSidebarPanel to handle async setup with loading/success/error states - Update AppSidebar to use () => Promise type for onSetupAuth Co-authored-by: Rodrigo Figueroa --- .../app/(app)/api/setup-convex-auth/route.ts | 564 ++++++++++++++++++ apps/web/app/(app)/p/[id]/page.tsx | 28 +- apps/web/components/app-sidebar.tsx | 4 +- apps/web/components/auth-sidebar-panel.tsx | 88 ++- 4 files changed, 650 insertions(+), 34 deletions(-) create mode 100644 apps/web/app/(app)/api/setup-convex-auth/route.ts diff --git a/apps/web/app/(app)/api/setup-convex-auth/route.ts b/apps/web/app/(app)/api/setup-convex-auth/route.ts new file mode 100644 index 00000000..bb6c0fcb --- /dev/null +++ b/apps/web/app/(app)/api/setup-convex-auth/route.ts @@ -0,0 +1,564 @@ +// API endpoint to set up Convex Auth with email/password for a project +// Writes auth files directly to the sandbox and commits via git integration + +import { NextRequest, NextResponse } from 'next/server' +import { db } from '@/lib/db' +import { projects } from '@react-native-vibe-code/database' +import { eq, and } from 'drizzle-orm' +import { auth } from '@/lib/auth/config' +import { headers } from 'next/headers' +import { Sandbox } from '@e2b/code-interpreter' + +export const maxDuration = 300 // 5 minutes for package installation + +// convex/auth.ts - Password provider configuration +const CONVEX_AUTH_TS = `import { convexAuth } from "@convex-dev/auth/server" +import { Password } from "@convex-dev/auth/providers/Password" + +export const { auth, signIn, signOut, store } = convexAuth({ + providers: [Password], +}) +` + +// convex/auth.config.ts - JWT configuration pointing to Convex site URL +const CONVEX_AUTH_CONFIG_TS = `export default { + providers: [ + { + domain: process.env.CONVEX_SITE_URL, + applicationID: "convex", + }, + ], +} +` + +// app/(auth)/_layout.tsx +const AUTH_LAYOUT_TSX = `import { Stack } from "expo-router" + +export default function AuthLayout() { + return +} +` + +// app/(auth)/sign-in.tsx +const SIGN_IN_TSX = `import { useState } from "react" +import { View, TextInput, StyleSheet, Text, Pressable, Alert, KeyboardAvoidingView, Platform } from "react-native" +import { useAuthActions } from "@convex-dev/auth/react" +import { useRouter } from "expo-router" + +export default function SignInScreen() { + const { signIn } = useAuthActions() + const router = useRouter() + const [email, setEmail] = useState("") + const [password, setPassword] = useState("") + const [isLoading, setIsLoading] = useState(false) + + const handleSignIn = async () => { + if (!email || !password) { + Alert.alert("Error", "Please enter your email and password") + return + } + setIsLoading(true) + try { + await signIn("password", { email, password, flow: "signIn" }) + router.replace("/") + } catch (error) { + Alert.alert("Sign In Failed", error instanceof Error ? error.message : "An error occurred") + } finally { + setIsLoading(false) + } + } + + return ( + + + Sign In + + + + {isLoading ? "Signing in..." : "Sign In"} + + router.push("/(auth)/sign-up")}> + Don't have an account? Sign up + + + + ) +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: "#fff", + }, + inner: { + flex: 1, + justifyContent: "center", + padding: 24, + }, + title: { + fontSize: 28, + fontWeight: "bold", + marginBottom: 32, + textAlign: "center", + }, + input: { + borderWidth: 1, + borderColor: "#ddd", + borderRadius: 8, + padding: 12, + marginBottom: 16, + fontSize: 16, + }, + button: { + backgroundColor: "#000", + borderRadius: 8, + padding: 16, + alignItems: "center", + marginBottom: 16, + }, + buttonDisabled: { + opacity: 0.6, + }, + buttonText: { + color: "#fff", + fontSize: 16, + fontWeight: "600", + }, + link: { + textAlign: "center", + color: "#666", + fontSize: 14, + }, +}) +` + +// app/(auth)/sign-up.tsx +const SIGN_UP_TSX = `import { useState } from "react" +import { View, TextInput, StyleSheet, Text, Pressable, Alert, KeyboardAvoidingView, Platform } from "react-native" +import { useAuthActions } from "@convex-dev/auth/react" +import { useRouter } from "expo-router" + +export default function SignUpScreen() { + const { signIn } = useAuthActions() + const router = useRouter() + const [email, setEmail] = useState("") + const [password, setPassword] = useState("") + const [confirmPassword, setConfirmPassword] = useState("") + const [isLoading, setIsLoading] = useState(false) + + const handleSignUp = async () => { + if (!email || !password || !confirmPassword) { + Alert.alert("Error", "Please fill in all fields") + return + } + if (password !== confirmPassword) { + Alert.alert("Error", "Passwords do not match") + return + } + setIsLoading(true) + try { + await signIn("password", { email, password, flow: "signUp" }) + router.replace("/") + } catch (error) { + Alert.alert("Sign Up Failed", error instanceof Error ? error.message : "An error occurred") + } finally { + setIsLoading(false) + } + } + + return ( + + + Create Account + + + + + {isLoading ? "Creating account..." : "Sign Up"} + + router.back()}> + Already have an account? Sign in + + + + ) +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: "#fff", + }, + inner: { + flex: 1, + justifyContent: "center", + padding: 24, + }, + title: { + fontSize: 28, + fontWeight: "bold", + marginBottom: 32, + textAlign: "center", + }, + input: { + borderWidth: 1, + borderColor: "#ddd", + borderRadius: 8, + padding: 12, + marginBottom: 16, + fontSize: 16, + }, + button: { + backgroundColor: "#000", + borderRadius: 8, + padding: 16, + alignItems: "center", + marginBottom: 16, + }, + buttonDisabled: { + opacity: 0.6, + }, + buttonText: { + color: "#fff", + fontSize: 16, + fontWeight: "600", + }, + link: { + textAlign: "center", + color: "#666", + fontSize: 14, + }, +}) +` + +/** + * Injects authTables import and spread into convex/schema.ts + */ +async function updateSchemaWithAuthTables(sandbox: Sandbox): Promise { + const schemaPath = '/home/user/app/convex/schema.ts' + let currentContent: string + try { + currentContent = await sandbox.files.read(schemaPath) + } catch { + console.log('[Auth Setup] convex/schema.ts not found, skipping schema update') + return + } + + if (currentContent.includes('authTables')) { + console.log('[Auth Setup] authTables already present in schema.ts, skipping') + return + } + + let newContent = currentContent + + // Add authTables import after last import statement + const authImport = `import { authTables } from "@convex-dev/auth/server"` + const importRegex = /^import .+$/gm + let lastImportMatch: RegExpExecArray | null = null + let match: RegExpExecArray | null + while ((match = importRegex.exec(currentContent)) !== null) { + lastImportMatch = match + } + + if (lastImportMatch) { + const insertPos = lastImportMatch.index + lastImportMatch[0].length + newContent = newContent.slice(0, insertPos) + '\n' + authImport + newContent.slice(insertPos) + } else { + newContent = authImport + '\n' + newContent + } + + // Spread authTables as first entry in defineSchema({...}) + newContent = newContent.replace( + /defineSchema\(\{/, + 'defineSchema({\n ...authTables,' + ) + + await sandbox.files.write(schemaPath, newContent) + console.log('[Auth Setup] Updated convex/schema.ts with authTables') +} + +/** + * Injects auth HTTP routes into convex/http.ts + */ +async function updateHttpWithAuthRoutes(sandbox: Sandbox): Promise { + const httpPath = '/home/user/app/convex/http.ts' + let currentContent: string + try { + currentContent = await sandbox.files.read(httpPath) + } catch { + // http.ts doesn't exist, write a fresh one + const freshHttpTs = `import { httpRouter } from "convex/server" +import { auth } from "./auth" + +const http = httpRouter() + +auth.addHttpRoutes(http) + +export default http +` + await sandbox.files.write(httpPath, freshHttpTs) + console.log('[Auth Setup] Created convex/http.ts with auth routes') + return + } + + if (currentContent.includes('auth.addHttpRoutes')) { + console.log('[Auth Setup] auth routes already present in http.ts, skipping') + return + } + + let newContent = currentContent + + // Add auth import after httpRouter import + const authImport = `import { auth } from "./auth"` + const httpRouterImportMatch = newContent.match(/^import \{ httpRouter \} from .+$/m) + if (httpRouterImportMatch) { + const insertPos = httpRouterImportMatch.index! + httpRouterImportMatch[0].length + newContent = newContent.slice(0, insertPos) + '\n' + authImport + newContent.slice(insertPos) + } else { + newContent = authImport + '\n' + newContent + } + + // Add auth.addHttpRoutes(http) before export default + newContent = newContent.replace( + /export default http/, + 'auth.addHttpRoutes(http)\n\nexport default http' + ) + + await sandbox.files.write(httpPath, newContent) + console.log('[Auth Setup] Updated convex/http.ts with auth routes') +} + +/** + * Updates app/_layout.tsx to use ConvexAuthProvider instead of ConvexProvider/ConvexWrapper + */ +async function updateLayoutWithConvexAuthProvider(sandbox: Sandbox): Promise { + const layoutPath = '/home/user/app/app/_layout.tsx' + let currentContent: string + try { + currentContent = await sandbox.files.read(layoutPath) + } catch { + console.log('[Auth Setup] app/_layout.tsx not found, skipping layout update') + return + } + + if (currentContent.includes('ConvexAuthProvider')) { + console.log('[Auth Setup] ConvexAuthProvider already present in _layout.tsx, skipping') + return + } + + let newContent = currentContent + + if (currentContent.includes('ConvexProvider') && currentContent.includes('ConvexWrapper')) { + // Cloud enable was run — replace ConvexWrapper pattern with ConvexAuthProvider + + // Replace the ConvexProvider + ConvexReactClient import line + newContent = newContent.replace( + /import \{ ConvexProvider, ConvexReactClient \} from ['"]convex\/react['"]\n/, + `import { ConvexAuthProvider } from "@convex-dev/auth/react-native"\nimport { ConvexReactClient } from "convex/react"\nimport * as SecureStore from "expo-secure-store"\n` + ) + + // Replace the conditional convex client initialization block with a direct one + newContent = newContent.replace( + /\/\/ Initialize Convex client\nconst convexUrl = process\.env\.EXPO_PUBLIC_CONVEX_URL\nconst convex = convexUrl\n \? new ConvexReactClient\(convexUrl, \{ unsavedChangesWarning: false \}\)\n : null\n/, + `const convex = new ConvexReactClient(process.env.EXPO_PUBLIC_CONVEX_URL!)\n` + ) + + // Remove the ConvexWrapper component definition + newContent = newContent.replace( + /\/\/ Wrapper to conditionally include ConvexProvider\nfunction ConvexWrapper\(\{ children \}: \{ children: React\.ReactNode \}\) \{\n if \(convex\) \{\n return \{children\}<\/ConvexProvider>\n \}\n return <>\{children\}\n\}\n/, + '' + ) + + // Replace and JSX tags + newContent = newContent.replace( + //g, + `` + ) + newContent = newContent.replace( + /<\/ConvexWrapper>/g, + `` + ) + } else if (currentContent.includes('ConvexProvider')) { + // ConvexProvider exists but no ConvexWrapper — simpler replacement + newContent = newContent.replace( + /import \{ ConvexProvider.*?\} from ['"]convex\/react['"]/, + `import { ConvexAuthProvider } from "@convex-dev/auth/react-native"\nimport { ConvexReactClient } from "convex/react"\nimport * as SecureStore from "expo-secure-store"` + ) + newContent = newContent.replace(/]*)>/g, ``) + newContent = newContent.replace(/<\/ConvexProvider>/g, ``) + } else { + // No Convex provider at all — inject ConvexAuthProvider around the root element + const authProviderImport = `import { ConvexAuthProvider } from "@convex-dev/auth/react-native"\nimport { ConvexReactClient } from "convex/react"\nimport * as SecureStore from "expo-secure-store"\n\nconst convex = new ConvexReactClient(process.env.EXPO_PUBLIC_CONVEX_URL!)\n` + + // Find last import and inject after it + const importRegex = /^import .+$/gm + let lastImportMatch: RegExpExecArray | null = null + let match: RegExpExecArray | null + while ((match = importRegex.exec(currentContent)) !== null) { + lastImportMatch = match + } + + if (lastImportMatch) { + const insertPos = lastImportMatch.index + lastImportMatch[0].length + newContent = newContent.slice(0, insertPos) + '\n\n' + authProviderImport + newContent.slice(insertPos) + } + + // Wrap the root return element with ConvexAuthProvider + console.log('[Auth Setup] No existing ConvexProvider found — manual wrapping may be needed in _layout.tsx') + } + + await sandbox.files.write(layoutPath, newContent) + console.log('[Auth Setup] Updated app/_layout.tsx with ConvexAuthProvider') +} + +export async function POST(request: NextRequest) { + try { + // Authenticate user + const session = await auth.api.getSession({ headers: await headers() }) + if (!session?.user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const { projectId } = body + + if (!projectId) { + return NextResponse.json({ error: 'Missing projectId' }, { status: 400 }) + } + + // Get project with ownership check + const [project] = await db + .select() + .from(projects) + .where(and(eq(projects.id, projectId), eq(projects.userId, session.user.id))) + .limit(1) + + if (!project) { + return NextResponse.json({ error: 'Project not found' }, { status: 404 }) + } + + if (!project.sandboxId) { + return NextResponse.json( + { error: 'No active sandbox found for this project' }, + { status: 400 } + ) + } + + // Connect to sandbox + console.log('[Auth Setup] Connecting to sandbox:', project.sandboxId) + const sandbox = await Sandbox.connect(project.sandboxId) + + // Step 1: Install required packages + console.log('[Auth Setup] Installing @convex-dev/auth, @auth/core, expo-secure-store...') + const installResult = await sandbox.commands.run( + 'cd /home/user/app && bun add @convex-dev/auth @auth/core expo-secure-store', + { timeoutMs: 120000 } + ) + if (installResult.exitCode !== 0) { + console.warn('[Auth Setup] Package install stderr:', installResult.stderr) + } + console.log('[Auth Setup] Packages installed') + + // Step 2: Write convex/auth.ts + console.log('[Auth Setup] Writing convex/auth.ts...') + await sandbox.files.write('/home/user/app/convex/auth.ts', CONVEX_AUTH_TS) + + // Step 3: Write convex/auth.config.ts + console.log('[Auth Setup] Writing convex/auth.config.ts...') + await sandbox.files.write('/home/user/app/convex/auth.config.ts', CONVEX_AUTH_CONFIG_TS) + + // Step 4: Update convex/schema.ts with authTables + console.log('[Auth Setup] Updating convex/schema.ts...') + await updateSchemaWithAuthTables(sandbox) + + // Step 5: Update convex/http.ts with auth routes + console.log('[Auth Setup] Updating convex/http.ts...') + await updateHttpWithAuthRoutes(sandbox) + + // Step 6: Update app/_layout.tsx to use ConvexAuthProvider + console.log('[Auth Setup] Updating app/_layout.tsx...') + await updateLayoutWithConvexAuthProvider(sandbox) + + // Step 7: Create auth screens directory and files + console.log('[Auth Setup] Creating auth screens...') + await sandbox.commands.run('mkdir -p /home/user/app/app/\\(auth\\)', { timeoutMs: 10000 }) + await sandbox.files.write('/home/user/app/app/(auth)/_layout.tsx', AUTH_LAYOUT_TSX) + await sandbox.files.write('/home/user/app/app/(auth)/sign-in.tsx', SIGN_IN_TSX) + await sandbox.files.write('/home/user/app/app/(auth)/sign-up.tsx', SIGN_UP_TSX) + + // Step 8: Commit all changes via git + console.log('[Auth Setup] Committing changes...') + try { + await sandbox.commands.run( + 'cd /home/user/app && git add -A && git commit -m "Setup Convex Auth with email/password authentication"', + { timeoutMs: 30000 } + ) + console.log('[Auth Setup] Git commit successful') + } catch (gitError) { + console.warn('[Auth Setup] Git commit failed (non-fatal):', gitError) + // Don't fail the operation if git commit fails + } + + console.log('[Auth Setup] Convex Auth setup complete') + + return NextResponse.json({ + success: true, + message: 'Convex Auth setup complete. Note: run "npx @convex-dev/auth" in your project to generate JWT keys and configure CONVEX_SITE_URL in your Convex dashboard.', + }) + } catch (error) { + console.error('[Auth Setup] Error:', error) + const errorMessage = error instanceof Error ? error.message : 'Failed to setup auth' + return NextResponse.json({ error: errorMessage }, { status: 500 }) + } +} diff --git a/apps/web/app/(app)/p/[id]/page.tsx b/apps/web/app/(app)/p/[id]/page.tsx index 7894d2f4..d1e74934 100644 --- a/apps/web/app/(app)/p/[id]/page.tsx +++ b/apps/web/app/(app)/p/[id]/page.tsx @@ -596,24 +596,18 @@ function ProjectPageInternal() { fetchCloudStatus() }, [fetchCloudStatus]) - const handleSetupAuth = useCallback(() => { - setDesktopSidebarPanel(null) - setMobileSidebarPanel(null) - append({ - role: 'user', - content: `Please set up Convex authentication with email/password for my Expo React Native app. Follow these steps: - -1. Install the required packages: @convex-dev/auth and @auth/core -2. Create convex/auth.ts with the Password provider from @convex-dev/auth/providers/Password -3. Update convex/schema.ts to spread authTables from @convex-dev/auth/server inside defineSchema -4. Create or update convex/http.ts to add authentication HTTP routes using auth.addHttpRoutes(http) -5. Run npx @convex-dev/auth to generate JWT private key and JWKS, then configure SITE_URL as an environment variable -6. Update the root app layout (_layout.tsx) to wrap with ConvexAuthProvider from @convex-dev/auth/react-native, using expo-secure-store as the storage option -7. Create a sign-in screen and sign-up screen with email and password input fields using the useAuthActions hook - -Important: Use @convex-dev/auth/react-native (not /react) for Expo compatibility and expo-secure-store for secure token persistence.`, + const handleSetupAuth = useCallback(async (): Promise => { + const response = await fetch('/api/setup-convex-auth', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ projectId }), }) - }, [append, setDesktopSidebarPanel, setMobileSidebarPanel]) + + if (!response.ok) { + const error = await response.json() + throw new Error(error.error || 'Failed to setup Convex Auth') + } + }, [projectId]) // Legacy compatibility const isDesktopMode = viewMode === 'desktop' diff --git a/apps/web/components/app-sidebar.tsx b/apps/web/components/app-sidebar.tsx index 13ee0e5a..9c01a402 100644 --- a/apps/web/components/app-sidebar.tsx +++ b/apps/web/components/app-sidebar.tsx @@ -40,7 +40,7 @@ interface AppSidebarProps { cloudEnabled?: boolean cloudDeploymentUrl?: string onCloudEnabled?: () => void - onSetupAuth?: () => void + onSetupAuth?: () => Promise } function SidebarToggle() { @@ -421,7 +421,7 @@ export function AppSidebar({ projectId={projectId} cloudEnabled={cloudEnabled || false} onNavigateToCloud={() => handlePanelChange('cloud')} - onSetupAuth={onSetupAuth || (() => {})} + onSetupAuth={onSetupAuth || (async () => {})} onClose={() => handlePanelChange(null)} /> diff --git a/apps/web/components/auth-sidebar-panel.tsx b/apps/web/components/auth-sidebar-panel.tsx index 6326bf87..a46d7c08 100644 --- a/apps/web/components/auth-sidebar-panel.tsx +++ b/apps/web/components/auth-sidebar-panel.tsx @@ -12,13 +12,14 @@ import { Database, Globe, Smartphone, + AlertCircle, } from 'lucide-react' interface AuthSidebarPanelProps { projectId?: string cloudEnabled: boolean onNavigateToCloud: () => void - onSetupAuth: () => void + onSetupAuth: () => Promise onClose: () => void } @@ -30,6 +31,8 @@ const SETUP_STEPS = [ { icon: Smartphone, text: 'ConvexAuthProvider in your app layout' }, ] +type SetupStatus = 'idle' | 'loading' | 'success' | 'error' + export function AuthSidebarPanel({ projectId, cloudEnabled, @@ -37,12 +40,19 @@ export function AuthSidebarPanel({ onSetupAuth, onClose, }: AuthSidebarPanelProps) { - const [isSettingUp, setIsSettingUp] = useState(false) + const [setupStatus, setSetupStatus] = useState('idle') + const [errorMessage, setErrorMessage] = useState(null) - const handleSetupAuth = () => { - setIsSettingUp(true) - onSetupAuth() - setTimeout(() => setIsSettingUp(false), 3000) + const handleSetupAuth = async () => { + setSetupStatus('loading') + setErrorMessage(null) + try { + await onSetupAuth() + setSetupStatus('success') + } catch (error) { + setSetupStatus('error') + setErrorMessage(error instanceof Error ? error.message : 'An unexpected error occurred') + } } return ( @@ -86,6 +96,31 @@ export function AuthSidebarPanel({ Enable Cloud First
+ ) : setupStatus === 'success' ? ( +
+
+ +
+
+ Auth setup complete +
+
+ Convex Auth files have been written and committed to your project. +
+
+
+ +
+

Next step

+

+ Run npx @convex-dev/auth in your project to generate JWT keys, then set CONVEX_SITE_URL in your Convex dashboard. +

+
+ + +
) : (

@@ -106,19 +141,42 @@ export function AuthSidebarPanel({

-
-

- The AI will configure Convex Auth with email/password authentication - and create sign-in and sign-up screens for your app. -

-
+ {setupStatus === 'error' && errorMessage && ( +
+ +
+
+ Setup failed +
+
+ {errorMessage} +
+
+
+ )} + + {setupStatus === 'loading' && ( +
+

+ Installing packages and writing auth files to your project. This may take a minute... +

+
+ )} + + {setupStatus === 'idle' && ( +
+

+ Auth files will be written directly to your project and committed via git. +

+
+ )}