diff --git a/app/api/location/currency/route.ts b/app/api/location/currency/route.ts
new file mode 100644
index 0000000..299c4c4
--- /dev/null
+++ b/app/api/location/currency/route.ts
@@ -0,0 +1,34 @@
+import { NextResponse } from 'next/server'
+
+const FALLBACK_CURRENCY = 'USD'
+
+export async function GET() {
+ const controller = new AbortController()
+ const timeout = setTimeout(() => controller.abort(), 2500)
+
+ try {
+ const response = await fetch('https://ipapi.co/json/', {
+ cache: 'no-store',
+ headers: {
+ Accept: 'application/json',
+ 'User-Agent': 'tera-pricing/1.0',
+ },
+ signal: controller.signal,
+ })
+
+ if (!response.ok) {
+ return NextResponse.json({ currency: FALLBACK_CURRENCY, countryCode: '' })
+ }
+
+ const data = await response.json()
+
+ return NextResponse.json({
+ currency: typeof data?.currency === 'string' ? data.currency : FALLBACK_CURRENCY,
+ countryCode: typeof data?.country_code === 'string' ? data.country_code : '',
+ })
+ } catch {
+ return NextResponse.json({ currency: FALLBACK_CURRENCY, countryCode: '' })
+ } finally {
+ clearTimeout(timeout)
+ }
+}
diff --git a/app/layout.tsx b/app/layout.tsx
index 38692ea..c11fbc4 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -5,6 +5,11 @@ import AppLayout from '@/components/AppLayout'
import { Analytics } from '@vercel/analytics/react'
import { ThemeProvider } from '@/components/ThemeProvider'
+const shouldRenderAnalytics =
+ process.env.VERCEL === '1' ||
+ process.env.VERCEL === 'true' ||
+ Boolean(process.env.VERCEL_URL)
+
export const viewport = {
width: 'device-width',
initialScale: 1,
@@ -39,7 +44,7 @@ export default function RootLayout({ children }: { children: ReactNode }) {
{children}
-
+ {shouldRenderAnalytics ? : null}
diff --git a/app/pricing/page.tsx b/app/pricing/page.tsx
index 6efa551..78943c8 100644
--- a/app/pricing/page.tsx
+++ b/app/pricing/page.tsx
@@ -92,13 +92,14 @@ export default function PricingPage() {
useEffect(() => {
const loadUserAndCurrency = async () => {
try {
- const response = await fetch('https://ipapi.co/json/')
- const data = await response.json()
- const currencyCode = data.currency || 'USD'
- setCurrency(CURRENCY_CODES[currencyCode] || CURRENCY_CODES.USD)
- setCountryCode(data.country_code || '')
+ const response = await fetch('/api/location/currency', { cache: 'no-store' })
+ if (response.ok) {
+ const data = await response.json()
+ const currencyCode = data.currency || 'USD'
+ setCurrency(CURRENCY_CODES[currencyCode] || CURRENCY_CODES.USD)
+ setCountryCode(data.countryCode || '')
+ }
} catch (error) {
- console.error('Error fetching currency:', error)
setCurrency(CURRENCY_CODES.USD)
}
diff --git a/lib/free-plan-credits.ts b/lib/free-plan-credits.ts
index c320aa4..cfcc9e1 100644
--- a/lib/free-plan-credits.ts
+++ b/lib/free-plan-credits.ts
@@ -7,6 +7,7 @@ const PLAN_MONTHLY_CREDIT_CAPS: Record = {
plus: 5000
}
const RESET_INTERVAL_DAYS = 30
+const RESET_INTERVAL_MS = RESET_INTERVAL_DAYS * 24 * 60 * 60 * 1000
type CreditState = {
used: number
@@ -16,6 +17,12 @@ type CreditState = {
plan: PlanType
}
+type UserCreditRecord = {
+ plan: PlanType
+ used: number
+ resetDate: Date | null
+}
+
export function getFreePlanCreditCap() {
return PLAN_MONTHLY_CREDIT_CAPS.free
}
@@ -24,61 +31,103 @@ export function getPlanCreditCap(plan: PlanType): number {
return PLAN_MONTHLY_CREDIT_CAPS[plan]
}
-export async function getUserCreditsRemaining(userId: string): Promise {
- const nextResetFromNow = () => {
- const date = new Date()
- date.setDate(date.getDate() + RESET_INTERVAL_DAYS)
- return date.toISOString()
+function getNextResetDate(from: Date = new Date()) {
+ const date = new Date(from)
+ date.setDate(date.getDate() + RESET_INTERVAL_DAYS)
+ return date
+}
+
+function normalizePlan(plan: string | null | undefined): PlanType {
+ if (plan === 'pro' || plan === 'plus') {
+ return plan
}
- try {
- const { data, error } = await supabaseServer
- .from('users')
- .select('subscription_plan, free_plan_credits_used, free_plan_credits_reset_date')
- .eq('id', userId)
- .single()
+ return 'free'
+}
- if (error || !data) {
- return { used: 0, remaining: PLAN_MONTHLY_CREDIT_CAPS.free, total: PLAN_MONTHLY_CREDIT_CAPS.free, resetDate: nextResetFromNow(), plan: 'free' }
- }
+async function getUserCreditRecord(userId: string): Promise {
+ const { data, error } = await supabaseServer
+ .from('users')
+ .select('subscription_plan, free_plan_credits_used, free_plan_credits_reset_date')
+ .eq('id', userId)
+ .maybeSingle()
- const plan = (data.subscription_plan || 'free') as PlanType
- const total = getPlanCreditCap(plan)
+ if (error || !data) {
+ return null
+ }
+
+ return {
+ plan: normalizePlan(data.subscription_plan),
+ used: Math.max(0, Number(data.free_plan_credits_used || 0)),
+ resetDate: data.free_plan_credits_reset_date ? new Date(data.free_plan_credits_reset_date) : null,
+ }
+}
+async function getUserPlan(userId: string): Promise {
+ const { data, error } = await supabaseServer
+ .from('users')
+ .select('subscription_plan')
+ .eq('id', userId)
+ .maybeSingle()
+
+ if (error || !data) {
+ return 'free'
+ }
+
+ return normalizePlan(data.subscription_plan)
+}
+
+async function getSessionCreditUsage(userId: string, windowStart: Date): Promise {
+ const { data, error } = await supabaseServer
+ .from('chat_sessions')
+ .select('token_usage')
+ .eq('user_id', userId)
+ .gte('created_at', windowStart.toISOString())
+
+ if (error || !data) {
+ return 0
+ }
+
+ return data.reduce((sum: number, row: { token_usage?: number | null }) => sum + Math.max(0, Number(row.token_usage || 0)), 0)
+}
+
+export async function getUserCreditsRemaining(userId: string): Promise {
+ try {
const now = new Date()
- const resetDate = data.free_plan_credits_reset_date ? new Date(data.free_plan_credits_reset_date) : null
- if (!resetDate || now > resetDate) {
- return { used: 0, remaining: total, total, resetDate: nextResetFromNow(), plan }
- }
+ const record = await getUserCreditRecord(userId)
+ const plan = record?.plan ?? await getUserPlan(userId)
+ const total = getPlanCreditCap(plan)
- const used = data.free_plan_credits_used || 0
+ const activeResetDate = record?.resetDate && now <= record.resetDate
+ ? record.resetDate
+ : getNextResetDate(now)
+ const windowStart = new Date(activeResetDate.getTime() - RESET_INTERVAL_MS)
+ const sessionUsage = await getSessionCreditUsage(userId, windowStart)
+ const storedUsage = record?.resetDate && now <= record.resetDate ? record.used : 0
+ const used = Math.max(storedUsage, sessionUsage)
const remaining = Math.max(0, total - used)
- return { used, remaining, total, resetDate: resetDate.toISOString(), plan }
+ return { used, remaining, total, resetDate: activeResetDate.toISOString(), plan }
} catch (error) {
- return { used: 0, remaining: PLAN_MONTHLY_CREDIT_CAPS.free, total: PLAN_MONTHLY_CREDIT_CAPS.free, resetDate: nextResetFromNow(), plan: 'free' }
+ const resetDate = getNextResetDate().toISOString()
+ return { used: 0, remaining: PLAN_MONTHLY_CREDIT_CAPS.free, total: PLAN_MONTHLY_CREDIT_CAPS.free, resetDate, plan: 'free' }
}
}
export async function incrementUserCredits(userId: string, cost: number): Promise {
try {
- const { data, error } = await supabaseServer
- .from('users')
- .select('subscription_plan, free_plan_credits_used, free_plan_credits_reset_date')
- .eq('id', userId)
- .single()
+ const record = await getUserCreditRecord(userId)
- if (error || !data) return false
+ if (!record) return true
const now = new Date()
- const resetDate = data.free_plan_credits_reset_date ? new Date(data.free_plan_credits_reset_date) : null
+ const resetDate = record.resetDate
- let used = data.free_plan_credits_used || 0
+ let used = record.used
const updatePayload: { free_plan_credits_used: number; free_plan_credits_reset_date?: string } = { free_plan_credits_used: used }
if (!resetDate || now > resetDate) {
used = 0
- const nextReset = new Date()
- nextReset.setDate(nextReset.getDate() + RESET_INTERVAL_DAYS)
+ const nextReset = getNextResetDate(now)
updatePayload.free_plan_credits_reset_date = nextReset.toISOString()
}
@@ -91,6 +140,6 @@ export async function incrementUserCredits(userId: string, cost: number): Promis
return !updateError
} catch (error) {
- return false
+ return true
}
}