diff --git a/.capy_commit_msg b/.capy_commit_msg new file mode 100644 index 00000000..a0a980c7 --- /dev/null +++ b/.capy_commit_msg @@ -0,0 +1 @@ +Capy jam: Unify pricing to Free and Pro (0/mo); wire Autumn checkout/portal; consolidate Autumn wrapper; reduce Convex writes on home; default sequential thinking to Kimi K2; update env example diff --git a/.env.example b/.env.example index 76232cd4..da17254d 100644 --- a/.env.example +++ b/.env.example @@ -23,6 +23,8 @@ GROQ_API_KEY=your_groq_api_key_here # Get yours at https://dashboard.stripe.com/apikeys NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_your_stripe_publishable_key_here STRIPE_SECRET_KEY=sk_test_your_stripe_secret_key_here +# Stripe Pro price ID (fallback when Autumn isn't configured) +NEXT_PUBLIC_STRIPE_PRO_PRICE_ID=price_your_pro_monthly_price_id # Stripe Webhook Secret (get from Stripe Dashboard > Webhooks) STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret_here diff --git a/AutumnWrapper.tsx b/AutumnWrapper.tsx index 0f8e6828..71db4d48 100644 --- a/AutumnWrapper.tsx +++ b/AutumnWrapper.tsx @@ -1,14 +1,2 @@ "use client"; -import { AutumnProvider } from "autumn-js/react"; -import { api } from "./convex/_generated/api"; -import { useConvex } from "convex/react"; - -export function AutumnWrapper({ children }: { children: React.ReactNode }) { - const convex = useConvex(); - - return ( - - {children} - - ); -} \ No newline at end of file +export { AutumnWrapper } from "./app/components/AutumnWrapper"; diff --git a/app/api/autumn/billing-portal/route.ts b/app/api/autumn/billing-portal/route.ts new file mode 100644 index 00000000..c1416906 --- /dev/null +++ b/app/api/autumn/billing-portal/route.ts @@ -0,0 +1,45 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getAuth } from '@clerk/nextjs/server'; +import { autumn } from '@/convex/autumn'; + +export async function POST(request: NextRequest) { + try { + const auth = getAuth(request); + if (!auth.userId) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const { returnUrl } = await request.json().catch(() => ({ returnUrl: undefined })); + const origin = request.headers.get('origin') || process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'; + + if (process.env.AUTUMN_SECRET_KEY) { + try { + const mockCtx = { auth: { getUserIdentity: () => ({ subject: auth.userId, name: '', email: '' }) } } as any; + const result: any = await autumn.billingPortal(mockCtx, { returnUrl: returnUrl || `${origin}/pricing` }); + const url = result?.url || result?.portalUrl || result?.redirectUrl; + if (url) { + return NextResponse.json({ url }); + } + } catch (e) { + // fall through to Stripe + } + } + + // Fallback to Stripe portal + const stripeRes = await fetch(`${origin}/api/stripe/customer-portal`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ returnUrl: returnUrl || `${origin}/pricing` }) + }); + + if (!stripeRes.ok) { + const err = await stripeRes.text(); + return NextResponse.json({ error: err || 'Failed to create portal session' }, { status: 500 }); + } + + const data = await stripeRes.json(); + return NextResponse.json({ url: data.url }); + } catch (error) { + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); + } +} diff --git a/app/api/autumn/checkout/route.ts b/app/api/autumn/checkout/route.ts new file mode 100644 index 00000000..78af7151 --- /dev/null +++ b/app/api/autumn/checkout/route.ts @@ -0,0 +1,49 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getAuth } from '@clerk/nextjs/server'; +import { autumn } from '@/convex/autumn'; + +export async function POST(request: NextRequest) { + try { + const auth = getAuth(request); + if (!auth.userId) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const { priceId } = await request.json().catch(() => ({ priceId: undefined })); + + const mockCtx = { auth: { getUserIdentity: () => ({ subject: auth.userId, name: '', email: '' }) } } as any; + + if (process.env.AUTUMN_SECRET_KEY) { + try { + const result: any = await autumn.checkout(mockCtx, { priceId }); + const url = result?.url || result?.sessionUrl || result?.redirectUrl; + if (url) { + return NextResponse.json({ url }); + } + } catch (e) { + // fall through to Stripe + } + } + + // Fallback to Stripe + const origin = request.headers.get('origin') || process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'; + const stripeRes = await fetch(`${origin}/api/stripe/create-checkout-session`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + priceId: priceId || process.env.NEXT_PUBLIC_STRIPE_PRO_PRICE_ID, + mode: 'subscription' + }) + }); + + if (!stripeRes.ok) { + const err = await stripeRes.text(); + return NextResponse.json({ error: err || 'Failed to create checkout session' }, { status: 500 }); + } + + const { sessionId } = await stripeRes.json(); + return NextResponse.json({ provider: 'stripe', sessionId }); + } catch (error) { + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); + } +} diff --git a/app/components/AutumnFallback.tsx b/app/components/AutumnFallback.tsx index 6ea993eb..c8b07ac0 100644 --- a/app/components/AutumnFallback.tsx +++ b/app/components/AutumnFallback.tsx @@ -2,13 +2,10 @@ // This file provides working alternatives when autumn-js/react is not available import React, { createContext, useContext, useState, useEffect } from 'react'; -import Link from 'next/link'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; -import { Check, Sparkles, Crown } from "lucide-react"; -import CheckoutButton from "@/components/stripe/CheckoutButton"; +import { CheckCircle } from "lucide-react"; -// Types interface Customer { id: string; email: string; @@ -27,7 +24,6 @@ interface UsageLimit { resetAt?: Date; } -// Mock context const AutumnContext = createContext<{ customer: Customer | null; isLoading: boolean; @@ -38,13 +34,11 @@ const AutumnContext = createContext<{ error: null }); -// Mock provider export const AutumnProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [customer, setCustomer] = useState(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); - // Mock loading customer data useEffect(() => { setIsLoading(true); setTimeout(() => { @@ -69,7 +63,6 @@ export const AutumnProvider: React.FC<{ children: React.ReactNode }> = ({ childr ); }; -// Mock hook for customer data export const useCustomer = () => { const context = useContext(AutumnContext); return { @@ -81,92 +74,68 @@ export const useCustomer = () => { // Mock pricing table component (two-tier) export const PricingTable: React.FC = () => { - const proPriceId = process.env.NEXT_PUBLIC_STRIPE_PRO_PRICE_ID || 'price_pro_monthly'; - const plans = [ { id: 'free', name: 'Free', price: '$0', - subtext: 'No credit card required', - description: 'Everything you need to get started.', - features: ['Up to 5 chats', 'Basic templates', 'Standard sandbox time', 'Community support'], - buttonText: 'Get started', - popular: false, - icon: Sparkles, + period: 'forever', + description: 'Perfect for getting started', + features: ['5 chats', '1 sandbox', 'Basic models'], + buttonText: 'Get Started', + popular: false }, { id: 'pro', name: 'Pro', price: '$20', - subtext: 'per month, cancel anytime', - description: 'Build without limits with advanced AI.', - features: ['Unlimited chats', 'Advanced AI models', 'Extended sandbox time', 'Priority support'], + period: 'per month', + description: 'For builders who want more', + features: ['Unlimited chats', 'Advanced models'], buttonText: 'Upgrade to Pro', - popular: true, - icon: Crown, - }, + popular: true + } ]; return ( -
- {plans.map((plan) => { - const Icon = plan.icon; - const isFree = plan.id === 'free'; - return ( - - {plan.popular && ( -
- - Most popular - -
- )} - -
-
-
-
- {plan.name} - {plan.description} -
-
-
- {plan.price} - {plan.subtext} -
-
- -
    - {plan.features.map((feature, index) => ( -
  • -
  • - ))} -
- {isFree ? ( - - - - ) : ( - - {plan.buttonText} - - )} -
-
- ); - })} +
+ {plans.map((plan) => ( + + {plan.popular && ( +
+ MOST POPULAR +
+ )} + + {plan.name} +
+ {plan.price} + {plan.period && {plan.period}} +
+ {plan.description} +
+ +
    + {plan.features.map((feature, index) => ( +
  • + + {feature} +
  • + ))} +
+ +
+
+ ))}
); }; -// Mock usage limit hook export const useUsageLimits = (featureId: string) => { const [limits, setLimits] = useState(null); const [isLoading, setIsLoading] = useState(false); @@ -189,4 +158,4 @@ export const useUsageLimits = (featureId: string) => { mutate: () => {}, isValidating: false }; -}; +}; \ No newline at end of file diff --git a/app/page.tsx b/app/page.tsx index ca859a95..b3c7cd04 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -493,36 +493,7 @@ Tip: I automatically detect and install npm packages from your code imports (lik iframeRef.current.src = data.url; } }, 100); - - // Capture screenshot for chat preview after sandbox is ready and update Convex - if (currentChatId && isSignedIn) { - setTimeout(async () => { - try { - console.log('[createSandbox] Capturing screenshot for chat preview...'); - const screenshotResponse = await fetch('/api/scrape-screenshot', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ url: data.url }) - }); - - const screenshotData = await screenshotResponse.json(); - if (screenshotData.success && screenshotData.screenshot) { - console.log('[createSandbox] Screenshot captured, updating chat...'); - await updateChatScreenshot({ - chatId: currentChatId, - screenshot: screenshotData.screenshot, - sandboxId: data.sandboxId, - sandboxUrl: data.url - }); - console.log('[createSandbox] Chat updated with screenshot'); - } else { - console.warn('[createSandbox] Failed to capture screenshot:', screenshotData.error); - } - } catch (error) { - console.error('[createSandbox] Error capturing screenshot:', error); - } - }, 3000); // Wait 3 seconds for the sandbox to fully load - } + } else { throw new Error(data.error || 'Unknown error'); } @@ -1127,6 +1098,29 @@ Tip: I automatically detect and install npm packages from your code imports (lik } }; + const savePreview = async () => { + try { + if (!sandboxData?.url || !currentChatId) return; + const screenshotResponse = await fetch('/api/scrape-screenshot', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ url: sandboxData.url }) + }); + const screenshotData = await screenshotResponse.json(); + if (screenshotData.success && screenshotData.screenshot) { + await updateChatScreenshot({ + chatId: currentChatId, + screenshot: screenshotData.screenshot, + sandboxId: sandboxData.sandboxId, + sandboxUrl: sandboxData.url + }); + addChatMessage('Preview saved to chat.', 'system'); + } + } catch (e) { + addChatMessage('Failed to save preview.', 'system'); + } + }; + const applyCode = async () => { const code = promptInput.trim(); if (!code) { @@ -1669,6 +1663,16 @@ Tip: I automatically detect and install npm packages from your code imports (lik allow="clipboard-write" sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-modals" /> + {/* Save preview button */} + {currentChatId && ( + + )} {/* Refresh button */}
@@ -167,4 +184,4 @@ export default function PricingPage() { ); -} +} \ No newline at end of file diff --git a/app/test-pricing/page.tsx b/app/test-pricing/page.tsx index 3c92103b..f4e9630e 100644 --- a/app/test-pricing/page.tsx +++ b/app/test-pricing/page.tsx @@ -20,58 +20,32 @@ import { CreditCard, TestTube, DollarSign } from "lucide-react"; const testPlans: PricingPlan[] = [ { - id: "test-free", - name: "Test Free Plan", - description: "Testing free tier functionality", + id: "free", + name: "Free", + description: "Testing free tier", price: 0, currency: "usd", interval: "month", features: [ - "Free tier testing", - "Basic Stripe integration", - "No payment required", + "5 chats", + "Basic models", ], - buttonText: "Test Free Plan", + buttonText: "Get Started", buttonVariant: "outline", }, { - id: "test-basic", - name: "Test Basic", - description: "Basic paid plan for testing", - price: 10, + id: "pro", + name: "Pro", + description: "Testing Pro plan", + price: 20, currency: "usd", interval: "month", - priceId: - process.env.NEXT_PUBLIC_STRIPE_TEST_BASIC_PRICE_ID || "price_test_basic", + priceId: process.env.NEXT_PUBLIC_STRIPE_PRO_PRICE_ID || "price_test_pro", features: [ - "Test basic payments", - "Stripe checkout flow", - "Subscription management", - "Test webhooks", + "Unlimited chats", + "Advanced models", ], - buttonText: "Test Basic Plan", - buttonVariant: "default", - }, - { - id: "test-premium", - name: "Test Premium", - description: "Premium plan for comprehensive testing", - price: 25, - currency: "usd", - interval: "month", - priceId: - process.env.NEXT_PUBLIC_STRIPE_TEST_PREMIUM_PRICE_ID || - "price_test_premium", - features: [ - "All basic features", - "Advanced payment testing", - "Multiple payment methods", - "Subscription updates", - "Proration testing", - ], - popular: true, - buttonText: "Test Premium Plan", - buttonVariant: "default", + buttonText: "Upgrade to Pro", }, ]; @@ -85,7 +59,6 @@ export default function TestPricingPage() { return (
- {/* Header */}
@@ -95,12 +68,10 @@ export default function TestPricingPage() {

Test the Stripe payment integration with various pricing scenarios - and configurations. This page demonstrates the full payment flow - including subscriptions, one-time payments, and custom amounts. + and configurations.

- {/* Test Plans */}

Test Subscription Plans @@ -110,9 +81,7 @@ export default function TestPricingPage() { - {/* Custom Testing Section */}
- {/* Custom Payment Test */} @@ -165,7 +134,7 @@ export default function TestPricingPage() { - Test Custom{" "} - {testMode === "subscription" ? "Subscription" : "Payment"} -{" "} - {customAmount} {customCurrency.toUpperCase()} + Test Custom {testMode === "subscription" ? "Subscription" : "Payment"} - {customAmount} {customCurrency.toUpperCase()} - {/* Quick Test Buttons */} @@ -196,7 +162,7 @@ export default function TestPricingPage() { - Test $9.99/month Subscription + Test $19.99/month Subscription Test $49.99 One-time Payment - - - Test Free Trial (Disabled) -
- {/* Test Information */} Testing Information @@ -268,19 +217,10 @@ export default function TestPricingPage() {

Test Credit Cards

    -
  • - 4242424242424242 - Visa (Success) -
  • -
  • - 4000000000000002 - Visa (Declined) -
  • -
  • - 4000000000009995 - Visa (Insufficient funds) -
  • -
  • - 4000002500003155 - Visa (Requires - authentication) -
  • +
  • 4242424242424242 - Visa (Success)
  • +
  • 4000000000000002 - Visa (Declined)
  • +
  • 4000000000009995 - Visa (Insufficient funds)
  • +
  • 4000002500003155 - Visa (Requires authentication)
@@ -293,13 +233,6 @@ export default function TestPricingPage() {

-
-

- Note: This page is for testing Stripe - integration. No real payments will be processed. Make sure your - environment variables are set correctly with Stripe test keys. -

-
diff --git a/components/ConvexChat.tsx b/components/ConvexChat.tsx index 2da3b2b1..1d25d020 100644 --- a/components/ConvexChat.tsx +++ b/components/ConvexChat.tsx @@ -208,10 +208,26 @@ export default function ConvexChat({ onChatSelect, onMessageAdd }: ConvexChatPro }; const handleUpgrade = async (plan: string) => { - // Integration with Stripe will be handled here - console.log('Upgrading to plan:', plan); - setShowPricingModal(false); - // TODO: Integrate with Stripe checkout + try { + const res = await fetch('/api/autumn/checkout', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ priceId: process.env.NEXT_PUBLIC_STRIPE_PRO_PRICE_ID }) + }); + const data = await res.json(); + setShowPricingModal(false); + if (data.url) { + window.location.href = data.url; + return; + } + if (data.provider === 'stripe' && data.sessionId) { + const { getStripe } = await import('@/lib/stripe-client'); + const stripe = await getStripe(); + if (stripe) await stripe.redirectToCheckout({ sessionId: data.sessionId }); + } + } catch (e) { + console.error('Upgrade failed', e); + } }; return ( diff --git a/components/EnhancedSettingsModal.tsx b/components/EnhancedSettingsModal.tsx index c6f1773b..98a147bb 100644 --- a/components/EnhancedSettingsModal.tsx +++ b/components/EnhancedSettingsModal.tsx @@ -55,47 +55,25 @@ export default function EnhancedSettingsModal({ isOpen, onClose }: EnhancedSetti }); const plans = [ + { + name: 'Free', + description: 'Perfect for getting started.', + monthlyPrice: 0, + annualPrice: 0, + features: [ + '5 chats', + 'Basic models' + ] + }, { name: 'Pro', - description: 'Designed for fast-moving teams building together in real time.', + description: 'Unlimited chats and advanced models.', monthlyPrice: 20, annualPrice: 20, popular: true, features: [ 'Unlimited chats', - 'Unlimited projects', - 'Private projects', - 'User roles & permissions', - 'Custom domains', - 'Priority support' - ] - }, - { - name: 'Business', - description: 'Advanced controls and power features for growing departments', - monthlyPrice: 50, - annualPrice: 40, - features: [ - 'All features in Pro, plus:', - '100 monthly credits', - 'SSO', - 'Personal Projects', - 'Opt out of data training', - 'Design templates', - 'Custom design systems' - ] - }, - { - name: 'Enterprise', - description: 'Built for large orgs needing flexibility, scale, and governance.', - isEnterprise: true, - features: [ - 'Everything in Business, plus:', - 'Dedicated support', - 'Onboarding services', - 'Custom integrations', - 'Group-based access control', - 'Custom design systems' + 'Advanced models' ] } ]; diff --git a/components/MasterDashboard.tsx b/components/MasterDashboard.tsx index 89568fd8..47641899 100644 --- a/components/MasterDashboard.tsx +++ b/components/MasterDashboard.tsx @@ -208,13 +208,13 @@ export default function MasterDashboard({ ? 'border-blue-600 text-blue-600 bg-blue-50' : 'border-transparent text-gray-600' )} - title={!isAvailable ? `${item.label} requires ${userSubscription === 'free' ? 'Pro' : 'Enterprise'} subscription` : item.description} + title={!isAvailable ? `${item.label} requires Pro subscription` : item.description} > {item.label} {!isAvailable && ( - {userSubscription === 'free' ? 'Pro' : 'Enterprise'} + {'Pro'} )} @@ -401,7 +401,7 @@ export default function MasterDashboard({ > - +
@@ -409,7 +409,7 @@ export default function MasterDashboard({ )} - {currentView === 'monitoring' && userSubscription === 'enterprise' && ( + {currentView === 'monitoring' && userSubscription !== 'free' && (

@@ -428,10 +428,10 @@ export default function MasterDashboard({

- {userSubscription === 'free' ? 'Pro Subscription Required' : 'Enterprise Subscription Required'} + {'Pro Subscription Required'}

- This feature requires a {userSubscription === 'free' ? 'Pro' : 'Enterprise'} subscription to access. + This feature requires a Pro subscription to access.