Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
91488bb
Implement standard tier: 1/mo, 8000 credits, billed yearly
CJWTRUST Jan 14, 2026
c284e37
Changes before Firebase Studio auto-run
ngoiyaeric Jan 14, 2026
a40cd86
feat: Implement Standard Tier credit system
ngoiyaeric Jan 14, 2026
1f9de85
fix: update import in app/api/user/credits/route.ts
ngoiyaeric Jan 14, 2026
696c177
fix: add schema to drizzle instance in lib/db/index.ts
ngoiyaeric Jan 14, 2026
f30205b
fix: use getTierConfig in app/api/user/credits/route.ts
ngoiyaeric Jan 14, 2026
584015e
fix(auth,credits): enhance oauth error logging and integrate credits …
google-labs-jules[bot] Jan 17, 2026
d6165e8
Merge pull request #437 from QueueLab/fix-oauth-and-credits-integrati…
ngoiyaeric Jan 17, 2026
8f2fa4e
Fix: Stripe popup not showing by using useCurrentUser hook
ngoiyaeric Jan 17, 2026
2a39100
Fix: Removed 7-day cooldown and used sessionStorage for easier testin…
ngoiyaeric Jan 17, 2026
7e9679a
Refactor: Connect credit popup to header icon and add basic test file
ngoiyaeric Jan 17, 2026
f38c5a2
Fix: Make history icon visible on desktop and use Sprout icon
ngoiyaeric Jan 17, 2026
9174d24
Fix: Add Stripe checkout, webhook, user init, and credit consumption …
ngoiyaeric Jan 17, 2026
b1d2875
Fix: Update Stripe API version to match SDK
ngoiyaeric Jan 17, 2026
08a6c9b
Fix: Update Stripe API version to '2025-12-15.clover' with type asser…
ngoiyaeric Jan 17, 2026
02b1f99
Fix: Await headers() in Stripe webhook for Next.js 16 compatibility
ngoiyaeric Jan 17, 2026
008989c
fix: add fallback stripe keys for build
ngoiyaeric Jan 17, 2026
f815d08
fix: dependency resolution and build issues
ngoiyaeric Jan 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,9 @@ SERVER_ACTIONS_ALLOWED_ORIGINS="*"
# Authentication Configuration
# Disable Supabase auth and use mock user for development/preview
AUTH_DISABLED_FOR_DEV="false"

# Standard Tier Configuration
STANDARD_TIER_PRICE_ID="price_standard_41_yearly"
STANDARD_TIER_CREDITS=8000
STANDARD_TIER_MONTHLY_PRICE=41
STANDARD_TIER_BILLING_CYCLE="yearly"
Comment on lines +10 to +15
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider renaming to .env.example and documenting placeholders.

Committing a .env file can accidentally leak secrets if real values are added later. The standard practice is to commit .env.example with placeholder values and add .env to .gitignore.

Also, price_standard_41_yearly appears to be a placeholder Stripe price ID—consider adding a comment indicating it must be replaced with a real Stripe price ID in production.

Suggested changes
 # Standard Tier Configuration
+# Replace STANDARD_TIER_PRICE_ID with your actual Stripe price ID from the dashboard
-STANDARD_TIER_PRICE_ID="price_standard_41_yearly"
+STANDARD_TIER_PRICE_ID="price_xxx"  # Get from Stripe Dashboard
 STANDARD_TIER_CREDITS=8000
 STANDARD_TIER_MONTHLY_PRICE=41
 STANDARD_TIER_BILLING_CYCLE="yearly"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Standard Tier Configuration
STANDARD_TIER_PRICE_ID="price_standard_41_yearly"
STANDARD_TIER_CREDITS=8000
STANDARD_TIER_MONTHLY_PRICE=41
STANDARD_TIER_BILLING_CYCLE="yearly"
# Standard Tier Configuration
# Replace STANDARD_TIER_PRICE_ID with your actual Stripe price ID from the dashboard
STANDARD_TIER_PRICE_ID="price_xxx" # Get from Stripe Dashboard
STANDARD_TIER_CREDITS=8000
STANDARD_TIER_MONTHLY_PRICE=41
STANDARD_TIER_BILLING_CYCLE="yearly"
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 12-12: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)


[warning] 13-13: [UnorderedKey] The STANDARD_TIER_CREDITS key should go before the STANDARD_TIER_PRICE_ID key

(UnorderedKey)


[warning] 14-14: [UnorderedKey] The STANDARD_TIER_MONTHLY_PRICE key should go before the STANDARD_TIER_PRICE_ID key

(UnorderedKey)


[warning] 15-15: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)


[warning] 15-15: [UnorderedKey] The STANDARD_TIER_BILLING_CYCLE key should go before the STANDARD_TIER_CREDITS key

(UnorderedKey)

