Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
219a065
v0.4.12: guardrails, mistral models, privacy policy updates (#1608)
waleedlatif1 Oct 12, 2025
7f82ed3
v0.4.13: bugfixes for dev containers, posthog redirect, helm updates
icecrasher321 Oct 13, 2025
fb0fa1f
v0.4.14: canvas speedup and copilot context window
Sg312 Oct 14, 2025
2bc8c7b
v0.4.15: helm chart updates, telegram tools, youtube tools, file uplo…
waleedlatif1 Oct 15, 2025
04f109c
v0.4.16: executions dashboard, UI fixes, zep tools, slack fixes
icecrasher321 Oct 16, 2025
da091df
v0.4.17: input format + files support for webhooks, docs updates, das…
waleedlatif1 Oct 16, 2025
e4ddeb0
v0.4.18: file upload tools, copilot upgrade, docs changes, model filt…
icecrasher321 Oct 19, 2025
641e353
v0.4.19: landing page fix
icecrasher321 Oct 19, 2025
9751c9f
v0.4.20: internal request, kb url fixes, docs styling
icecrasher321 Oct 21, 2025
1b7437a
v0.4.21: more internal auth changes, supabase vector search tool
icecrasher321 Oct 22, 2025
71ae27b
v0.4.22: fix execution context pass for google sheets
icecrasher321 Oct 22, 2025
9b2490c
v0.4.23: webflow tools + triggers, copilot api key fix (#1723)
waleedlatif1 Oct 23, 2025
7f1ff7f
fix(billing): should allow restoring subscription (#1728)
icecrasher321 Oct 25, 2025
a02016e
v0.4.24: sso for chat deployment, usage indicator for file storage, m…
icecrasher321 Oct 27, 2025
9a4b9e2
v0.4.25: variables block, sort ordering for kb, careers page, storage…
waleedlatif1 Oct 29, 2025
69073a3
- Replace `await import` with `require` for improved compatibility in…
web-flow Oct 30, 2025
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
7 changes: 5 additions & 2 deletions apps/sim/app/api/billing/portal/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { db } from '@sim/db'
import { subscription as subscriptionTable, user } from '@sim/db/schema'
import { and, eq } from 'drizzle-orm'
import { and, eq, or } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { requireStripeClient } from '@/lib/billing/stripe-client'
Expand Down Expand Up @@ -38,7 +38,10 @@ export async function POST(request: NextRequest) {
.where(
and(
eq(subscriptionTable.referenceId, organizationId),
eq(subscriptionTable.status, 'active')
or(
eq(subscriptionTable.status, 'active'),
eq(subscriptionTable.cancelAtPeriodEnd, true)
)
)
)
.limit(1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
AlertDialogTitle,
} from '@/components/ui/alert-dialog'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { useSession, useSubscription } from '@/lib/auth-client'
import { createLogger } from '@/lib/logs/console/logger'
import { getBaseUrl } from '@/lib/urls/utils'
Expand All @@ -30,6 +29,7 @@ interface CancelSubscriptionProps {
}
subscriptionData?: {
periodEnd?: Date | null
cancelAtPeriodEnd?: boolean
}
}

Expand Down Expand Up @@ -127,35 +127,48 @@ export function CancelSubscription({ subscription, subscriptionData }: CancelSub
const subscriptionStatus = getSubscriptionStatus()
const activeOrgId = activeOrganization?.id

// For team/enterprise plans, get the subscription ID from organization store
if ((subscriptionStatus.isTeam || subscriptionStatus.isEnterprise) && activeOrgId) {
const orgSubscription = useOrganizationStore.getState().subscriptionData
if (isCancelAtPeriodEnd) {
if (!betterAuthSubscription.restore) {
throw new Error('Subscription restore not available')
}

let referenceId: string
let subscriptionId: string | undefined

if ((subscriptionStatus.isTeam || subscriptionStatus.isEnterprise) && activeOrgId) {
const orgSubscription = useOrganizationStore.getState().subscriptionData
referenceId = activeOrgId
subscriptionId = orgSubscription?.id
} else {
// For personal subscriptions, use user ID and let better-auth find the subscription
referenceId = session.user.id
subscriptionId = undefined
}

logger.info('Restoring subscription', { referenceId, subscriptionId })

if (orgSubscription?.id && orgSubscription?.cancelAtPeriodEnd) {
// Restore the organization subscription
if (!betterAuthSubscription.restore) {
throw new Error('Subscription restore not available')
}

const result = await betterAuthSubscription.restore({
referenceId: activeOrgId,
subscriptionId: orgSubscription.id,
})
logger.info('Organization subscription restored successfully', result)
// Build restore params - only include subscriptionId if we have one (team/enterprise)
const restoreParams: any = { referenceId }
if (subscriptionId) {
restoreParams.subscriptionId = subscriptionId
}

const result = await betterAuthSubscription.restore(restoreParams)

logger.info('Subscription restored successfully', result)
}

// Refresh state and close
await refresh()
if (activeOrgId) {
await loadOrganizationSubscription(activeOrgId)
await refreshOrganization().catch(() => {})
}

setIsDialogOpen(false)
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Failed to keep subscription'
const errorMessage = error instanceof Error ? error.message : 'Failed to restore subscription'
setError(errorMessage)
logger.error('Failed to keep subscription', { error })
logger.error('Failed to restore subscription', { error })
} finally {
setIsLoading(false)
}
Expand Down Expand Up @@ -190,19 +203,15 @@ export function CancelSubscription({ subscription, subscriptionData }: CancelSub
const periodEndDate = getPeriodEndDate()

// Check if subscription is set to cancel at period end
const isCancelAtPeriodEnd = (() => {
const subscriptionStatus = getSubscriptionStatus()
if (subscriptionStatus.isTeam || subscriptionStatus.isEnterprise) {
return useOrganizationStore.getState().subscriptionData?.cancelAtPeriodEnd === true
}
return false
})()
const isCancelAtPeriodEnd = subscriptionData?.cancelAtPeriodEnd === true

return (
<>
<div className='flex items-center justify-between'>
<div>
<span className='font-medium text-sm'>Manage Subscription</span>
<span className='font-medium text-sm'>
{isCancelAtPeriodEnd ? 'Restore Subscription' : 'Manage Subscription'}
</span>
{isCancelAtPeriodEnd && (
<p className='mt-1 text-muted-foreground text-xs'>
You'll keep access until {formatDate(periodEndDate)}
Expand All @@ -217,22 +226,24 @@ export function CancelSubscription({ subscription, subscriptionData }: CancelSub
'h-8 rounded-[8px] font-medium text-xs transition-all duration-200',
error
? 'border-red-500 text-red-500 dark:border-red-500 dark:text-red-500'
: 'text-muted-foreground hover:border-red-500 hover:bg-red-500 hover:text-white dark:hover:border-red-500 dark:hover:bg-red-500'
: isCancelAtPeriodEnd
? 'text-muted-foreground hover:border-green-500 hover:bg-green-500 hover:text-white dark:hover:border-green-500 dark:hover:bg-green-500'
: 'text-muted-foreground hover:border-red-500 hover:bg-red-500 hover:text-white dark:hover:border-red-500 dark:hover:bg-red-500'
)}
>
{error ? 'Error' : 'Manage'}
{error ? 'Error' : isCancelAtPeriodEnd ? 'Restore' : 'Manage'}
</Button>
</div>

<AlertDialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
{isCancelAtPeriodEnd ? 'Manage' : 'Cancel'} {subscription.plan} subscription?
{isCancelAtPeriodEnd ? 'Restore' : 'Cancel'} {subscription.plan} subscription?
</AlertDialogTitle>
<AlertDialogDescription>
{isCancelAtPeriodEnd
? 'Your subscription is set to cancel at the end of the billing period. You can reactivate it or manage other settings.'
? 'Your subscription is set to cancel at the end of the billing period. Would you like to keep your subscription active?'
: `You'll be redirected to Stripe to manage your subscription. You'll keep access until ${formatDate(
periodEndDate
)}, then downgrade to free plan.`}{' '}
Expand Down Expand Up @@ -260,38 +271,23 @@ export function CancelSubscription({ subscription, subscriptionData }: CancelSub
<AlertDialogFooter className='flex'>
<AlertDialogCancel
className='h-9 w-full rounded-[8px]'
onClick={handleKeep}
onClick={isCancelAtPeriodEnd ? () => setIsDialogOpen(false) : handleKeep}
disabled={isLoading}
>
Keep Subscription
{isCancelAtPeriodEnd ? 'Cancel' : 'Keep Subscription'}
</AlertDialogCancel>

{(() => {
const subscriptionStatus = getSubscriptionStatus()
if (
subscriptionStatus.isPaid &&
(activeOrganization?.id
? useOrganizationStore.getState().subscriptionData?.cancelAtPeriodEnd
: false)
) {
if (subscriptionStatus.isPaid && isCancelAtPeriodEnd) {
return (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<div className='w-full'>
<AlertDialogAction
disabled
className='h-9 w-full cursor-not-allowed rounded-[8px] bg-muted text-muted-foreground opacity-50'
>
Continue
</AlertDialogAction>
</div>
</TooltipTrigger>
<TooltipContent side='top'>
<p>Subscription will be cancelled at end of billing period</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<AlertDialogAction
onClick={handleKeep}
className='h-9 w-full rounded-[8px] bg-green-500 text-white transition-all duration-200 hover:bg-green-600 dark:bg-green-500 dark:hover:bg-green-600'
disabled={isLoading}
>
{isLoading ? 'Restoring...' : 'Restore Subscription'}
</AlertDialogAction>
)
}
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,7 @@ export function Subscription({ onOpenChange }: SubscriptionProps) {
}}
subscriptionData={{
periodEnd: subscriptionData?.periodEnd || null,
cancelAtPeriodEnd: subscriptionData?.cancelAtPeriodEnd,
}}
/>
</div>
Expand Down
12 changes: 12 additions & 0 deletions apps/sim/lib/billing/core/billing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ export async function getSimplifiedBillingSummary(
metadata: any
stripeSubscriptionId: string | null
periodEnd: Date | string | null
cancelAtPeriodEnd?: boolean
// Usage details
usage: {
current: number
Expand Down Expand Up @@ -318,6 +319,7 @@ export async function getSimplifiedBillingSummary(
metadata: subscription.metadata || null,
stripeSubscriptionId: subscription.stripeSubscriptionId || null,
periodEnd: subscription.periodEnd || null,
cancelAtPeriodEnd: subscription.cancelAtPeriodEnd || undefined,
// Usage details
usage: {
current: usageData.currentUsage,
Expand Down Expand Up @@ -393,6 +395,7 @@ export async function getSimplifiedBillingSummary(
metadata: subscription?.metadata || null,
stripeSubscriptionId: subscription?.stripeSubscriptionId || null,
periodEnd: subscription?.periodEnd || null,
cancelAtPeriodEnd: subscription?.cancelAtPeriodEnd || undefined,
// Usage details
usage: {
current: currentUsage,
Expand Down Expand Up @@ -450,5 +453,14 @@ function getDefaultBillingSummary(type: 'individual' | 'organization') {
lastPeriodCost: 0,
daysRemaining: 0,
},
...(type === 'organization' && {
organizationData: {
seatCount: 0,
memberCount: 0,
totalBasePrice: 0,
totalCurrentUsage: 0,
totalOverage: 0,
},
}),
}
}
23 changes: 18 additions & 5 deletions apps/sim/lib/uploads/core/setup.server.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import { existsSync } from 'fs'
import { mkdir } from 'fs/promises'
import path, { join } from 'path'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
import { getStorageProvider, USE_BLOB_STORAGE, USE_S3_STORAGE } from '@/lib/uploads/core/setup'

const logger = createLogger('UploadsSetup')

// Server-only upload directory path
const PROJECT_ROOT = path.resolve(process.cwd())
export const UPLOAD_DIR_SERVER = join(PROJECT_ROOT, 'uploads')
// Use dynamic import for path to avoid bundling issues
let UPLOAD_DIR_SERVER: string | undefined

if (typeof window === 'undefined') {
const path = require('path')
const PROJECT_ROOT = path.resolve(process.cwd())
UPLOAD_DIR_SERVER = path.join(PROJECT_ROOT, 'uploads')
}

export { UPLOAD_DIR_SERVER }

/**
* Server-only function to ensure uploads directory exists
Expand All @@ -25,7 +30,15 @@ export async function ensureUploadsDirectory() {
return true
}

if (typeof window !== 'undefined' || !UPLOAD_DIR_SERVER) {
// Skip on client side
return true
}
Comment on lines +33 to +36
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: The !UPLOAD_DIR_SERVER check might be redundant since UPLOAD_DIR_SERVER is only undefined when typeof window !== 'undefined'. Is there a scenario where UPLOAD_DIR_SERVER could be undefined on the server side?

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/sim/lib/uploads/core/setup.server.ts
Line: 33:36

Comment:
**logic:** The `!UPLOAD_DIR_SERVER` check might be redundant since `UPLOAD_DIR_SERVER` is only undefined when `typeof window !== 'undefined'`. Is there a scenario where UPLOAD_DIR_SERVER could be undefined on the server side?

How can I resolve this? If you propose a fix, please make it concise.


try {
const { existsSync } = require('fs')
const { mkdir } = require('fs/promises')

if (!existsSync(UPLOAD_DIR_SERVER)) {
await mkdir(UPLOAD_DIR_SERVER, { recursive: true })
} else {
Expand Down
32 changes: 22 additions & 10 deletions apps/sim/lib/uploads/core/storage-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,15 @@ export async function uploadFile(
return uploadToS3(file, fileName, contentType, configOrSize)
}

if (typeof window !== 'undefined') {
throw new Error('Local file upload is only supported on the server')
}

logger.info(`Uploading file to local storage: ${fileName}`)
const { writeFile } = await import('fs/promises')
const { join } = await import('path')
const { v4: uuidv4 } = await import('uuid')
const { UPLOAD_DIR_SERVER } = await import('@/lib/uploads/core/setup.server')
const { writeFile } = require('fs/promises')
const { join } = require('path')
const { v4: uuidv4 } = require('uuid')
const { UPLOAD_DIR_SERVER } = require('@/lib/uploads/core/setup.server')

const safeFileName = fileName.replace(/[^a-zA-Z0-9.-]/g, '_').replace(/\.\./g, '')
const uniqueKey = `${uuidv4()}-${safeFileName}`
Expand Down Expand Up @@ -169,10 +173,14 @@ export async function downloadFile(
return downloadFromS3(key)
}

if (typeof window !== 'undefined') {
throw new Error('Local file download is only supported on the server')
}

logger.info(`Downloading file from local storage: ${key}`)
const { readFile } = await import('fs/promises')
const { join, resolve, sep } = await import('path')
const { UPLOAD_DIR_SERVER } = await import('@/lib/uploads/core/setup.server')
const { readFile } = require('fs/promises')
const { join, resolve, sep } = require('path')
const { UPLOAD_DIR_SERVER } = require('@/lib/uploads/core/setup.server')

const safeKey = key.replace(/\.\./g, '').replace(/[/\\]/g, '')
const filePath = join(UPLOAD_DIR_SERVER, safeKey)
Expand Down Expand Up @@ -210,10 +218,14 @@ export async function deleteFile(key: string): Promise<void> {
return deleteFromS3(key)
}

if (typeof window !== 'undefined') {
throw new Error('Local file deletion is only supported on the server')
}

logger.info(`Deleting file from local storage: ${key}`)
const { unlink } = await import('fs/promises')
const { join, resolve, sep } = await import('path')
const { UPLOAD_DIR_SERVER } = await import('@/lib/uploads/core/setup.server')
const { unlink } = require('fs/promises')
const { join, resolve, sep } = require('path')
const { UPLOAD_DIR_SERVER } = require('@/lib/uploads/core/setup.server')

const safeKey = key.replace(/\.\./g, '').replace(/[/\\]/g, '')
const filePath = join(UPLOAD_DIR_SERVER, safeKey)
Expand Down
26 changes: 25 additions & 1 deletion apps/sim/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,30 @@ import { isDev, isHosted } from './lib/environment'
import { getMainCSPPolicy, getWorkflowExecutionCSPPolicy } from './lib/security/csp'

const nextConfig: NextConfig = {
webpack: (config, { isServer }) => {
if (!isServer) {
// Don't resolve server-only modules on the client
config.resolve.fallback = {
...config.resolve.fallback,
fs: false,
'fs/promises': false,
path: false,
dns: false,
util: false,
net: false,
tls: false,
crypto: false,
stream: false,
perf_hooks: false,
os: false,
http: false,
https: false,
zlib: false,
child_process: false,
}
}
return config
},
devIndicators: false,
images: {
remotePatterns: [
Expand Down Expand Up @@ -75,7 +99,7 @@ const nextConfig: NextConfig = {
turbopack: {
resolveExtensions: ['.tsx', '.ts', '.jsx', '.js', '.mjs', '.json'],
},
serverExternalPackages: ['pdf-parse'],
serverExternalPackages: ['pdf-parse', '@azure/storage-blob', '@aws-sdk/client-s3', '@aws-sdk/s3-request-presigner'],
experimental: {
optimizeCss: true,
turbopackSourceMaps: false,
Expand Down
Loading
Loading