Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
838a3a3
chore(deps): update workspace references in yarn.lock and enhance tsc…
claudfuen Jun 19, 2025
47815bc
feat(stripe): implement subscription data retrieval and cache management
claudfuen Jun 19, 2025
1cf32c2
feat(billing): enhance subscription management and UI components
claudfuen Jun 19, 2025
b94e7f3
feat(setup): implement organization setup flow and subscription checks
claudfuen Jun 19, 2025
a23186e
Merge branch 'main' of https://github.com/trycompai/comp into claudio…
claudfuen Jun 20, 2025
4939848
refactor: update turbo.json formatting and enhance ComplianceHeader c…
carhartlewis Jun 21, 2025
1807735
style: enhance ComplianceHeader component with improved logo display …
carhartlewis Jun 21, 2025
dfff28b
refactor: update compliance components with improved text description…
carhartlewis Jun 21, 2025
4792901
feat: add API endpoint to fetch cached published websites from the da…
carhartlewis Jun 21, 2025
2e10fb0
Merge pull request #1003 from carhartlewis/lewis/comp-trust-portal-tw…
Marfuen Jun 21, 2025
e1d57d4
feat(setup): enhance organization setup and upgrade flow
claudfuen Jun 21, 2025
ece28b0
refactor: update AttachmentItem and TaskFilterHeader components to us…
claudfuen Jun 21, 2025
6f8821e
fix: adjust scale of AnimatedGradientBackground in upgrade page for i…
claudfuen Jun 21, 2025
27e6b4c
refactor: remove SetupHeader from invite page layout for cleaner design
claudfuen Jun 21, 2025
bb782f5
style: enhance pricing card styles for improved visual feedback
claudfuen Jun 21, 2025
c149fc2
style: update AnimatedGradientBackground and enhance pricing card styles
claudfuen Jun 22, 2025
04835c6
feat: add SelectionIndicator component and enhance pricing card inter…
claudfuen Jun 22, 2025
975b1b1
style: refine pricing card layout and enhance visual indicators
claudfuen Jun 22, 2025
f928e5e
style: refine pricing card layout and improve spacing
claudfuen Jun 22, 2025
c61922f
feat: update environment variables for Stripe pricing plans
claudfuen Jun 22, 2025
3753cc5
feat: update pricing cards with new features and subscription handling
claudfuen Jun 23, 2025
69bd082
feat: implement self-serve subscription option and update pricing det…
claudfuen Jun 23, 2025
9b6adfc
feat: refactor subscription handling and introduce self-serve plan
claudfuen Jun 23, 2025
3a50c55
fix: update subscription data handling and remove unused checkout com…
claudfuen Jun 23, 2025
b53bf9e
Merge branch 'main' of https://github.com/trycompai/comp into claudio…
claudfuen Jun 23, 2025
101dff3
refactor: remove chooseSelfServeAction function
claudfuen Jun 23, 2025
021a6b4
Merge pull request #988 from trycompai/claudio/stripe
claudfuen Jun 23, 2025
c878675
chore: update yarn.lock and enhance animated gradient background
claudfuen Jun 23, 2025
dca5af3
Merge pull request #1008 from trycompai/claudio/stripe
claudfuen Jun 23, 2025
7bd80c0
feat: enhance AnimatedGradientBackground with pulse and glow effects
claudfuen Jun 23, 2025
bd516f7
feat: add rage mode and scroll rotation effects to AnimatedGradientBa…
claudfuen Jun 23, 2025
9c8f932
Merge pull request #1009 from trycompai/claudio/stripe
claudfuen Jun 23, 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: 6 additions & 1 deletion apps/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,22 @@
"@prisma/instrumentation": "6.6.0",
"@react-email/components": "^0.0.41",
"@react-email/render": "^1.1.2",
"@react-three/drei": "^10.3.0",
"@react-three/fiber": "^9.1.2",
"@react-three/postprocessing": "^3.0.4",
"@tanstack/react-query": "^5.74.4",
"@tanstack/react-table": "^8.21.3",
"@trigger.dev/react-hooks": "3.3.17",
"@trigger.dev/sdk": "3.3.17",
"@types/three": "^0.177.0",
"@uploadthing/react": "^7.3.0",
"@upstash/ratelimit": "^2.0.5",
"@vercel/sdk": "^1.7.1",
"ai": "^4.3.16",
"axios": "^1.9.0",
"better-auth": "^1.2.8",
"d3": "^7.9.0",
"framer-motion": "^12.9.2",
"framer-motion": "^12.18.1",
"geist": "^1.3.1",
"motion": "^12.9.2",
"next": "15.4.0-canary.85",
Expand All @@ -62,6 +66,7 @@
"resend": "^4.4.1",
"sonner": "^1.7.1",
"stripe": "^18.1.0",
"three": "^0.177.0",
"ts-pattern": "^5.7.0",
"use-debounce": "^10.0.4",
"use-long-press": "^3.3.0",
Expand Down
46 changes: 46 additions & 0 deletions apps/app/src/actions/organization/choose-self-serve.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
'use server';

