Skip to content

Commit ea49d25

Browse files
authored
Merge pull request #1004 from trycompai/main
[comp] Production Deploy
2 parents 48ecc39 + 9c8f932 commit ea49d25

64 files changed

Lines changed: 4853 additions & 467 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/app/package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,22 @@
2727
"@prisma/instrumentation": "6.6.0",
2828
"@react-email/components": "^0.0.41",
2929
"@react-email/render": "^1.1.2",
30+
"@react-three/drei": "^10.3.0",
31+
"@react-three/fiber": "^9.1.2",
32+
"@react-three/postprocessing": "^3.0.4",
3033
"@tanstack/react-query": "^5.74.4",
3134
"@tanstack/react-table": "^8.21.3",
3235
"@trigger.dev/react-hooks": "3.3.17",
3336
"@trigger.dev/sdk": "3.3.17",
37+
"@types/three": "^0.177.0",
3438
"@uploadthing/react": "^7.3.0",
3539
"@upstash/ratelimit": "^2.0.5",
3640
"@vercel/sdk": "^1.7.1",
3741
"ai": "^4.3.16",
3842
"axios": "^1.9.0",
3943
"better-auth": "^1.2.8",
4044
"d3": "^7.9.0",
41-
"framer-motion": "^12.9.2",
45+
"framer-motion": "^12.18.1",
4246
"geist": "^1.3.1",
4347
"motion": "^12.9.2",
4448
"next": "15.4.0-canary.85",
@@ -62,6 +66,7 @@
6266
"resend": "^4.4.1",
6367
"sonner": "^1.7.1",
6468
"stripe": "^18.1.0",
69+
"three": "^0.177.0",
6570
"ts-pattern": "^5.7.0",
6671
"use-debounce": "^10.0.4",
6772
"use-long-press": "^3.3.0",
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
'use server';
2+
3+
import { authWithOrgAccessClient } from '@/actions/safe-action';
4+
import { db } from '@comp/db';
5+
import { revalidatePath } from 'next/cache';
6+
import { headers } from 'next/headers';
7+
import { z } from 'zod';
8+
9+
const chooseSelfServeSchema = z.object({
10+
organizationId: z.string(),
11+
});
12+
13+
export const chooseSelfServeAction = authWithOrgAccessClient
14+
.inputSchema(chooseSelfServeSchema)
15+
.metadata({
16+
name: 'choose-self-serve',
17+
track: {
18+
event: 'choose-self-serve',
19+
description: 'User chose the self-serve (free) plan',
20+
channel: 'server',
21+
},
22+
})
23+
.action(async ({ parsedInput, ctx }) => {
24+
const { organizationId } = parsedInput;
25+
const { member } = ctx;
26+
27+
// Update the organization to mark that they chose self-serve
28+
await db.organization.update({
29+
where: {
30+
id: organizationId,
31+
},
32+
data: {
33+
subscriptionType: 'SELF_SERVE',
34+
},
35+
});
36+
37+
// Revalidate the path
38+
const headersList = await headers();
39+
let path = headersList.get('x-pathname') || headersList.get('referer') || '';
40+
path = path.replace(/\/[a-z]{2}\//, '/');
41+
revalidatePath(path);
42+
43+
return {
44+
success: true,
45+
};
46+
});
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
'use server';
2+
3+
import { stripe } from '@/actions/organization/lib/stripe';
4+
import { env } from '@/env.mjs';
5+
import { client } from '@comp/kv';
6+
import Stripe from 'stripe';
7+
8+
type PriceDetails = {
9+
id: string;
10+
unitAmount: number | null;
11+
currency: string;
12+
interval: Stripe.Price.Recurring.Interval | null;
13+
productName: string | null;
14+
};
15+
16+
export type CachedPrices = {
17+
monthlyPrice: PriceDetails | null;
18+
yearlyPrice: PriceDetails | null;
19+
fetchedAt: number;
20+
};
21+
22+
const CACHE_DURATION = 30 * 60; // 30 minutes in seconds
23+
24+
export async function fetchStripePriceDetails(): Promise<CachedPrices> {
25+
// Fetch from Stripe
26+
const monthlyPriceId = env.NEXT_PUBLIC_STRIPE_SUBSCRIPTION_MANAGED_MONTHLY_PRICE_ID;
27+
const yearlyPriceId = env.NEXT_PUBLIC_STRIPE_SUBSCRIPTION_MANAGED_YEARLY_PRICE_ID;
28+
29+
// Create a unique cache key that includes the price IDs
30+
const cacheKey = `stripe:managed-prices:${monthlyPriceId || 'none'}:${yearlyPriceId || 'none'}`;
31+
32+
try {
33+
// Check cache first
34+
const cached = await client.get<CachedPrices>(cacheKey);
35+
if (cached && cached.fetchedAt && Date.now() - cached.fetchedAt < CACHE_DURATION * 1000) {
36+
return cached;
37+
}
38+
} catch (error) {
39+
console.error('[STRIPE] Error reading from cache:', error);
40+
}
41+
42+
let monthlyPrice: PriceDetails | null = null;
43+
let yearlyPrice: PriceDetails | null = null;
44+
45+
try {
46+
// Fetch monthly price if ID exists
47+
if (monthlyPriceId) {
48+
const price = await stripe.prices.retrieve(monthlyPriceId, {
49+
expand: ['product'],
50+
});
51+
52+
monthlyPrice = {
53+
id: price.id,
54+
unitAmount: price.unit_amount,
55+
currency: price.currency,
56+
interval: price.recurring?.interval ?? null,
57+
productName:
58+
price.product && typeof price.product === 'object' && !price.product.deleted
59+
? price.product.name
60+
: null,
61+
};
62+
}
63+
64+
// Fetch yearly price if ID exists
65+
if (yearlyPriceId) {
66+
const price = await stripe.prices.retrieve(yearlyPriceId, {
67+
expand: ['product'],
68+
});
69+
70+
yearlyPrice = {
71+
id: price.id,
72+
unitAmount: price.unit_amount,
73+
currency: price.currency,
74+
interval: price.recurring?.interval ?? null,
75+
productName:
76+
price.product && typeof price.product === 'object' && !price.product.deleted
77+
? price.product.name
78+
: null,
79+
};
80+
}
81+
} catch (error) {
82+
console.error('[STRIPE] Error fetching prices:', error);
83+
}
84+
85+
const priceData: CachedPrices = {
86+
monthlyPrice,
87+
yearlyPrice,
88+
fetchedAt: Date.now(),
89+
};
90+
91+
// Cache the results
92+
try {
93+
await client.set(cacheKey, priceData, {
94+
ex: CACHE_DURATION,
95+
});
96+
} catch (error) {
97+
console.error('[STRIPE] Error caching price data:', error);
98+
}
99+
100+
return priceData;
101+
}

apps/app/src/app/(app)/[orgId]/components/OnboardingTracker.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,12 @@ export const OnboardingTracker = ({
4141
}) => {
4242
const [currentMessageIndex, setCurrentMessageIndex] = useState(0);
4343
const triggerJobId = onboarding.triggerJobId;
44-
const { run, error } = useRealtimeRun(triggerJobId ?? undefined, {
44+
45+
if (!triggerJobId || !publicAccessToken) {
46+
return <div className="text-muted-foreground text-sm">Unable to load onboarding tracker.</div>;
47+
}
48+
49+
const { run, error } = useRealtimeRun(triggerJobId, {
4550
accessToken: publicAccessToken,
4651
});
4752

apps/app/src/app/(app)/[orgId]/layout.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import { getSubscriptionData } from '@/app/api/stripe/getSubscriptionData';
12
import { AnimatedLayout } from '@/components/animated-layout';
23
import { Header } from '@/components/header';
34
import { AssistantSheet } from '@/components/sheets/assistant-sheet';
45
import { Sidebar } from '@/components/sidebar';
56
import { SidebarProvider } from '@/context/sidebar-context';
7+
import { SubscriptionProvider } from '@/context/subscription-context';
68
import { auth } from '@/utils/auth';
79
import { db } from '@comp/db';
810
import dynamic from 'next/dynamic';
@@ -60,6 +62,18 @@ export default async function Layout({
6062
return redirect('/auth/unauthorized');
6163
}
6264

65+
// Fetch subscription data for the organization
66+
const subscriptionData = await getSubscriptionData(requestedOrgId);
67+
68+
// Log subscription status for monitoring
69+
if (subscriptionData.status === 'none') {
70+
console.log(`[SUBSCRIPTION] No subscription for org ${requestedOrgId}`);
71+
} else if (subscriptionData.status === 'self-serve') {
72+
console.log(`[SUBSCRIPTION] Org ${requestedOrgId} is on self-serve (free) plan`);
73+
} else {
74+
console.log(`[SUBSCRIPTION] Org ${requestedOrgId} status: ${subscriptionData.status}`);
75+
}
76+
6377
const onboarding = await db.onboarding.findFirst({
6478
where: {
6579
organizationId: requestedOrgId,
@@ -83,7 +97,7 @@ export default async function Layout({
8397
className="textured-background mx-auto px-4 py-4"
8498
style={{ minHeight: `calc(100vh - ${pixelsOffset}px)` }}
8599
>
86-
{children}
100+
<SubscriptionProvider subscription={subscriptionData}>{children}</SubscriptionProvider>
87101
</div>
88102
<AssistantSheet />
89103
</AnimatedLayout>
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
'use client';
2+
3+
import { Button } from '@comp/ui/button';
4+
import {
5+
Dialog,
6+
DialogContent,
7+
DialogDescription,
8+
DialogFooter,
9+
DialogHeader,
10+
DialogTitle,
11+
} from '@comp/ui/dialog';
12+
import { Loader2 } from 'lucide-react';
13+
14+
interface CancelSubscriptionDialogProps {
15+
open: boolean;
16+
onOpenChange: (open: boolean) => void;
17+
onConfirm: () => void;
18+
isLoading?: boolean;
19+
currentPeriodEnd?: number;
20+
}
21+
22+
export function CancelSubscriptionDialog({
23+
open,
24+
onOpenChange,
25+
onConfirm,
26+
isLoading = false,
27+
currentPeriodEnd,
28+
}: CancelSubscriptionDialogProps) {
29+
const formattedDate = currentPeriodEnd
30+
? new Date(currentPeriodEnd * 1000).toLocaleDateString('en-US', {
31+
year: 'numeric',
32+
month: 'long',
33+
day: 'numeric',
34+
})
35+
: null;
36+
37+
return (
38+
<Dialog open={open} onOpenChange={onOpenChange}>
39+
<DialogContent>
40+
<DialogHeader>
41+
<DialogTitle>Cancel Subscription</DialogTitle>
42+
<DialogDescription className="space-y-3 pt-3">
43+
<p>Are you sure you want to cancel your subscription?</p>
44+
{formattedDate && (
45+
<p className="text-sm">
46+
Your subscription will remain active until{' '}
47+
<span className="font-medium">{formattedDate}</span>. You can resume your
48+
subscription at any time before this date.
49+
</p>
50+
)}
51+
<p className="text-sm text-muted-foreground">
52+
You'll lose access to all premium features after your current billing period ends.
53+
</p>
54+
</DialogDescription>
55+
</DialogHeader>
56+
<DialogFooter className="gap-2">
57+
<Button variant="ghost" onClick={() => onOpenChange(false)} disabled={isLoading}>
58+
Keep Subscription
59+
</Button>
60+
<Button
61+
variant="destructive"
62+
onClick={() => {
63+
onConfirm();
64+
}}
65+
disabled={isLoading}
66+
>
67+
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
68+
Cancel Subscription
69+
</Button>
70+
</DialogFooter>
71+
</DialogContent>
72+
</Dialog>
73+
);
74+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import Loader from '@/components/ui/loader';
2+
3+
export default Loader;

0 commit comments

Comments
 (0)