mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-20 20:38:16 -05:00
Compare commits
14 Commits
feat/tools
...
v0.4.24
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a02016e247 | ||
|
|
7f1ff7fd86 | ||
|
|
9b2490c4b1 | ||
|
|
71ae27b6cd | ||
|
|
1b7437af14 | ||
|
|
9751c9f5c4 | ||
|
|
641e353d03 | ||
|
|
e4ddeb09d6 | ||
|
|
da091dfe8a | ||
|
|
04f109c1f4 | ||
|
|
2bc8c7bf39 | ||
|
|
fb0fa1fd21 | ||
|
|
7f82ed381a | ||
|
|
219a065a7c |
@@ -1,6 +1,6 @@
|
|||||||
import { db } from '@sim/db'
|
import { db } from '@sim/db'
|
||||||
import { subscription as subscriptionTable, user } from '@sim/db/schema'
|
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 { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { requireStripeClient } from '@/lib/billing/stripe-client'
|
import { requireStripeClient } from '@/lib/billing/stripe-client'
|
||||||
@@ -38,7 +38,10 @@ export async function POST(request: NextRequest) {
|
|||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(subscriptionTable.referenceId, organizationId),
|
eq(subscriptionTable.referenceId, organizationId),
|
||||||
eq(subscriptionTable.status, 'active')
|
or(
|
||||||
|
eq(subscriptionTable.status, 'active'),
|
||||||
|
eq(subscriptionTable.cancelAtPeriodEnd, true)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.limit(1)
|
.limit(1)
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from '@/components/ui/alert-dialog'
|
} from '@/components/ui/alert-dialog'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
|
||||||
import { useSession, useSubscription } from '@/lib/auth-client'
|
import { useSession, useSubscription } from '@/lib/auth-client'
|
||||||
import { createLogger } from '@/lib/logs/console/logger'
|
import { createLogger } from '@/lib/logs/console/logger'
|
||||||
import { getBaseUrl } from '@/lib/urls/utils'
|
import { getBaseUrl } from '@/lib/urls/utils'
|
||||||
@@ -30,6 +29,7 @@ interface CancelSubscriptionProps {
|
|||||||
}
|
}
|
||||||
subscriptionData?: {
|
subscriptionData?: {
|
||||||
periodEnd?: Date | null
|
periodEnd?: Date | null
|
||||||
|
cancelAtPeriodEnd?: boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,35 +127,48 @@ export function CancelSubscription({ subscription, subscriptionData }: CancelSub
|
|||||||
const subscriptionStatus = getSubscriptionStatus()
|
const subscriptionStatus = getSubscriptionStatus()
|
||||||
const activeOrgId = activeOrganization?.id
|
const activeOrgId = activeOrganization?.id
|
||||||
|
|
||||||
// For team/enterprise plans, get the subscription ID from organization store
|
if (isCancelAtPeriodEnd) {
|
||||||
if ((subscriptionStatus.isTeam || subscriptionStatus.isEnterprise) && activeOrgId) {
|
if (!betterAuthSubscription.restore) {
|
||||||
const orgSubscription = useOrganizationStore.getState().subscriptionData
|
throw new Error('Subscription restore not available')
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 })
|
||||||
|
|
||||||
|
// 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()
|
await refresh()
|
||||||
if (activeOrgId) {
|
if (activeOrgId) {
|
||||||
await loadOrganizationSubscription(activeOrgId)
|
await loadOrganizationSubscription(activeOrgId)
|
||||||
await refreshOrganization().catch(() => {})
|
await refreshOrganization().catch(() => {})
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsDialogOpen(false)
|
setIsDialogOpen(false)
|
||||||
} catch (error) {
|
} 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)
|
setError(errorMessage)
|
||||||
logger.error('Failed to keep subscription', { error })
|
logger.error('Failed to restore subscription', { error })
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
@@ -190,19 +203,15 @@ export function CancelSubscription({ subscription, subscriptionData }: CancelSub
|
|||||||
const periodEndDate = getPeriodEndDate()
|
const periodEndDate = getPeriodEndDate()
|
||||||
|
|
||||||
// Check if subscription is set to cancel at period end
|
// Check if subscription is set to cancel at period end
|
||||||
const isCancelAtPeriodEnd = (() => {
|
const isCancelAtPeriodEnd = subscriptionData?.cancelAtPeriodEnd === true
|
||||||
const subscriptionStatus = getSubscriptionStatus()
|
|
||||||
if (subscriptionStatus.isTeam || subscriptionStatus.isEnterprise) {
|
|
||||||
return useOrganizationStore.getState().subscriptionData?.cancelAtPeriodEnd === true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='flex items-center justify-between'>
|
<div className='flex items-center justify-between'>
|
||||||
<div>
|
<div>
|
||||||
<span className='font-medium text-sm'>Manage Subscription</span>
|
<span className='font-medium text-sm'>
|
||||||
|
{isCancelAtPeriodEnd ? 'Restore Subscription' : 'Manage Subscription'}
|
||||||
|
</span>
|
||||||
{isCancelAtPeriodEnd && (
|
{isCancelAtPeriodEnd && (
|
||||||
<p className='mt-1 text-muted-foreground text-xs'>
|
<p className='mt-1 text-muted-foreground text-xs'>
|
||||||
You'll keep access until {formatDate(periodEndDate)}
|
You'll keep access until {formatDate(periodEndDate)}
|
||||||
@@ -217,10 +226,12 @@ export function CancelSubscription({ subscription, subscriptionData }: CancelSub
|
|||||||
'h-8 rounded-[8px] font-medium text-xs transition-all duration-200',
|
'h-8 rounded-[8px] font-medium text-xs transition-all duration-200',
|
||||||
error
|
error
|
||||||
? 'border-red-500 text-red-500 dark:border-red-500 dark:text-red-500'
|
? '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>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -228,11 +239,11 @@ export function CancelSubscription({ subscription, subscriptionData }: CancelSub
|
|||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>
|
<AlertDialogTitle>
|
||||||
{isCancelAtPeriodEnd ? 'Manage' : 'Cancel'} {subscription.plan} subscription?
|
{isCancelAtPeriodEnd ? 'Restore' : 'Cancel'} {subscription.plan} subscription?
|
||||||
</AlertDialogTitle>
|
</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
{isCancelAtPeriodEnd
|
{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(
|
: `You'll be redirected to Stripe to manage your subscription. You'll keep access until ${formatDate(
|
||||||
periodEndDate
|
periodEndDate
|
||||||
)}, then downgrade to free plan.`}{' '}
|
)}, then downgrade to free plan.`}{' '}
|
||||||
@@ -260,38 +271,23 @@ export function CancelSubscription({ subscription, subscriptionData }: CancelSub
|
|||||||
<AlertDialogFooter className='flex'>
|
<AlertDialogFooter className='flex'>
|
||||||
<AlertDialogCancel
|
<AlertDialogCancel
|
||||||
className='h-9 w-full rounded-[8px]'
|
className='h-9 w-full rounded-[8px]'
|
||||||
onClick={handleKeep}
|
onClick={isCancelAtPeriodEnd ? () => setIsDialogOpen(false) : handleKeep}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
Keep Subscription
|
{isCancelAtPeriodEnd ? 'Cancel' : 'Keep Subscription'}
|
||||||
</AlertDialogCancel>
|
</AlertDialogCancel>
|
||||||
|
|
||||||
{(() => {
|
{(() => {
|
||||||
const subscriptionStatus = getSubscriptionStatus()
|
const subscriptionStatus = getSubscriptionStatus()
|
||||||
if (
|
if (subscriptionStatus.isPaid && isCancelAtPeriodEnd) {
|
||||||
subscriptionStatus.isPaid &&
|
|
||||||
(activeOrganization?.id
|
|
||||||
? useOrganizationStore.getState().subscriptionData?.cancelAtPeriodEnd
|
|
||||||
: false)
|
|
||||||
) {
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider delayDuration={0}>
|
<AlertDialogAction
|
||||||
<Tooltip>
|
onClick={handleKeep}
|
||||||
<TooltipTrigger asChild>
|
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'
|
||||||
<div className='w-full'>
|
disabled={isLoading}
|
||||||
<AlertDialogAction
|
>
|
||||||
disabled
|
{isLoading ? 'Restoring...' : 'Restore Subscription'}
|
||||||
className='h-9 w-full cursor-not-allowed rounded-[8px] bg-muted text-muted-foreground opacity-50'
|
</AlertDialogAction>
|
||||||
>
|
|
||||||
Continue
|
|
||||||
</AlertDialogAction>
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side='top'>
|
|
||||||
<p>Subscription will be cancelled at end of billing period</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -523,6 +523,7 @@ export function Subscription({ onOpenChange }: SubscriptionProps) {
|
|||||||
}}
|
}}
|
||||||
subscriptionData={{
|
subscriptionData={{
|
||||||
periodEnd: subscriptionData?.periodEnd || null,
|
periodEnd: subscriptionData?.periodEnd || null,
|
||||||
|
cancelAtPeriodEnd: subscriptionData?.cancelAtPeriodEnd,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -220,6 +220,7 @@ export async function getSimplifiedBillingSummary(
|
|||||||
metadata: any
|
metadata: any
|
||||||
stripeSubscriptionId: string | null
|
stripeSubscriptionId: string | null
|
||||||
periodEnd: Date | string | null
|
periodEnd: Date | string | null
|
||||||
|
cancelAtPeriodEnd?: boolean
|
||||||
// Usage details
|
// Usage details
|
||||||
usage: {
|
usage: {
|
||||||
current: number
|
current: number
|
||||||
@@ -318,6 +319,7 @@ export async function getSimplifiedBillingSummary(
|
|||||||
metadata: subscription.metadata || null,
|
metadata: subscription.metadata || null,
|
||||||
stripeSubscriptionId: subscription.stripeSubscriptionId || null,
|
stripeSubscriptionId: subscription.stripeSubscriptionId || null,
|
||||||
periodEnd: subscription.periodEnd || null,
|
periodEnd: subscription.periodEnd || null,
|
||||||
|
cancelAtPeriodEnd: subscription.cancelAtPeriodEnd || undefined,
|
||||||
// Usage details
|
// Usage details
|
||||||
usage: {
|
usage: {
|
||||||
current: usageData.currentUsage,
|
current: usageData.currentUsage,
|
||||||
@@ -393,6 +395,7 @@ export async function getSimplifiedBillingSummary(
|
|||||||
metadata: subscription?.metadata || null,
|
metadata: subscription?.metadata || null,
|
||||||
stripeSubscriptionId: subscription?.stripeSubscriptionId || null,
|
stripeSubscriptionId: subscription?.stripeSubscriptionId || null,
|
||||||
periodEnd: subscription?.periodEnd || null,
|
periodEnd: subscription?.periodEnd || null,
|
||||||
|
cancelAtPeriodEnd: subscription?.cancelAtPeriodEnd || undefined,
|
||||||
// Usage details
|
// Usage details
|
||||||
usage: {
|
usage: {
|
||||||
current: currentUsage,
|
current: currentUsage,
|
||||||
@@ -450,5 +453,14 @@ function getDefaultBillingSummary(type: 'individual' | 'organization') {
|
|||||||
lastPeriodCost: 0,
|
lastPeriodCost: 0,
|
||||||
daysRemaining: 0,
|
daysRemaining: 0,
|
||||||
},
|
},
|
||||||
|
...(type === 'organization' && {
|
||||||
|
organizationData: {
|
||||||
|
seatCount: 0,
|
||||||
|
memberCount: 0,
|
||||||
|
totalBasePrice: 0,
|
||||||
|
totalCurrentUsage: 0,
|
||||||
|
totalOverage: 0,
|
||||||
|
},
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export interface SubscriptionData {
|
|||||||
metadata: any | null
|
metadata: any | null
|
||||||
stripeSubscriptionId: string | null
|
stripeSubscriptionId: string | null
|
||||||
periodEnd: Date | null
|
periodEnd: Date | null
|
||||||
|
cancelAtPeriodEnd?: boolean
|
||||||
usage: UsageData
|
usage: UsageData
|
||||||
billingBlocked?: boolean
|
billingBlocked?: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user