🤖 Prompt for AI Agents
In @.env around lines 10 - 15, Rename the committed .env to .env.example and
replace real-looking values with placeholders so no secrets are checked in;
update the variables STANDARD_TIER_PRICE_ID, STANDARD_TIER_CREDITS,
STANDARD_TIER_MONTHLY_PRICE, and STANDARD_TIER_BILLING_CYCLE to placeholder
values (e.g., PRICE_ID_PLACEHOLDER) and add a comment next to
STANDARD_TIER_PRICE_ID indicating it must be replaced with a real Stripe price
ID in production; also add .env to .gitignore to prevent local env files from
being committed.

12 changes: 12 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Supabase Configuration
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key-here

# Stripe Configuration
STANDARD_TIER_PRICE_ID=price_placeholder # must be real Stripe price ID in prod
STANDARD_TIER_CREDITS=8000
STANDARD_TIER_MONTHLY_PRICE=41
STANDARD_TIER_BILLING_CYCLE=yearly

# Other Environment Variables
# Add other existing env vars here with placeholder values
80 changes: 30 additions & 50 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,59 +1,39 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz

# testing
/coverage

# next.js
/.next/
/out/

# production
/build
# Dependency directories
node_modules/
.bun/

# Build outputs
.next/
dist/
build/
out/

# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
.env.*.local

# misc
# IDE/Editor
.vscode/
.idea/
*.swp
*.swo
.DS_Store
*.pem

# debug
# Logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
bun.lockb

# local env files
.env*.local

# log files
dev_server.log
server.log
# Testing
playwright-report/
test-results/
coverage/

# vercel
.vercel

# typescript
# Misc
.vercel/
*.tsbuildinfo
next-env.d.ts

# Playwright
/playwright-report/
/test-results/
/dev.log
# AlphaEarth Embeddings - Sensitive Files
# Add these lines to your main .gitignore

# GCP Service Account Credentials (NEVER commit)
gcp_credentials.json
**/gcp_credentials.json

# AlphaEarth Index File (large, should be downloaded separately)
aef_index.csv

# Environment variables with GCP credentials
.env.local
.env.production.local
*.log
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "esbenp.prettier-vscode",
"IDX.corgiMode": true
}
7 changes: 7 additions & 0 deletions app/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { saveChat } from '@/lib/actions/chat';
import { getCurrentUserIdOnServer } from '@/lib/auth/get-current-user';
import { type Chat } from '@/lib/types';
import { v4 as uuidv4 } from 'uuid';
import { checkAndConsumeCredits } from '@/lib/middleware/check-credits';

export async function POST(request: NextRequest) {
try {
Expand All @@ -11,6 +12,12 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

// Check and consume credits
const creditCheck = await checkAndConsumeCredits(request, 10);
if (creditCheck.error) {
return NextResponse.json({ error: creditCheck.error }, { status: creditCheck.status });
}

const body = await request.json();
const { title, initialMessageContent, role = 'user' }
= body;
Expand Down
45 changes: 45 additions & 0 deletions app/api/checkout/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';
import { getCurrentUserIdOnServer } from '@/lib/auth/get-current-user';

// Ensure Stripe is initialized with the secret key
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || 'sk_test_dummy', {
apiVersion: '2025-12-15.clover' as any, // Use the specific version expected by the SDK
});

export async function POST(req: NextRequest) {
try {
const userId = await getCurrentUserIdOnServer();
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

const body = await req.json();
const { priceId, returnUrl } = body;

if (!priceId) {
return NextResponse.json({ error: 'Price ID is required' }, { status: 400 });
}

const session = await stripe.checkout.sessions.create({
mode: 'subscription', // or 'payment' for one-time
payment_method_types: ['card'],
line_items: [
{
price: priceId,
quantity: 1,
},
],
metadata: {
userId: userId, // Pass userId to metadata for webhook
},
success_url: `${returnUrl || req.headers.get('origin')}/?success=true`,
cancel_url: `${returnUrl || req.headers.get('origin')}/?canceled=true`,
});

return NextResponse.json({ url: session.url });
} catch (error: any) {
console.error('Error creating checkout session:', error);
return NextResponse.json({ error: error.message }, { status: 500 });
}
}
53 changes: 53 additions & 0 deletions app/api/user/credits/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { users } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';
import { getSupabaseServerClient } from '@/lib/supabase/client';
import { TIERS, parseTier, getTierConfig } from '@/lib/utils/subscription';

export async function GET(req: NextRequest) {
try {
const supabase = getSupabaseServerClient();
const {
data: { user },
error: userError
} = await supabase.auth.getUser();

if (userError || !user) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}

// Get user from database
const dbUser = await db.query.users.findFirst({
where: eq(users.id, user.id)
});

if (!dbUser) {
return NextResponse.json(
{ error: 'User not found' },
{ status: 404 }
);
}

const tier = parseTier(dbUser.tier);
// If user is not on Standard tier, they might not need credits logic,
// but for now we return the credits regardless.
// If the tier doesn't support credits (e.g. Free or Pro), the UI can handle it.

return NextResponse.json({
credits: dbUser.credits,
tier: tier,
features: getTierConfig(tier)
});

} catch (error) {
console.error('Error fetching user credits:', error);
return NextResponse.json(
{ error: 'Internal Server Error' },
{ status: 500 }
);
}
}
68 changes: 68 additions & 0 deletions app/api/webhooks/stripe/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { headers } from 'next/headers';
import { NextResponse } from 'next/server';
import Stripe from 'stripe';
import { getSupabaseServiceClient } from '@/lib/supabase/client';
import { TIER_CONFIGS, TIERS } from '@/lib/utils/subscription';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || 'sk_test_dummy', {
apiVersion: '2025-12-15.clover' as any,
});

