mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
feat(usage-indicator): added ability to see current usage (#925)
* feat(usage-indicator): added ability to see current usage * feat(billing): added billing ennabled flag for usage indicator, enforcement of billing usage --------- Co-authored-by: waleedlatif1 <walif6@gmail.com>
This commit is contained in:
@@ -245,6 +245,8 @@ describe('Chat API Route', () => {
|
||||
NODE_ENV: 'development',
|
||||
NEXT_PUBLIC_APP_URL: 'http://localhost:3000',
|
||||
},
|
||||
isTruthy: (value: string | boolean | number | undefined) =>
|
||||
typeof value === 'string' ? value === 'true' || value === '1' : Boolean(value),
|
||||
}))
|
||||
|
||||
const validData = {
|
||||
@@ -287,6 +289,8 @@ describe('Chat API Route', () => {
|
||||
NODE_ENV: 'development',
|
||||
NEXT_PUBLIC_APP_URL: 'http://localhost:3000',
|
||||
},
|
||||
isTruthy: (value: string | boolean | number | undefined) =>
|
||||
typeof value === 'string' ? value === 'true' || value === '1' : Boolean(value),
|
||||
}))
|
||||
|
||||
const validData = {
|
||||
|
||||
@@ -30,6 +30,8 @@ vi.mock('@/lib/env', () => ({
|
||||
env: {
|
||||
OPENAI_API_KEY: 'test-api-key',
|
||||
},
|
||||
isTruthy: (value: string | boolean | number | undefined) =>
|
||||
typeof value === 'string' ? value === 'true' || value === '1' : Boolean(value),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/documents/utils', () => ({
|
||||
|
||||
@@ -15,7 +15,11 @@ vi.mock('drizzle-orm', () => ({
|
||||
sql: (strings: TemplateStringsArray, ...expr: any[]) => ({ strings, expr }),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/env', () => ({ env: { OPENAI_API_KEY: 'test-key' } }))
|
||||
vi.mock('@/lib/env', () => ({
|
||||
env: { OPENAI_API_KEY: 'test-key' },
|
||||
isTruthy: (value: string | boolean | number | undefined) =>
|
||||
typeof value === 'string' ? value === 'true' || value === '1' : Boolean(value),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/documents/utils', () => ({
|
||||
retryWithExponentialBackoff: (fn: any) => fn(),
|
||||
|
||||
@@ -89,6 +89,10 @@
|
||||
|
||||
/* Base Component Properties */
|
||||
--base-muted-foreground: #737373;
|
||||
|
||||
/* Gradient Colors */
|
||||
--gradient-primary: 263 85% 70%; /* More vibrant purple */
|
||||
--gradient-secondary: 336 95% 65%; /* More vibrant pink */
|
||||
}
|
||||
|
||||
/* Dark Mode Theme */
|
||||
@@ -145,6 +149,10 @@
|
||||
|
||||
/* Base Component Properties */
|
||||
--base-muted-foreground: #a3a3a3;
|
||||
|
||||
/* Gradient Colors - Adjusted for dark mode */
|
||||
--gradient-primary: 263 90% 75%; /* More vibrant purple for dark mode */
|
||||
--gradient-secondary: 336 100% 72%; /* More vibrant pink for dark mode */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,6 +333,13 @@ input[type="search"]::-ms-clear {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Gradient Text Utility - Use with Tailwind gradient directions */
|
||||
.gradient-text {
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
/* Animation Classes */
|
||||
.animate-pulse-ring {
|
||||
animation: pulse-ring 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
|
||||
@@ -3,7 +3,9 @@ export { FolderTree } from './folder-tree/folder-tree'
|
||||
export { HelpModal } from './help-modal/help-modal'
|
||||
export { LogsFilters } from './logs-filters/logs-filters'
|
||||
export { SettingsModal } from './settings-modal/settings-modal'
|
||||
export { SubscriptionModal } from './subscription-modal/subscription-modal'
|
||||
export { Toolbar } from './toolbar/toolbar'
|
||||
export { UsageIndicator } from './usage-indicator/usage-indicator'
|
||||
export { WorkflowContextMenu } from './workflow-context-menu/workflow-context-menu'
|
||||
export { WorkflowList } from './workflow-list/workflow-list'
|
||||
export { WorkspaceHeader } from './workspace-header/workspace-header'
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
UserCircle,
|
||||
Users,
|
||||
} from 'lucide-react'
|
||||
import { isDev } from '@/lib/environment'
|
||||
import { isBillingEnabled } from '@/lib/environment'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useSubscriptionStore } from '@/stores/subscription/store'
|
||||
|
||||
@@ -40,7 +40,7 @@ type NavigationItem = {
|
||||
| 'privacy'
|
||||
label: string
|
||||
icon: React.ComponentType<{ className?: string }>
|
||||
hideInDev?: boolean
|
||||
hideWhenBillingDisabled?: boolean
|
||||
requiresTeam?: boolean
|
||||
}
|
||||
|
||||
@@ -79,13 +79,13 @@ const allNavigationItems: NavigationItem[] = [
|
||||
id: 'subscription',
|
||||
label: 'Subscription',
|
||||
icon: CreditCard,
|
||||
hideInDev: true,
|
||||
hideWhenBillingDisabled: true,
|
||||
},
|
||||
{
|
||||
id: 'team',
|
||||
label: 'Team',
|
||||
icon: Users,
|
||||
hideInDev: true,
|
||||
hideWhenBillingDisabled: true,
|
||||
requiresTeam: true,
|
||||
},
|
||||
]
|
||||
@@ -99,7 +99,7 @@ export function SettingsNavigation({
|
||||
const subscription = getSubscriptionStatus()
|
||||
|
||||
const navigationItems = allNavigationItems.filter((item) => {
|
||||
if (item.hideInDev && isDev) {
|
||||
if (item.hideWhenBillingDisabled && !isBillingEnabled) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { X } from 'lucide-react'
|
||||
import { Button, Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui'
|
||||
import { client } from '@/lib/auth-client'
|
||||
import { isBillingEnabled } from '@/lib/environment'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
@@ -82,7 +82,14 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
|
||||
}
|
||||
}, [onOpenChange])
|
||||
|
||||
const isSubscriptionEnabled = !!client.subscription
|
||||
// Redirect away from billing tabs if billing is disabled
|
||||
useEffect(() => {
|
||||
if (!isBillingEnabled && (activeSection === 'subscription' || activeSection === 'team')) {
|
||||
setActiveSection('general')
|
||||
}
|
||||
}, [activeSection])
|
||||
|
||||
const isSubscriptionEnabled = isBillingEnabled
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
@@ -134,9 +141,11 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
|
||||
<Subscription onOpenChange={onOpenChange} />
|
||||
</div>
|
||||
)}
|
||||
<div className={cn('h-full', activeSection === 'team' ? 'block' : 'hidden')}>
|
||||
<TeamManagement />
|
||||
</div>
|
||||
{isBillingEnabled && (
|
||||
<div className={cn('h-full', activeSection === 'team' ? 'block' : 'hidden')}>
|
||||
<TeamManagement />
|
||||
</div>
|
||||
)}
|
||||
<div className={cn('h-full', activeSection === 'privacy' ? 'block' : 'hidden')}>
|
||||
<Privacy />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import {
|
||||
Building2,
|
||||
Check,
|
||||
Clock,
|
||||
Database,
|
||||
DollarSign,
|
||||
HeadphonesIcon,
|
||||
Infinity as InfinityIcon,
|
||||
MessageSquare,
|
||||
Server,
|
||||
Users,
|
||||
Workflow,
|
||||
Zap,
|
||||
} from 'lucide-react'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogContent,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useSession, useSubscription } from '@/lib/auth-client'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useOrganizationStore } from '@/stores/organization'
|
||||
import { useSubscriptionStore } from '@/stores/subscription/store'
|
||||
|
||||
const logger = createLogger('SubscriptionModal')
|
||||
|
||||
interface SubscriptionModalProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
interface PlanFeature {
|
||||
text: string
|
||||
included: boolean
|
||||
icon?: any
|
||||
}
|
||||
|
||||
export function SubscriptionModal({ open, onOpenChange }: SubscriptionModalProps) {
|
||||
const { data: session } = useSession()
|
||||
const betterAuthSubscription = useSubscription()
|
||||
const { activeOrganization } = useOrganizationStore()
|
||||
const { loadData, getSubscriptionStatus, isLoading } = useSubscriptionStore()
|
||||
|
||||
// Load subscription data when modal opens
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
loadData()
|
||||
}
|
||||
}, [open, loadData])
|
||||
|
||||
const subscription = getSubscriptionStatus()
|
||||
|
||||
const handleUpgrade = useCallback(
|
||||
async (targetPlan: 'pro' | 'team') => {
|
||||
if (!session?.user?.id) return
|
||||
|
||||
const subscriptionData = useSubscriptionStore.getState().subscriptionData
|
||||
const currentSubscriptionId = subscriptionData?.stripeSubscriptionId
|
||||
|
||||
let referenceId = session.user.id
|
||||
if (subscription.isTeam && activeOrganization?.id) {
|
||||
referenceId = activeOrganization.id
|
||||
}
|
||||
|
||||
const currentUrl = window.location.origin + window.location.pathname
|
||||
|
||||
try {
|
||||
const upgradeParams: any = {
|
||||
plan: targetPlan,
|
||||
referenceId,
|
||||
successUrl: currentUrl,
|
||||
cancelUrl: currentUrl,
|
||||
seats: targetPlan === 'team' ? 1 : undefined,
|
||||
}
|
||||
|
||||
if (currentSubscriptionId) {
|
||||
upgradeParams.subscriptionId = currentSubscriptionId
|
||||
}
|
||||
|
||||
await betterAuthSubscription.upgrade(upgradeParams)
|
||||
} catch (error) {
|
||||
logger.error('Failed to initiate subscription upgrade:', error)
|
||||
alert('Failed to initiate upgrade. Please try again or contact support.')
|
||||
}
|
||||
},
|
||||
[session?.user?.id, subscription.isTeam, activeOrganization?.id, betterAuthSubscription]
|
||||
)
|
||||
|
||||
const handleContactUs = () => {
|
||||
window.open('https://form.typeform.com/to/jqCO12pF', '_blank')
|
||||
}
|
||||
|
||||
// Define all 4 plans
|
||||
const plans = [
|
||||
{
|
||||
name: 'Free',
|
||||
price: '$0',
|
||||
description: '',
|
||||
features: [
|
||||
{ text: '$10 free inference credit', included: true, icon: DollarSign },
|
||||
{ text: '10 runs per minute (sync)', included: true, icon: Zap },
|
||||
{ text: '50 runs per minute (async)', included: true, icon: Clock },
|
||||
{ text: '7-day log retention', included: true, icon: Database },
|
||||
],
|
||||
isActive: subscription.isFree,
|
||||
action: null, // No action for free plan
|
||||
},
|
||||
{
|
||||
name: 'Pro',
|
||||
price: '$20',
|
||||
description: '/month',
|
||||
features: [
|
||||
{ text: '25 runs per minute (sync)', included: true, icon: Zap },
|
||||
{ text: '200 runs per minute (async)', included: true, icon: Clock },
|
||||
{ text: 'Unlimited workspaces', included: true, icon: Building2 },
|
||||
{ text: 'Unlimited workflows', included: true, icon: Workflow },
|
||||
{ text: 'Unlimited invites', included: true, icon: Users },
|
||||
{ text: 'Unlimited log retention', included: true, icon: Database },
|
||||
],
|
||||
isActive: subscription.isPro && !subscription.isTeam,
|
||||
action: subscription.isFree ? () => handleUpgrade('pro') : null,
|
||||
},
|
||||
{
|
||||
name: 'Team',
|
||||
price: '$40',
|
||||
description: '/month',
|
||||
features: [
|
||||
{ text: '75 runs per minute (sync)', included: true, icon: Zap },
|
||||
{ text: '500 runs per minute (async)', included: true, icon: Clock },
|
||||
{ text: 'Everything in Pro', included: true, icon: InfinityIcon },
|
||||
{ text: 'Dedicated Slack channel', included: true, icon: MessageSquare },
|
||||
],
|
||||
isActive: subscription.isTeam,
|
||||
action: !subscription.isTeam ? () => handleUpgrade('team') : null,
|
||||
},
|
||||
{
|
||||
name: 'Enterprise',
|
||||
price: '',
|
||||
description: '',
|
||||
features: [
|
||||
{ text: 'Custom rate limits', included: true, icon: Zap },
|
||||
{ text: 'Enterprise hosting license', included: true, icon: Server },
|
||||
{ text: 'Custom enterprise support', included: true, icon: HeadphonesIcon },
|
||||
],
|
||||
isActive: subscription.isEnterprise,
|
||||
action: handleContactUs,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
<AlertDialogContent className='!fixed !inset-0 !m-0 data-[state=open]:!translate-x-0 data-[state=open]:!translate-y-0 flex h-full max-h-full w-full max-w-full flex-col gap-0 rounded-none border-0 p-0'>
|
||||
<AlertDialogHeader className='flex-shrink-0 px-6 py-5'>
|
||||
<AlertDialogTitle className='font-medium text-lg'>Upgrade your plan</AlertDialogTitle>
|
||||
</AlertDialogHeader>
|
||||
|
||||
<div className='flex min-h-0 flex-1 items-center justify-center overflow-hidden px-8 pb-8'>
|
||||
<div className='flex w-full max-w-4xl flex-col gap-6'>
|
||||
{/* Main Plans Grid - Free, Pro, Team */}
|
||||
<div className='grid grid-cols-1 gap-6 md:grid-cols-3'>
|
||||
{plans.slice(0, 3).map((plan) => (
|
||||
<div
|
||||
key={plan.name}
|
||||
className={cn('relative flex flex-col rounded-[10px] border p-6')}
|
||||
>
|
||||
{/* Plan Header */}
|
||||
<div className='mb-6'>
|
||||
<h3 className='mb-3 font-semibold text-lg'>{plan.name}</h3>
|
||||
<div className='flex items-baseline'>
|
||||
<span className='font-semibold text-3xl'>{plan.price}</span>
|
||||
{plan.description && (
|
||||
<span className='ml-1 text-muted-foreground text-sm'>
|
||||
{plan.description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Features */}
|
||||
<ul className='mb-6 flex-1 space-y-3'>
|
||||
{plan.features.map((feature, index) => (
|
||||
<li key={index} className='flex items-start gap-2 text-sm'>
|
||||
{feature.icon ? (
|
||||
<feature.icon className='mt-0.5 h-4 w-4 flex-shrink-0 text-muted-foreground' />
|
||||
) : (
|
||||
<Check className='mt-0.5 h-4 w-4 flex-shrink-0 text-green-500' />
|
||||
)}
|
||||
<span className='text-muted-foreground'>{feature.text}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{/* Action Button */}
|
||||
<div className='mt-auto'>
|
||||
{plan.isActive ? (
|
||||
<Button variant='secondary' className='w-full rounded-[8px]' disabled>
|
||||
Current plan
|
||||
</Button>
|
||||
) : plan.action ? (
|
||||
<Button
|
||||
onClick={plan.action}
|
||||
className='w-full rounded-[8px]'
|
||||
variant='default'
|
||||
>
|
||||
Upgrade
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant='outline' className='w-full rounded-[8px]' disabled>
|
||||
{plan.name === 'Free' ? 'Basic plan' : 'Upgrade'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Enterprise Plan - Full Width */}
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex flex-col rounded-[10px] border p-6 md:flex-row md:items-center md:justify-between',
|
||||
plans[3].isActive && 'border-gray-400'
|
||||
)}
|
||||
>
|
||||
{/* Left Side - Plan Info */}
|
||||
<div className='mb-4 md:mb-0'>
|
||||
<h3 className='mb-2 font-semibold text-lg'>{plans[3].name}</h3>
|
||||
<p className='mb-3 text-muted-foreground text-sm'>
|
||||
Custom solutions tailored to your enterprise needs
|
||||
</p>
|
||||
<div className='flex items-center gap-4'>
|
||||
{plans[3].features.map((feature, index) => (
|
||||
<div key={index} className='flex items-center gap-4'>
|
||||
<div className='flex items-center gap-2 text-sm'>
|
||||
{feature.icon ? (
|
||||
<feature.icon className='h-4 w-4 flex-shrink-0 text-muted-foreground' />
|
||||
) : (
|
||||
<Check className='h-4 w-4 flex-shrink-0 text-green-500' />
|
||||
)}
|
||||
<span className='text-muted-foreground'>{feature.text}</span>
|
||||
</div>
|
||||
{index < plans[3].features.length - 1 && (
|
||||
<div className='h-4 w-px bg-border' />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Side - Button */}
|
||||
<div className='md:ml-auto md:w-[200px]'>
|
||||
{plans[3].isActive ? (
|
||||
<Button variant='secondary' className='w-full rounded-[8px]' disabled>
|
||||
Current plan
|
||||
</Button>
|
||||
) : plans[3].action ? (
|
||||
<Button
|
||||
onClick={plans[3].action}
|
||||
className='w-full rounded-[8px]'
|
||||
variant='default'
|
||||
>
|
||||
Contact us
|
||||
</Button>
|
||||
) : (
|
||||
<Button className='w-full rounded-[8px]' variant='default' disabled>
|
||||
Contact us
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import { Badge, Progress, Skeleton } from '@/components/ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useSubscriptionStore } from '@/stores/subscription/store'
|
||||
|
||||
// Constants for reusable styles
|
||||
const GRADIENT_BADGE_STYLES =
|
||||
'gradient-text h-[1.125rem] rounded-[6px] border-gradient-primary/20 bg-gradient-to-b from-gradient-primary via-gradient-secondary to-gradient-primary px-2 py-0 font-medium text-xs'
|
||||
const GRADIENT_TEXT_STYLES =
|
||||
'gradient-text bg-gradient-to-b from-gradient-primary via-gradient-secondary to-gradient-primary'
|
||||
const CONTAINER_STYLES =
|
||||
'pointer-events-auto flex-shrink-0 rounded-[10px] border bg-background px-3 py-2.5 shadow-xs cursor-pointer transition-colors hover:bg-muted/50'
|
||||
|
||||
// Plan name mapping
|
||||
const PLAN_NAMES = {
|
||||
enterprise: 'Enterprise',
|
||||
team: 'Team',
|
||||
pro: 'Pro',
|
||||
free: 'Free',
|
||||
} as const
|
||||
|
||||
interface UsageIndicatorProps {
|
||||
onClick?: (badgeType: 'add' | 'upgrade') => void
|
||||
}
|
||||
|
||||
export function UsageIndicator({ onClick }: UsageIndicatorProps) {
|
||||
const { loadData, getUsage, getSubscriptionStatus, isLoading } = useSubscriptionStore()
|
||||
|
||||
// Load subscription data on mount
|
||||
useEffect(() => {
|
||||
loadData()
|
||||
}, [loadData])
|
||||
|
||||
const usage = getUsage()
|
||||
const subscription = getSubscriptionStatus()
|
||||
|
||||
// Show skeleton while loading
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className={CONTAINER_STYLES} onClick={() => onClick?.('upgrade')}>
|
||||
<div className='space-y-2'>
|
||||
{/* Plan and usage info skeleton */}
|
||||
<div className='flex items-center justify-between'>
|
||||
<Skeleton className='h-5 w-12' />
|
||||
<Skeleton className='h-4 w-20' />
|
||||
</div>
|
||||
|
||||
{/* Progress Bar skeleton */}
|
||||
<Skeleton className='h-2 w-full' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Calculate progress percentage (capped at 100)
|
||||
const progressPercentage = Math.min(usage.percentUsed, 100)
|
||||
|
||||
// Determine plan type
|
||||
const planType = subscription.isEnterprise
|
||||
? 'enterprise'
|
||||
: subscription.isTeam
|
||||
? 'team'
|
||||
: subscription.isPro
|
||||
? 'pro'
|
||||
: 'free'
|
||||
|
||||
// Determine badge to show
|
||||
const showAddBadge = planType !== 'free' && usage.percentUsed >= 85
|
||||
const badgeText = planType === 'free' ? 'Upgrade' : 'Add'
|
||||
const badgeType = planType === 'free' ? 'upgrade' : 'add'
|
||||
|
||||
return (
|
||||
<div className={CONTAINER_STYLES} onClick={() => onClick?.(badgeType)}>
|
||||
<div className='space-y-2'>
|
||||
{/* Plan and usage info */}
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<span
|
||||
className={cn(
|
||||
'font-medium text-sm',
|
||||
planType === 'free' ? 'text-foreground' : GRADIENT_TEXT_STYLES
|
||||
)}
|
||||
>
|
||||
{PLAN_NAMES[planType]}
|
||||
</span>
|
||||
{(showAddBadge || planType === 'free') && (
|
||||
<Badge className={GRADIENT_BADGE_STYLES}>{badgeText}</Badge>
|
||||
)}
|
||||
</div>
|
||||
<span className='text-muted-foreground text-xs tabular-nums'>
|
||||
${usage.current.toFixed(2)} / ${usage.limit}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<Progress value={progressPercentage} className='h-2' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -171,11 +171,6 @@ const PermissionsTableSkeleton = React.memo(() => (
|
||||
|
||||
PermissionsTableSkeleton.displayName = 'PermissionsTableSkeleton'
|
||||
|
||||
const getStatusBadgeStyles = (status: 'sent' | 'member' | 'modified'): string => {
|
||||
// Use consistent gray styling for all statuses to align with modal design
|
||||
return 'inline-flex items-center rounded-[8px] bg-gray-100 px-2 py-1 text-xs font-medium text-gray-700 dark:bg-gray-800 dark:text-gray-300'
|
||||
}
|
||||
|
||||
const PermissionsTable = ({
|
||||
userPermissions,
|
||||
onPermissionChange,
|
||||
@@ -314,12 +309,12 @@ const PermissionsTable = ({
|
||||
<div className='flex items-center gap-2'>
|
||||
<span className='font-medium text-card-foreground text-sm'>{user.email}</span>
|
||||
{isPendingInvitation && (
|
||||
<span className='inline-flex items-center rounded-[8px] bg-blue-100 px-2 py-1 font-medium text-blue-700 text-xs dark:bg-blue-900/30 dark:text-blue-400'>
|
||||
<span className='inline-flex items-center rounded-[8px] bg-gray-100 px-2 py-1 font-medium text-gray-700 text-xs dark:bg-gray-800 dark:text-gray-300'>
|
||||
Sent
|
||||
</span>
|
||||
)}
|
||||
{hasChanges && (
|
||||
<span className='inline-flex items-center rounded-[8px] bg-orange-100 px-2 py-1 font-medium text-orange-700 text-xs dark:bg-orange-900/30 dark:text-orange-400'>
|
||||
<span className='inline-flex items-center rounded-[8px] bg-gray-100 px-2 py-1 font-medium text-gray-700 text-xs dark:bg-gray-800 dark:text-gray-300'>
|
||||
Modified
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { HelpCircle, LibraryBig, ScrollText, Search, Settings, Shapes } from 'lu
|
||||
import { useParams, usePathname, useRouter } from 'next/navigation'
|
||||
import { Button, ScrollArea, Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui'
|
||||
import { useSession } from '@/lib/auth-client'
|
||||
import { isBillingEnabled } from '@/lib/environment'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { generateWorkspaceName } from '@/lib/naming'
|
||||
import { cn } from '@/lib/utils'
|
||||
@@ -16,7 +17,9 @@ import {
|
||||
HelpModal,
|
||||
LogsFilters,
|
||||
SettingsModal,
|
||||
SubscriptionModal,
|
||||
Toolbar,
|
||||
UsageIndicator,
|
||||
WorkspaceHeader,
|
||||
WorkspaceSelector,
|
||||
} from '@/app/workspace/[workspaceId]/w/components/sidebar/components'
|
||||
@@ -141,8 +144,9 @@ const SIDEBAR_HEIGHTS = {
|
||||
WORKSPACE_HEADER: 48, // estimated height of workspace header
|
||||
SEARCH: 48, // h-12
|
||||
WORKFLOW_SELECTOR: 212, // h-[212px]
|
||||
NAVIGATION: 48, // h-12 buttons
|
||||
NAVIGATION: 42, // h-[42px] buttons
|
||||
WORKSPACE_SELECTOR: 171, // optimized height: p-2(16) + h-[104px](104) + mt-2(8) + border-t(1) + pt-2(8) + h-8(32) = 169px
|
||||
USAGE_INDICATOR: 58, // actual height: border(2) + py-2.5(20) + content(~36) = 58px
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -688,6 +692,7 @@ export function Sidebar() {
|
||||
const [showHelp, setShowHelp] = useState(false)
|
||||
const [showInviteMembers, setShowInviteMembers] = useState(false)
|
||||
const [showSearchModal, setShowSearchModal] = useState(false)
|
||||
const [showSubscriptionModal, setShowSubscriptionModal] = useState(false)
|
||||
|
||||
// Separate regular workflows from temporary marketplace workflows
|
||||
const { regularWorkflows, tempWorkflows } = useMemo(() => {
|
||||
@@ -1008,7 +1013,7 @@ export function Sidebar() {
|
||||
}`}
|
||||
style={{
|
||||
top: `${toolbarTop}px`,
|
||||
bottom: `${navigationBottom + 42 + 12}px`, // Navigation height + gap
|
||||
bottom: `${navigationBottom + SIDEBAR_HEIGHTS.NAVIGATION + SIDEBAR_GAP + (isBillingEnabled ? SIDEBAR_HEIGHTS.USAGE_INDICATOR + SIDEBAR_GAP : 0)}px`, // Navigation height + gap + UsageIndicator height + gap (if billing enabled)
|
||||
}}
|
||||
>
|
||||
<Toolbar
|
||||
@@ -1024,12 +1029,36 @@ export function Sidebar() {
|
||||
}`}
|
||||
style={{
|
||||
top: `${toolbarTop}px`,
|
||||
bottom: `${navigationBottom + 42 + 12}px`, // Navigation height + gap
|
||||
bottom: `${navigationBottom + SIDEBAR_HEIGHTS.NAVIGATION + SIDEBAR_GAP + (isBillingEnabled ? SIDEBAR_HEIGHTS.USAGE_INDICATOR + SIDEBAR_GAP : 0)}px`, // Navigation height + gap + UsageIndicator height + gap (if billing enabled)
|
||||
}}
|
||||
>
|
||||
<LogsFilters />
|
||||
</div>
|
||||
|
||||
{/* Floating Usage Indicator - Only shown when billing enabled */}
|
||||
{isBillingEnabled && (
|
||||
<div
|
||||
className='pointer-events-auto fixed left-4 z-50 w-56'
|
||||
style={{ bottom: `${navigationBottom + SIDEBAR_HEIGHTS.NAVIGATION + SIDEBAR_GAP}px` }} // Navigation height + gap
|
||||
>
|
||||
<UsageIndicator
|
||||
onClick={(badgeType) => {
|
||||
if (badgeType === 'add') {
|
||||
// Open settings modal on subscription tab
|
||||
if (typeof window !== 'undefined') {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('open-settings', { detail: { tab: 'subscription' } })
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Open subscription modal for upgrade
|
||||
setShowSubscriptionModal(true)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Floating Navigation - Always visible */}
|
||||
<div
|
||||
className='pointer-events-auto fixed left-4 z-50 w-56'
|
||||
@@ -1046,6 +1075,7 @@ export function Sidebar() {
|
||||
<SettingsModal open={showSettings} onOpenChange={setShowSettings} />
|
||||
<HelpModal open={showHelp} onOpenChange={setShowHelp} />
|
||||
<InviteModal open={showInviteMembers} onOpenChange={setShowInviteMembers} />
|
||||
<SubscriptionModal open={showSubscriptionModal} onOpenChange={setShowSubscriptionModal} />
|
||||
<SearchModal
|
||||
open={showSearchModal}
|
||||
onOpenChange={setShowSearchModal}
|
||||
|
||||
@@ -23,7 +23,7 @@ import { getBaseURL } from '@/lib/auth-client'
|
||||
import { DEFAULT_FREE_CREDITS } from '@/lib/billing/constants'
|
||||
import { quickValidateEmail } from '@/lib/email/validation'
|
||||
import { env, isTruthy } from '@/lib/env'
|
||||
import { isProd } from '@/lib/environment'
|
||||
import { isBillingEnabled, isProd } from '@/lib/environment'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { getEmailDomain } from '@/lib/urls/utils'
|
||||
import { db } from '@/db'
|
||||
@@ -1160,8 +1160,8 @@ export const auth = betterAuth({
|
||||
},
|
||||
],
|
||||
}),
|
||||
// Only include the Stripe plugin in production
|
||||
...(isProd && stripeClient
|
||||
// Only include the Stripe plugin when billing is enabled
|
||||
...(isBillingEnabled && stripeClient
|
||||
? [
|
||||
stripe({
|
||||
stripeClient,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { getUserUsageLimit } from '@/lib/billing/core/usage'
|
||||
import { isProd } from '@/lib/environment'
|
||||
import { isBillingEnabled } from '@/lib/environment'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { db } from '@/db'
|
||||
import { userStats } from '@/db/schema'
|
||||
@@ -24,8 +24,8 @@ interface UsageData {
|
||||
*/
|
||||
export async function checkUsageStatus(userId: string): Promise<UsageData> {
|
||||
try {
|
||||
// In development, always return permissive limits
|
||||
if (!isProd) {
|
||||
// If billing is disabled, always return permissive limits
|
||||
if (!isBillingEnabled) {
|
||||
// Get actual usage from the database for display purposes
|
||||
const statsRecords = await db.select().from(userStats).where(eq(userStats.userId, userId))
|
||||
const currentUsage =
|
||||
@@ -115,8 +115,8 @@ export async function checkUsageStatus(userId: string): Promise<UsageData> {
|
||||
*/
|
||||
export async function checkAndNotifyUsage(userId: string): Promise<void> {
|
||||
try {
|
||||
// Skip usage notifications in development
|
||||
if (!isProd) {
|
||||
// Skip usage notifications if billing is disabled
|
||||
if (!isBillingEnabled) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -182,8 +182,8 @@ export async function checkServerSideUsageLimits(userId: string): Promise<{
|
||||
message?: string
|
||||
}> {
|
||||
try {
|
||||
// In development, always allow execution
|
||||
if (!isProd) {
|
||||
// If billing is disabled, always allow execution
|
||||
if (!isBillingEnabled) {
|
||||
return {
|
||||
isExceeded: false,
|
||||
currentUsage: 0,
|
||||
|
||||
@@ -10,6 +10,8 @@ vi.mock('@/lib/env', () => ({
|
||||
env: {
|
||||
BETTER_AUTH_SECRET: 'test-secret-key',
|
||||
},
|
||||
isTruthy: (value: string | boolean | number | undefined) =>
|
||||
typeof value === 'string' ? value === 'true' || value === '1' : Boolean(value),
|
||||
}))
|
||||
|
||||
describe('unsubscribe utilities', () => {
|
||||
|
||||
@@ -16,192 +16,193 @@ export const env = createEnv({
|
||||
|
||||
server: {
|
||||
// Core Database & Authentication
|
||||
DATABASE_URL: z.string().url(), // Primary database connection string
|
||||
BETTER_AUTH_URL: z.string().url(), // Base URL for Better Auth service
|
||||
BETTER_AUTH_SECRET: z.string().min(32), // Secret key for Better Auth JWT signing
|
||||
DISABLE_REGISTRATION: z.boolean().optional(), // Flag to disable new user registration
|
||||
ALLOWED_LOGIN_EMAILS: z.string().optional(), // Comma-separated list of allowed email addresses for login
|
||||
ALLOWED_LOGIN_DOMAINS: z.string().optional(), // Comma-separated list of allowed email domains for login
|
||||
ENCRYPTION_KEY: z.string().min(32), // Key for encrypting sensitive data
|
||||
INTERNAL_API_SECRET: z.string().min(32), // Secret for internal API authentication
|
||||
SIM_AGENT_API_KEY: z.string().min(1).optional(), // Secret for internal sim agent API authentication
|
||||
SIM_AGENT_API_URL: z.string().url().optional(), // URL for internal sim agent API
|
||||
DATABASE_URL: z.string().url(), // Primary database connection string
|
||||
BETTER_AUTH_URL: z.string().url(), // Base URL for Better Auth service
|
||||
BETTER_AUTH_SECRET: z.string().min(32), // Secret key for Better Auth JWT signing
|
||||
DISABLE_REGISTRATION: z.boolean().optional(), // Flag to disable new user registration
|
||||
ALLOWED_LOGIN_EMAILS: z.string().optional(), // Comma-separated list of allowed email addresses for login
|
||||
ALLOWED_LOGIN_DOMAINS: z.string().optional(), // Comma-separated list of allowed email domains for login
|
||||
ENCRYPTION_KEY: z.string().min(32), // Key for encrypting sensitive data
|
||||
INTERNAL_API_SECRET: z.string().min(32), // Secret for internal API authentication
|
||||
SIM_AGENT_API_KEY: z.string().min(1).optional(), // Secret for internal sim agent API authentication
|
||||
SIM_AGENT_API_URL: z.string().url().optional(), // URL for internal sim agent API
|
||||
|
||||
// Database & Storage
|
||||
POSTGRES_URL: z.string().url().optional(), // Alternative PostgreSQL connection string
|
||||
REDIS_URL: z.string().url().optional(), // Redis connection string for caching/sessions
|
||||
POSTGRES_URL: z.string().url().optional(), // Alternative PostgreSQL connection string
|
||||
REDIS_URL: z.string().url().optional(), // Redis connection string for caching/sessions
|
||||
|
||||
// Payment & Billing (Stripe)
|
||||
STRIPE_SECRET_KEY: z.string().min(1).optional(), // Stripe secret key for payment processing
|
||||
STRIPE_BILLING_WEBHOOK_SECRET: z.string().min(1).optional(), // Webhook secret for billing events
|
||||
STRIPE_WEBHOOK_SECRET: z.string().min(1).optional(), // General Stripe webhook secret
|
||||
STRIPE_FREE_PRICE_ID: z.string().min(1).optional(), // Stripe price ID for free tier
|
||||
FREE_TIER_COST_LIMIT: z.number().optional(), // Cost limit for free tier users
|
||||
STRIPE_PRO_PRICE_ID: z.string().min(1).optional(), // Stripe price ID for pro tier
|
||||
PRO_TIER_COST_LIMIT: z.number().optional(), // Cost limit for pro tier users
|
||||
STRIPE_TEAM_PRICE_ID: z.string().min(1).optional(), // Stripe price ID for team tier
|
||||
TEAM_TIER_COST_LIMIT: z.number().optional(), // Cost limit for team tier users
|
||||
STRIPE_ENTERPRISE_PRICE_ID: z.string().min(1).optional(), // Stripe price ID for enterprise tier
|
||||
ENTERPRISE_TIER_COST_LIMIT: z.number().optional(), // Cost limit for enterprise tier users
|
||||
// Payment & Billing
|
||||
BILLING_ENABLED: z.boolean().optional(), // Enable billing enforcement and usage tracking
|
||||
STRIPE_SECRET_KEY: z.string().min(1).optional(), // Stripe secret key for payment processing
|
||||
STRIPE_BILLING_WEBHOOK_SECRET: z.string().min(1).optional(), // Webhook secret for billing events
|
||||
STRIPE_WEBHOOK_SECRET: z.string().min(1).optional(), // General Stripe webhook secret
|
||||
STRIPE_FREE_PRICE_ID: z.string().min(1).optional(), // Stripe price ID for free tier
|
||||
FREE_TIER_COST_LIMIT: z.number().optional(), // Cost limit for free tier users
|
||||
STRIPE_PRO_PRICE_ID: z.string().min(1).optional(), // Stripe price ID for pro tier
|
||||
PRO_TIER_COST_LIMIT: z.number().optional(), // Cost limit for pro tier users
|
||||
STRIPE_TEAM_PRICE_ID: z.string().min(1).optional(), // Stripe price ID for team tier
|
||||
TEAM_TIER_COST_LIMIT: z.number().optional(), // Cost limit for team tier users
|
||||
STRIPE_ENTERPRISE_PRICE_ID: z.string().min(1).optional(), // Stripe price ID for enterprise tier
|
||||
ENTERPRISE_TIER_COST_LIMIT: z.number().optional(), // Cost limit for enterprise tier users
|
||||
|
||||
// Email & Communication
|
||||
RESEND_API_KEY: z.string().min(1).optional(), // Resend API key for transactional emails
|
||||
EMAIL_DOMAIN: z.string().min(1).optional(), // Domain for sending emails
|
||||
RESEND_API_KEY: z.string().min(1).optional(), // Resend API key for transactional emails
|
||||
EMAIL_DOMAIN: z.string().min(1).optional(), // Domain for sending emails
|
||||
|
||||
// AI/LLM Provider API Keys
|
||||
OPENAI_API_KEY: z.string().min(1).optional(), // Primary OpenAI API key
|
||||
OPENAI_API_KEY_1: z.string().min(1).optional(), // Additional OpenAI API key for load balancing
|
||||
OPENAI_API_KEY_2: z.string().min(1).optional(), // Additional OpenAI API key for load balancing
|
||||
OPENAI_API_KEY_3: z.string().min(1).optional(), // Additional OpenAI API key for load balancing
|
||||
MISTRAL_API_KEY: z.string().min(1).optional(), // Mistral AI API key
|
||||
ANTHROPIC_API_KEY_1: z.string().min(1).optional(), // Primary Anthropic Claude API key
|
||||
ANTHROPIC_API_KEY_2: z.string().min(1).optional(), // Additional Anthropic API key for load balancing
|
||||
ANTHROPIC_API_KEY_3: z.string().min(1).optional(), // Additional Anthropic API key for load balancing
|
||||
FREESTYLE_API_KEY: z.string().min(1).optional(), // Freestyle AI API key
|
||||
OLLAMA_URL: z.string().url().optional(), // Ollama local LLM server URL
|
||||
ELEVENLABS_API_KEY: z.string().min(1).optional(), // ElevenLabs API key for text-to-speech in deployed chat
|
||||
SERPER_API_KEY: z.string().min(1).optional(), // Serper API key for online search
|
||||
OPENAI_API_KEY: z.string().min(1).optional(), // Primary OpenAI API key
|
||||
OPENAI_API_KEY_1: z.string().min(1).optional(), // Additional OpenAI API key for load balancing
|
||||
OPENAI_API_KEY_2: z.string().min(1).optional(), // Additional OpenAI API key for load balancing
|
||||
OPENAI_API_KEY_3: z.string().min(1).optional(), // Additional OpenAI API key for load balancing
|
||||
MISTRAL_API_KEY: z.string().min(1).optional(), // Mistral AI API key
|
||||
ANTHROPIC_API_KEY_1: z.string().min(1).optional(), // Primary Anthropic Claude API key
|
||||
ANTHROPIC_API_KEY_2: z.string().min(1).optional(), // Additional Anthropic API key for load balancing
|
||||
ANTHROPIC_API_KEY_3: z.string().min(1).optional(), // Additional Anthropic API key for load balancing
|
||||
FREESTYLE_API_KEY: z.string().min(1).optional(), // Freestyle AI API key
|
||||
OLLAMA_URL: z.string().url().optional(), // Ollama local LLM server URL
|
||||
ELEVENLABS_API_KEY: z.string().min(1).optional(), // ElevenLabs API key for text-to-speech in deployed chat
|
||||
SERPER_API_KEY: z.string().min(1).optional(), // Serper API key for online search
|
||||
|
||||
// Azure OpenAI Configuration
|
||||
AZURE_OPENAI_ENDPOINT: z.string().url().optional(), // Azure OpenAI service endpoint
|
||||
AZURE_OPENAI_API_VERSION: z.string().optional(), // Azure OpenAI API version
|
||||
AZURE_OPENAI_ENDPOINT: z.string().url().optional(), // Azure OpenAI service endpoint
|
||||
AZURE_OPENAI_API_VERSION: z.string().optional(), // Azure OpenAI API version
|
||||
|
||||
// Monitoring & Analytics
|
||||
TELEMETRY_ENDPOINT: z.string().url().optional(), // Custom telemetry/analytics endpoint
|
||||
COST_MULTIPLIER: z.number().optional(), // Multiplier for cost calculations
|
||||
COPILOT_COST_MULTIPLIER: z.number().optional(), // Multiplier for copilot cost calculations
|
||||
SENTRY_ORG: z.string().optional(), // Sentry organization for error tracking
|
||||
SENTRY_PROJECT: z.string().optional(), // Sentry project for error tracking
|
||||
SENTRY_AUTH_TOKEN: z.string().optional(), // Sentry authentication token
|
||||
TELEMETRY_ENDPOINT: z.string().url().optional(), // Custom telemetry/analytics endpoint
|
||||
COST_MULTIPLIER: z.number().optional(), // Multiplier for cost calculations
|
||||
COPILOT_COST_MULTIPLIER: z.number().optional(), // Multiplier for copilot cost calculations
|
||||
SENTRY_ORG: z.string().optional(), // Sentry organization for error tracking
|
||||
SENTRY_PROJECT: z.string().optional(), // Sentry project for error tracking
|
||||
SENTRY_AUTH_TOKEN: z.string().optional(), // Sentry authentication token
|
||||
|
||||
// External Services
|
||||
JWT_SECRET: z.string().min(1).optional(), // JWT signing secret for custom tokens
|
||||
BROWSERBASE_API_KEY: z.string().min(1).optional(), // Browserbase API key for browser automation
|
||||
BROWSERBASE_PROJECT_ID: z.string().min(1).optional(), // Browserbase project ID
|
||||
GITHUB_TOKEN: z.string().optional(), // GitHub personal access token for API access
|
||||
JWT_SECRET: z.string().min(1).optional(), // JWT signing secret for custom tokens
|
||||
BROWSERBASE_API_KEY: z.string().min(1).optional(), // Browserbase API key for browser automation
|
||||
BROWSERBASE_PROJECT_ID: z.string().min(1).optional(), // Browserbase project ID
|
||||
GITHUB_TOKEN: z.string().optional(), // GitHub personal access token for API access
|
||||
|
||||
// Infrastructure & Deployment
|
||||
NEXT_RUNTIME: z.string().optional(), // Next.js runtime environment
|
||||
VERCEL_ENV: z.string().optional(), // Vercel deployment environment
|
||||
DOCKER_BUILD: z.boolean().optional(), // Flag indicating Docker build environment
|
||||
NEXT_RUNTIME: z.string().optional(), // Next.js runtime environment
|
||||
VERCEL_ENV: z.string().optional(), // Vercel deployment environment
|
||||
DOCKER_BUILD: z.boolean().optional(), // Flag indicating Docker build environment
|
||||
|
||||
// Background Jobs & Scheduling
|
||||
TRIGGER_SECRET_KEY: z.string().min(1).optional(), // Trigger.dev secret key for background jobs
|
||||
CRON_SECRET: z.string().optional(), // Secret for authenticating cron job requests
|
||||
JOB_RETENTION_DAYS: z.string().optional().default('1'), // Days to retain job logs/data
|
||||
TRIGGER_SECRET_KEY: z.string().min(1).optional(), // Trigger.dev secret key for background jobs
|
||||
CRON_SECRET: z.string().optional(), // Secret for authenticating cron job requests
|
||||
JOB_RETENTION_DAYS: z.string().optional().default('1'), // Days to retain job logs/data
|
||||
|
||||
// Cloud Storage - AWS S3
|
||||
AWS_REGION: z.string().optional(), // AWS region for S3 buckets
|
||||
AWS_ACCESS_KEY_ID: z.string().optional(), // AWS access key ID
|
||||
AWS_SECRET_ACCESS_KEY: z.string().optional(), // AWS secret access key
|
||||
S3_BUCKET_NAME: z.string().optional(), // S3 bucket for general file storage
|
||||
S3_LOGS_BUCKET_NAME: z.string().optional(), // S3 bucket for storing logs
|
||||
S3_KB_BUCKET_NAME: z.string().optional(), // S3 bucket for knowledge base files
|
||||
S3_EXECUTION_FILES_BUCKET_NAME: z.string().optional(), // S3 bucket for workflow execution files
|
||||
S3_CHAT_BUCKET_NAME: z.string().optional(), // S3 bucket for chat logos
|
||||
S3_COPILOT_BUCKET_NAME: z.string().optional(), // S3 bucket for copilot files
|
||||
AWS_REGION: z.string().optional(), // AWS region for S3 buckets
|
||||
AWS_ACCESS_KEY_ID: z.string().optional(), // AWS access key ID
|
||||
AWS_SECRET_ACCESS_KEY: z.string().optional(), // AWS secret access key
|
||||
S3_BUCKET_NAME: z.string().optional(), // S3 bucket for general file storage
|
||||
S3_LOGS_BUCKET_NAME: z.string().optional(), // S3 bucket for storing logs
|
||||
S3_KB_BUCKET_NAME: z.string().optional(), // S3 bucket for knowledge base files
|
||||
S3_EXECUTION_FILES_BUCKET_NAME: z.string().optional(), // S3 bucket for workflow execution files
|
||||
S3_CHAT_BUCKET_NAME: z.string().optional(), // S3 bucket for chat logos
|
||||
S3_COPILOT_BUCKET_NAME: z.string().optional(), // S3 bucket for copilot files
|
||||
|
||||
// Cloud Storage - Azure Blob
|
||||
AZURE_ACCOUNT_NAME: z.string().optional(), // Azure storage account name
|
||||
AZURE_ACCOUNT_KEY: z.string().optional(), // Azure storage account key
|
||||
AZURE_CONNECTION_STRING: z.string().optional(), // Azure storage connection string
|
||||
AZURE_STORAGE_CONTAINER_NAME: z.string().optional(), // Azure container for general files
|
||||
AZURE_STORAGE_KB_CONTAINER_NAME: z.string().optional(), // Azure container for knowledge base files
|
||||
AZURE_STORAGE_EXECUTION_FILES_CONTAINER_NAME: z.string().optional(), // Azure container for workflow execution files
|
||||
AZURE_STORAGE_CHAT_CONTAINER_NAME: z.string().optional(), // Azure container for chat logos
|
||||
AZURE_STORAGE_COPILOT_CONTAINER_NAME: z.string().optional(), // Azure container for copilot files
|
||||
AZURE_ACCOUNT_NAME: z.string().optional(), // Azure storage account name
|
||||
AZURE_ACCOUNT_KEY: z.string().optional(), // Azure storage account key
|
||||
AZURE_CONNECTION_STRING: z.string().optional(), // Azure storage connection string
|
||||
AZURE_STORAGE_CONTAINER_NAME: z.string().optional(), // Azure container for general files
|
||||
AZURE_STORAGE_KB_CONTAINER_NAME: z.string().optional(), // Azure container for knowledge base files
|
||||
AZURE_STORAGE_EXECUTION_FILES_CONTAINER_NAME: z.string().optional(), // Azure container for workflow execution files
|
||||
AZURE_STORAGE_CHAT_CONTAINER_NAME: z.string().optional(), // Azure container for chat logos
|
||||
AZURE_STORAGE_COPILOT_CONTAINER_NAME: z.string().optional(), // Azure container for copilot files
|
||||
|
||||
// Data Retention
|
||||
FREE_PLAN_LOG_RETENTION_DAYS: z.string().optional(), // Log retention days for free plan users
|
||||
FREE_PLAN_LOG_RETENTION_DAYS: z.string().optional(), // Log retention days for free plan users
|
||||
|
||||
// Rate Limiting Configuration
|
||||
RATE_LIMIT_WINDOW_MS: z.string().optional().default('60000'), // Rate limit window duration in milliseconds (default: 1 minute)
|
||||
MANUAL_EXECUTION_LIMIT: z.string().optional().default('999999'), // Manual execution bypass value (effectively unlimited)
|
||||
RATE_LIMIT_FREE_SYNC: z.string().optional().default('10'), // Free tier sync API executions per minute
|
||||
RATE_LIMIT_FREE_ASYNC: z.string().optional().default('50'), // Free tier async API executions per minute
|
||||
RATE_LIMIT_PRO_SYNC: z.string().optional().default('25'), // Pro tier sync API executions per minute
|
||||
RATE_LIMIT_PRO_ASYNC: z.string().optional().default('200'), // Pro tier async API executions per minute
|
||||
RATE_LIMIT_TEAM_SYNC: z.string().optional().default('75'), // Team tier sync API executions per minute
|
||||
RATE_LIMIT_TEAM_ASYNC: z.string().optional().default('500'), // Team tier async API executions per minute
|
||||
RATE_LIMIT_ENTERPRISE_SYNC: z.string().optional().default('150'), // Enterprise tier sync API executions per minute
|
||||
RATE_LIMIT_ENTERPRISE_ASYNC: z.string().optional().default('1000'), // Enterprise tier async API executions per minute
|
||||
MANUAL_EXECUTION_LIMIT: z.string().optional().default('999999'),// Manual execution bypass value (effectively unlimited)
|
||||
RATE_LIMIT_FREE_SYNC: z.string().optional().default('10'), // Free tier sync API executions per minute
|
||||
RATE_LIMIT_FREE_ASYNC: z.string().optional().default('50'), // Free tier async API executions per minute
|
||||
RATE_LIMIT_PRO_SYNC: z.string().optional().default('25'), // Pro tier sync API executions per minute
|
||||
RATE_LIMIT_PRO_ASYNC: z.string().optional().default('200'), // Pro tier async API executions per minute
|
||||
RATE_LIMIT_TEAM_SYNC: z.string().optional().default('75'), // Team tier sync API executions per minute
|
||||
RATE_LIMIT_TEAM_ASYNC: z.string().optional().default('500'), // Team tier async API executions per minute
|
||||
RATE_LIMIT_ENTERPRISE_SYNC: z.string().optional().default('150'), // Enterprise tier sync API executions per minute
|
||||
RATE_LIMIT_ENTERPRISE_ASYNC: z.string().optional().default('1000'), // Enterprise tier async API executions per minute
|
||||
|
||||
// Real-time Communication
|
||||
SOCKET_SERVER_URL: z.string().url().optional(), // WebSocket server URL for real-time features
|
||||
SOCKET_PORT: z.number().optional(), // Port for WebSocket server
|
||||
PORT: z.number().optional(), // Main application port
|
||||
ALLOWED_ORIGINS: z.string().optional(), // CORS allowed origins
|
||||
SOCKET_SERVER_URL: z.string().url().optional(), // WebSocket server URL for real-time features
|
||||
SOCKET_PORT: z.number().optional(), // Port for WebSocket server
|
||||
PORT: z.number().optional(), // Main application port
|
||||
ALLOWED_ORIGINS: z.string().optional(), // CORS allowed origins
|
||||
|
||||
// OAuth Integration Credentials - All optional, enables third-party integrations
|
||||
GOOGLE_CLIENT_ID: z.string().optional(), // Google OAuth client ID for Google services
|
||||
GOOGLE_CLIENT_SECRET: z.string().optional(), // Google OAuth client secret
|
||||
GITHUB_CLIENT_ID: z.string().optional(), // GitHub OAuth client ID for GitHub integration
|
||||
GITHUB_CLIENT_SECRET: z.string().optional(), // GitHub OAuth client secret
|
||||
GITHUB_REPO_CLIENT_ID: z.string().optional(), // GitHub OAuth client ID for repo access
|
||||
GITHUB_REPO_CLIENT_SECRET: z.string().optional(), // GitHub OAuth client secret for repo access
|
||||
X_CLIENT_ID: z.string().optional(), // X (Twitter) OAuth client ID
|
||||
X_CLIENT_SECRET: z.string().optional(), // X (Twitter) OAuth client secret
|
||||
CONFLUENCE_CLIENT_ID: z.string().optional(), // Atlassian Confluence OAuth client ID
|
||||
CONFLUENCE_CLIENT_SECRET: z.string().optional(), // Atlassian Confluence OAuth client secret
|
||||
JIRA_CLIENT_ID: z.string().optional(), // Atlassian Jira OAuth client ID
|
||||
JIRA_CLIENT_SECRET: z.string().optional(), // Atlassian Jira OAuth client secret
|
||||
AIRTABLE_CLIENT_ID: z.string().optional(), // Airtable OAuth client ID
|
||||
AIRTABLE_CLIENT_SECRET: z.string().optional(), // Airtable OAuth client secret
|
||||
SUPABASE_CLIENT_ID: z.string().optional(), // Supabase OAuth client ID
|
||||
SUPABASE_CLIENT_SECRET: z.string().optional(), // Supabase OAuth client secret
|
||||
NOTION_CLIENT_ID: z.string().optional(), // Notion OAuth client ID
|
||||
NOTION_CLIENT_SECRET: z.string().optional(), // Notion OAuth client secret
|
||||
DISCORD_CLIENT_ID: z.string().optional(), // Discord OAuth client ID
|
||||
DISCORD_CLIENT_SECRET: z.string().optional(), // Discord OAuth client secret
|
||||
MICROSOFT_CLIENT_ID: z.string().optional(), // Microsoft OAuth client ID for Office 365/Teams
|
||||
MICROSOFT_CLIENT_SECRET: z.string().optional(), // Microsoft OAuth client secret
|
||||
HUBSPOT_CLIENT_ID: z.string().optional(), // HubSpot OAuth client ID
|
||||
HUBSPOT_CLIENT_SECRET: z.string().optional(), // HubSpot OAuth client secret
|
||||
WEALTHBOX_CLIENT_ID: z.string().optional(), // WealthBox OAuth client ID
|
||||
WEALTHBOX_CLIENT_SECRET: z.string().optional(), // WealthBox OAuth client secret
|
||||
LINEAR_CLIENT_ID: z.string().optional(), // Linear OAuth client ID
|
||||
LINEAR_CLIENT_SECRET: z.string().optional(), // Linear OAuth client secret
|
||||
SLACK_CLIENT_ID: z.string().optional(), // Slack OAuth client ID
|
||||
SLACK_CLIENT_SECRET: z.string().optional(), // Slack OAuth client secret
|
||||
REDDIT_CLIENT_ID: z.string().optional(), // Reddit OAuth client ID
|
||||
REDDIT_CLIENT_SECRET: z.string().optional(), // Reddit OAuth client secret
|
||||
GOOGLE_CLIENT_ID: z.string().optional(), // Google OAuth client ID for Google services
|
||||
GOOGLE_CLIENT_SECRET: z.string().optional(), // Google OAuth client secret
|
||||
GITHUB_CLIENT_ID: z.string().optional(), // GitHub OAuth client ID for GitHub integration
|
||||
GITHUB_CLIENT_SECRET: z.string().optional(), // GitHub OAuth client secret
|
||||
GITHUB_REPO_CLIENT_ID: z.string().optional(), // GitHub OAuth client ID for repo access
|
||||
GITHUB_REPO_CLIENT_SECRET: z.string().optional(), // GitHub OAuth client secret for repo access
|
||||
X_CLIENT_ID: z.string().optional(), // X (Twitter) OAuth client ID
|
||||
X_CLIENT_SECRET: z.string().optional(), // X (Twitter) OAuth client secret
|
||||
CONFLUENCE_CLIENT_ID: z.string().optional(), // Atlassian Confluence OAuth client ID
|
||||
CONFLUENCE_CLIENT_SECRET: z.string().optional(), // Atlassian Confluence OAuth client secret
|
||||
JIRA_CLIENT_ID: z.string().optional(), // Atlassian Jira OAuth client ID
|
||||
JIRA_CLIENT_SECRET: z.string().optional(), // Atlassian Jira OAuth client secret
|
||||
AIRTABLE_CLIENT_ID: z.string().optional(), // Airtable OAuth client ID
|
||||
AIRTABLE_CLIENT_SECRET: z.string().optional(), // Airtable OAuth client secret
|
||||
SUPABASE_CLIENT_ID: z.string().optional(), // Supabase OAuth client ID
|
||||
SUPABASE_CLIENT_SECRET: z.string().optional(), // Supabase OAuth client secret
|
||||
NOTION_CLIENT_ID: z.string().optional(), // Notion OAuth client ID
|
||||
NOTION_CLIENT_SECRET: z.string().optional(), // Notion OAuth client secret
|
||||
DISCORD_CLIENT_ID: z.string().optional(), // Discord OAuth client ID
|
||||
DISCORD_CLIENT_SECRET: z.string().optional(), // Discord OAuth client secret
|
||||
MICROSOFT_CLIENT_ID: z.string().optional(), // Microsoft OAuth client ID for Office 365/Teams
|
||||
MICROSOFT_CLIENT_SECRET: z.string().optional(), // Microsoft OAuth client secret
|
||||
HUBSPOT_CLIENT_ID: z.string().optional(), // HubSpot OAuth client ID
|
||||
HUBSPOT_CLIENT_SECRET: z.string().optional(), // HubSpot OAuth client secret
|
||||
WEALTHBOX_CLIENT_ID: z.string().optional(), // WealthBox OAuth client ID
|
||||
WEALTHBOX_CLIENT_SECRET: z.string().optional(), // WealthBox OAuth client secret
|
||||
LINEAR_CLIENT_ID: z.string().optional(), // Linear OAuth client ID
|
||||
LINEAR_CLIENT_SECRET: z.string().optional(), // Linear OAuth client secret
|
||||
SLACK_CLIENT_ID: z.string().optional(), // Slack OAuth client ID
|
||||
SLACK_CLIENT_SECRET: z.string().optional(), // Slack OAuth client secret
|
||||
REDDIT_CLIENT_ID: z.string().optional(), // Reddit OAuth client ID
|
||||
REDDIT_CLIENT_SECRET: z.string().optional(), // Reddit OAuth client secret
|
||||
},
|
||||
|
||||
client: {
|
||||
// Core Application URLs - Required for frontend functionality
|
||||
NEXT_PUBLIC_APP_URL: z.string().url(), // Base URL of the application (e.g., https://app.sim.ai)
|
||||
NEXT_PUBLIC_VERCEL_URL: z.string().optional(), // Vercel deployment URL for preview/production
|
||||
NEXT_PUBLIC_APP_URL: z.string().url(), // Base URL of the application (e.g., https://app.sim.ai)
|
||||
NEXT_PUBLIC_VERCEL_URL: z.string().optional(), // Vercel deployment URL for preview/production
|
||||
|
||||
// Client-side Services
|
||||
NEXT_PUBLIC_SENTRY_DSN: z.string().url().optional(), // Sentry DSN for client-side error tracking
|
||||
NEXT_PUBLIC_SOCKET_URL: z.string().url().optional(), // WebSocket server URL for real-time features
|
||||
NEXT_PUBLIC_SENTRY_DSN: z.string().url().optional(), // Sentry DSN for client-side error tracking
|
||||
NEXT_PUBLIC_SOCKET_URL: z.string().url().optional(), // WebSocket server URL for real-time features
|
||||
|
||||
// Asset Storage
|
||||
NEXT_PUBLIC_BLOB_BASE_URL: z.string().url().optional(), // Base URL for Vercel Blob storage (CDN assets)
|
||||
NEXT_PUBLIC_BLOB_BASE_URL: z.string().url().optional(), // Base URL for Vercel Blob storage (CDN assets)
|
||||
|
||||
// Google Services - For client-side Google integrations
|
||||
NEXT_PUBLIC_GOOGLE_CLIENT_ID: z.string().optional(), // Google OAuth client ID for browser auth
|
||||
NEXT_PUBLIC_GOOGLE_CLIENT_ID: z.string().optional(), // Google OAuth client ID for browser auth
|
||||
|
||||
// Analytics & Tracking
|
||||
NEXT_PUBLIC_RB2B_KEY: z.string().optional(), // RB2B tracking key for B2B analytics
|
||||
NEXT_PUBLIC_GOOGLE_API_KEY: z.string().optional(), // Google API key for client-side API calls
|
||||
NEXT_PUBLIC_GOOGLE_PROJECT_NUMBER: z.string().optional(), // Google project number for Drive picker
|
||||
NEXT_PUBLIC_RB2B_KEY: z.string().optional(), // RB2B tracking key for B2B analytics
|
||||
NEXT_PUBLIC_GOOGLE_API_KEY: z.string().optional(), // Google API key for client-side API calls
|
||||
NEXT_PUBLIC_GOOGLE_PROJECT_NUMBER: z.string().optional(), // Google project number for Drive picker
|
||||
|
||||
// UI Branding & Whitelabeling
|
||||
NEXT_PUBLIC_BRAND_NAME: z.string().optional(), // Custom brand name (defaults to "Sim")
|
||||
NEXT_PUBLIC_BRAND_LOGO_URL: z.string().url().optional(), // Custom logo URL
|
||||
NEXT_PUBLIC_BRAND_FAVICON_URL: z.string().url().optional(), // Custom favicon URL
|
||||
NEXT_PUBLIC_BRAND_PRIMARY_COLOR: z.string().optional(), // Primary brand color (hex)
|
||||
NEXT_PUBLIC_BRAND_SECONDARY_COLOR: z.string().optional(), // Secondary brand color (hex)
|
||||
NEXT_PUBLIC_BRAND_ACCENT_COLOR: z.string().optional(), // Accent brand color (hex)
|
||||
NEXT_PUBLIC_CUSTOM_CSS_URL: z.string().url().optional(), // Custom CSS stylesheet URL
|
||||
NEXT_PUBLIC_HIDE_BRANDING: z.string().optional(), // Hide "Powered by" branding
|
||||
NEXT_PUBLIC_CUSTOM_FOOTER_TEXT: z.string().optional(), // Custom footer text
|
||||
NEXT_PUBLIC_SUPPORT_EMAIL: z.string().email().optional(), // Custom support email
|
||||
NEXT_PUBLIC_SUPPORT_URL: z.string().url().optional(), // Custom support URL
|
||||
NEXT_PUBLIC_DOCUMENTATION_URL: z.string().url().optional(), // Custom documentation URL
|
||||
NEXT_PUBLIC_TERMS_URL: z.string().url().optional(), // Custom terms of service URL
|
||||
NEXT_PUBLIC_PRIVACY_URL: z.string().url().optional(), // Custom privacy policy URL
|
||||
NEXT_PUBLIC_BRAND_NAME: z.string().optional(), // Custom brand name (defaults to "Sim")
|
||||
NEXT_PUBLIC_BRAND_LOGO_URL: z.string().url().optional(), // Custom logo URL
|
||||
NEXT_PUBLIC_BRAND_FAVICON_URL: z.string().url().optional(), // Custom favicon URL
|
||||
NEXT_PUBLIC_BRAND_PRIMARY_COLOR: z.string().optional(), // Primary brand color (hex)
|
||||
NEXT_PUBLIC_BRAND_SECONDARY_COLOR: z.string().optional(), // Secondary brand color (hex)
|
||||
NEXT_PUBLIC_BRAND_ACCENT_COLOR: z.string().optional(), // Accent brand color (hex)
|
||||
NEXT_PUBLIC_CUSTOM_CSS_URL: z.string().url().optional(), // Custom CSS stylesheet URL
|
||||
NEXT_PUBLIC_HIDE_BRANDING: z.string().optional(), // Hide "Powered by" branding
|
||||
NEXT_PUBLIC_CUSTOM_FOOTER_TEXT: z.string().optional(), // Custom footer text
|
||||
NEXT_PUBLIC_SUPPORT_EMAIL: z.string().email().optional(), // Custom support email
|
||||
NEXT_PUBLIC_SUPPORT_URL: z.string().url().optional(), // Custom support URL
|
||||
NEXT_PUBLIC_DOCUMENTATION_URL: z.string().url().optional(), // Custom documentation URL
|
||||
NEXT_PUBLIC_TERMS_URL: z.string().url().optional(), // Custom terms of service URL
|
||||
NEXT_PUBLIC_PRIVACY_URL: z.string().url().optional(), // Custom privacy policy URL
|
||||
},
|
||||
|
||||
// Variables available on both server and client
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Environment utility functions for consistent environment detection across the application
|
||||
*/
|
||||
import { env } from './env'
|
||||
import { env, isTruthy } from './env'
|
||||
|
||||
/**
|
||||
* Is the application running in production mode
|
||||
@@ -23,6 +23,11 @@ export const isTest = env.NODE_ENV === 'test'
|
||||
*/
|
||||
export const isHosted = env.NEXT_PUBLIC_APP_URL === 'https://www.sim.ai'
|
||||
|
||||
/**
|
||||
* Is billing enforcement enabled
|
||||
*/
|
||||
export const isBillingEnabled = isTruthy(env.BILLING_ENABLED)
|
||||
|
||||
/**
|
||||
* Get cost multiplier based on environment
|
||||
*/
|
||||
|
||||
@@ -52,6 +52,10 @@ export default {
|
||||
'4': 'hsl(var(--chart-4))',
|
||||
'5': 'hsl(var(--chart-5))',
|
||||
},
|
||||
gradient: {
|
||||
primary: 'hsl(var(--gradient-primary))',
|
||||
secondary: 'hsl(var(--gradient-secondary))',
|
||||
},
|
||||
},
|
||||
fontWeight: {
|
||||
medium: '460',
|
||||
|
||||
@@ -50,6 +50,7 @@ vi.mock('@/blocks/registry', () => ({
|
||||
})),
|
||||
getAllBlocks: vi.fn(() => ({})),
|
||||
}))
|
||||
|
||||
const originalConsoleError = console.error
|
||||
const originalConsoleWarn = console.warn
|
||||
|
||||
|
||||
Reference in New Issue
Block a user