import { authWithOrgAccessClient } from '@/actions/safe-action';
import { db } from '@comp/db';
import { revalidatePath } from 'next/cache';
import { headers } from 'next/headers';
import { z } from 'zod';

const chooseSelfServeSchema = z.object({
organizationId: z.string(),
});

export const chooseSelfServeAction = authWithOrgAccessClient
.inputSchema(chooseSelfServeSchema)
.metadata({
name: 'choose-self-serve',
track: {
event: 'choose-self-serve',
description: 'User chose the self-serve (free) plan',
channel: 'server',
},
})
.action(async ({ parsedInput, ctx }) => {
const { organizationId } = parsedInput;
const { member } = ctx;

// Update the organization to mark that they chose self-serve
await db.organization.update({
where: {
id: organizationId,
},
data: {
subscriptionType: 'SELF_SERVE',
},
});

// Revalidate the path
const headersList = await headers();
let path = headersList.get('x-pathname') || headersList.get('referer') || '';
path = path.replace(/\/[a-z]{2}\//, '/');
revalidatePath(path);

return {
success: true,
};
});
101 changes: 101 additions & 0 deletions apps/app/src/actions/stripe/fetch-price-details.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
'use server';

import { stripe } from '@/actions/organization/lib/stripe';
import { env } from '@/env.mjs';
import { client } from '@comp/kv';
import Stripe from 'stripe';

type PriceDetails = {
id: string;
unitAmount: number | null;
currency: string;
interval: Stripe.Price.Recurring.Interval | null;
productName: string | null;
};

export type CachedPrices = {
monthlyPrice: PriceDetails | null;
yearlyPrice: PriceDetails | null;
fetchedAt: number;
};

const CACHE_DURATION = 30 * 60; // 30 minutes in seconds

export async function fetchStripePriceDetails(): Promise<CachedPrices> {
// Fetch from Stripe
const monthlyPriceId = env.NEXT_PUBLIC_STRIPE_SUBSCRIPTION_MANAGED_MONTHLY_PRICE_ID;
const yearlyPriceId = env.NEXT_PUBLIC_STRIPE_SUBSCRIPTION_MANAGED_YEARLY_PRICE_ID;

// Create a unique cache key that includes the price IDs
const cacheKey = `stripe:managed-prices:${monthlyPriceId || 'none'}:${yearlyPriceId || 'none'}`;

try {
// Check cache first
const cached = await client.get<CachedPrices>(cacheKey);
if (cached && cached.fetchedAt && Date.now() - cached.fetchedAt < CACHE_DURATION * 1000) {
return cached;
}
} catch (error) {
console.error('[STRIPE] Error reading from cache:', error);
}

let monthlyPrice: PriceDetails | null = null;
let yearlyPrice: PriceDetails | null = null;

try {
// Fetch monthly price if ID exists
if (monthlyPriceId) {
const price = await stripe.prices.retrieve(monthlyPriceId, {
expand: ['product'],
});

monthlyPrice = {
id: price.id,
unitAmount: price.unit_amount,
currency: price.currency,
interval: price.recurring?.interval ?? null,
productName:
price.product && typeof price.product === 'object' && !price.product.deleted
? price.product.name
: null,
};
}

// Fetch yearly price if ID exists
if (yearlyPriceId) {
const price = await stripe.prices.retrieve(yearlyPriceId, {
expand: ['product'],
});

yearlyPrice = {
id: price.id,
unitAmount: price.unit_amount,
currency: price.currency,
interval: price.recurring?.interval ?? null,
productName:
price.product && typeof price.product === 'object' && !price.product.deleted
? price.product.name
: null,
};
}
} catch (error) {
console.error('[STRIPE] Error fetching prices:', error);
}

const priceData: CachedPrices = {
monthlyPrice,
yearlyPrice,
fetchedAt: Date.now(),
};

// Cache the results
try {
await client.set(cacheKey, priceData, {
ex: CACHE_DURATION,
});
} catch (error) {
console.error('[STRIPE] Error caching price data:', error);
}

return priceData;
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,12 @@ export const OnboardingTracker = ({
}) => {
const [currentMessageIndex, setCurrentMessageIndex] = useState(0);
const triggerJobId = onboarding.triggerJobId;
const { run, error } = useRealtimeRun(triggerJobId ?? undefined, {

if (!triggerJobId || !publicAccessToken) {
return <div className="text-muted-foreground text-sm">Unable to load onboarding tracker.</div>;
}

const { run, error } = useRealtimeRun(triggerJobId, {
accessToken: publicAccessToken,
});

Expand Down
16 changes: 15 additions & 1 deletion apps/app/src/app/(app)/[orgId]/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { getSubscriptionData } from '@/app/api/stripe/getSubscriptionData';
import { AnimatedLayout } from '@/components/animated-layout';
import { Header } from '@/components/header';
import { AssistantSheet } from '@/components/sheets/assistant-sheet';
import { Sidebar } from '@/components/sidebar';
import { SidebarProvider } from '@/context/sidebar-context';
import { SubscriptionProvider } from '@/context/subscription-context';
import { auth } from '@/utils/auth';
import { db } from '@comp/db';
import dynamic from 'next/dynamic';
Expand Down Expand Up @@ -60,6 +62,18 @@ export default async function Layout({
return redirect('/auth/unauthorized');
}

// Fetch subscription data for the organization
const subscriptionData = await getSubscriptionData(requestedOrgId);

// Log subscription status for monitoring
if (subscriptionData.status === 'none') {
console.log(`[SUBSCRIPTION] No subscription for org ${requestedOrgId}`);
} else if (subscriptionData.status === 'self-serve') {
console.log(`[SUBSCRIPTION] Org ${requestedOrgId} is on self-serve (free) plan`);
} else {
console.log(`[SUBSCRIPTION] Org ${requestedOrgId} status: ${subscriptionData.status}`);
}

const onboarding = await db.onboarding.findFirst({
where: {
organizationId: requestedOrgId,
Expand All @@ -83,7 +97,7 @@ export default async function Layout({
className="textured-background mx-auto px-4 py-4"
style={{ minHeight: `calc(100vh - ${pixelsOffset}px)` }}
>
{children}
<SubscriptionProvider subscription={subscriptionData}>{children}</SubscriptionProvider>
</div>
<AssistantSheet />
</AnimatedLayout>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
'use client';

import { Button } from '@comp/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@comp/ui/dialog';
import { Loader2 } from 'lucide-react';

interface CancelSubscriptionDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onConfirm: () => void;
isLoading?: boolean;
currentPeriodEnd?: number;
}

export function CancelSubscriptionDialog({
open,
onOpenChange,
onConfirm,
isLoading = false,
currentPeriodEnd,
}: CancelSubscriptionDialogProps) {
const formattedDate = currentPeriodEnd
? new Date(currentPeriodEnd * 1000).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})
: null;

return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>Cancel Subscription</DialogTitle>
<DialogDescription className="space-y-3 pt-3">
<p>Are you sure you want to cancel your subscription?</p>
{formattedDate && (
<p className="text-sm">
Your subscription will remain active until{' '}
<span className="font-medium">{formattedDate}</span>. You can resume your
subscription at any time before this date.
</p>
)}
<p className="text-sm text-muted-foreground">
You'll lose access to all premium features after your current billing period ends.
</p>
</DialogDescription>
</DialogHeader>
<DialogFooter className="gap-2">
<Button variant="ghost" onClick={() => onOpenChange(false)} disabled={isLoading}>
Keep Subscription
</Button>
<Button
variant="destructive"
onClick={() => {
onConfirm();
}}
disabled={isLoading}
>
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Cancel Subscription
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
3 changes: 3 additions & 0 deletions apps/app/src/app/(app)/[orgId]/settings/billing/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Loader from '@/components/ui/loader';

export default Loader;
Loading
Loading