const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET || 'whsec_dummy';

export async function POST(req: Request) {
const body = await req.text();
const headersList = await headers(); // Await headers() as per Next.js 15+ / 16
const signature = headersList.get('stripe-signature') as string;

let event: Stripe.Event;

try {
event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
} catch (err: any) {
console.error(`Webhook signature verification failed: ${err.message}`);
return NextResponse.json({ error: `Webhook Error: ${err.message}` }, { status: 400 });
}

const supabase = getSupabaseServiceClient();

try {
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object as Stripe.Checkout.Session;
const userId = session.metadata?.userId;

if (userId) {
console.log(`Processing checkout for user: ${userId}`);
// Update user credits and tier
// Assuming Standard Tier for this example, or derive from session
const standardCredits = TIER_CONFIGS[TIERS.STANDARD].credits;

const { error } = await supabase
.from('users')
.update({
credits: standardCredits,
tier: 'standard',
// updated_at: new Date().toISOString()
})
.eq('id', userId);

if (error) {
console.error('Error updating user credits in Supabase:', error);
throw error;
}
console.log(`Updated credits for user ${userId} to ${standardCredits}`);
}
break;
}
// Handle other event types
default:
console.log(`Unhandled event type ${event.type}`);
}
} catch (error) {
console.error('Error handling webhook event:', error);
return NextResponse.json({ error: 'Webhook handler failed' }, { status: 500 });
}

return NextResponse.json({ received: true });
}
34 changes: 33 additions & 1 deletion app/auth/callback/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { cookies } from 'next/headers'
import { NextResponse } from 'next/server'
import { TIER_CONFIGS, TIERS } from '@/lib/utils/subscription';

export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url)
Expand Down Expand Up @@ -31,11 +32,42 @@ export async function GET(request: Request) {
}
)
const { error } = await supabase.auth.exchangeCodeForSession(code)
if (!error) {
if (error) {
console.error('[Auth Callback] Exchange code error:', {
message: error.message,
status: error.status,
name: error.name,
code: code?.substring(0, 10) + '...'
})
return NextResponse.redirect(`${origin}/auth/auth-code-error?error=${encodeURIComponent(error.message)}`)
} else {
try {
const { data: { user }, error: userErr } = await supabase.auth.getUser()
if (!userErr && user) {
console.log('[Auth Callback] User signed in:', user.email)

// Check if user exists in the 'users' table
const { data: existingUser, error: fetchError } = await supabase
.from('users')
.select('*')
.eq('id', user.id)
.single()

if (!existingUser && !fetchError) {
console.log('[Auth Callback] Initializing new user:', user.id);
// Create new user entry
const { error: insertError } = await supabase.from('users').insert({
id: user.id,
email: user.email,
credits: 0, // Start with 0 or free tier credits
tier: 'free',
// Add other default fields if necessary
});

if (insertError) {
console.error('[Auth Callback] Error creating user record:', insertError);
}
}
}
} catch (e) {
console.warn('[Auth Callback] Could not fetch user after exchange', e)
Expand Down
2 changes: 2 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { MapLoadingProvider } from '@/components/map-loading-context';
import ConditionalLottie from '@/components/conditional-lottie';
import { MapProvider } from '@/components/map/map-context'
import { getSupabaseUserAndSessionOnServer } from '@/lib/auth/get-current-user'
import { PurchaseCreditsProvider } from '@/components/providers/purchase-credits-provider';

// Force dynamic rendering since we check auth with cookies
export const dynamic = 'force-dynamic'
Expand Down Expand Up @@ -93,6 +94,7 @@ export default async function RootLayout({
<ConditionalLottie />
{children}
<Sidebar />
<PurchaseCreditsProvider />
<Footer />
<Toaster />
</MapLoadingProvider>
Expand Down
Loading