Skip to content

Commit bfbbcdb

Browse files
authored
Merge pull request #1035 from trycompai/claudio/comp-225-clean-up-animated-flow-ai-loader-edited-convert-to-banner
[dev] [claudfuen] claudio/comp-225-clean-up-animated-flow-ai-loader-edited-convert-to-banner
2 parents 7ca7641 + e91f1f0 commit bfbbcdb

File tree

7 files changed

+299
-5
lines changed

7 files changed

+299
-5
lines changed

apps/app/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,15 @@
3939
"@tiptap/extension-table-row": "^2.22.3",
4040
"@trigger.dev/react-hooks": "3.3.17",
4141
"@trigger.dev/sdk": "3.3.17",
42+
"@types/canvas-confetti": "^1.9.0",
4243
"@types/three": "^0.177.0",
4344
"@uploadthing/react": "^7.3.0",
4445
"@upstash/ratelimit": "^2.0.5",
4546
"@vercel/sdk": "^1.7.1",
4647
"ai": "^4.3.16",
4748
"axios": "^1.9.0",
4849
"better-auth": "^1.2.8",
50+
"canvas-confetti": "^1.9.3",
4951
"d3": "^7.9.0",
5052
"dub": "^0.63.5",
5153
"framer-motion": "^12.18.1",

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { getSubscriptionData } from '@/app/api/stripe/getSubscriptionData';
22
import { AnimatedLayout } from '@/components/animated-layout';
3+
import { CheckoutCompleteDialog } from '@/components/dialogs/checkout-complete-dialog';
34
import { Header } from '@/components/header';
45
import { AssistantSheet } from '@/components/sheets/assistant-sheet';
56
import { Sidebar } from '@/components/sidebar';
@@ -10,6 +11,7 @@ import { db } from '@comp/db';
1011
import dynamic from 'next/dynamic';
1112
import { cookies, headers } from 'next/headers';
1213
import { redirect } from 'next/navigation';
14+
import { Suspense } from 'react';
1315
import { OnboardingTracker } from './components/OnboardingTracker';
1416

1517
const HotKeys = dynamic(() => import('@/components/hot-keys').then((mod) => mod.HotKeys), {
@@ -100,6 +102,9 @@ export default async function Layout({
100102
<SubscriptionProvider subscription={subscriptionData}>{children}</SubscriptionProvider>
101103
</div>
102104
<AssistantSheet />
105+
<Suspense fallback={null}>
106+
<CheckoutCompleteDialog />
107+
</Suspense>
103108
</AnimatedLayout>
104109
<HotKeys />
105110
</SidebarProvider>

apps/app/src/app/(app)/upgrade/[orgId]/pricing-cards.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ export function PricingCards({ organizationId, priceDetails }: PricingCardsProps
220220
chooseSelfServeAction,
221221
{
222222
onSuccess: () => {
223-
router.push(`/${organizationId}`);
223+
router.push(`/${organizationId}?checkoutComplete=starter`);
224224
},
225225
onError: ({ error }) => {
226226
toast.error(error.serverError || 'Failed to set up free plan');
@@ -252,7 +252,7 @@ export function PricingCards({ organizationId, priceDetails }: PricingCardsProps
252252
organizationId,
253253
mode: 'subscription',
254254
priceId,
255-
successUrl: `${baseUrl}/${organizationId}/settings/billing?success=true`,
255+
successUrl: `${baseUrl}/api/stripe/success?organizationId=${organizationId}&planType=done-for-you`,
256256
cancelUrl: `${baseUrl}/upgrade/${organizationId}`,
257257
allowPromotionCodes: true,
258258
metadata: {

apps/app/src/app/api/stripe/success/route.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ import { syncStripeDataToKV } from '../syncStripeDataToKv';
77
export async function GET(req: Request) {
88
const { user } = await getServersideSession(req);
99

10-
// Extract organizationId from query parameters
10+
// Extract organizationId and planType from query parameters
1111
const url = new URL(req.url);
1212
const organizationId = url.searchParams.get('organizationId');
13+
const planType = url.searchParams.get('planType') || 'done-for-you'; // Default to done-for-you for backwards compatibility
1314

1415
if (!organizationId) {
1516
return redirect('/');
@@ -29,9 +30,10 @@ export async function GET(req: Request) {
2930

3031
const stripeCustomerId = await client.get(`stripe:organization:${organizationId}`);
3132
if (!stripeCustomerId) {
32-
return redirect('/');
33+
return redirect(`/${organizationId}`);
3334
}
3435

3536
await syncStripeDataToKV(stripeCustomerId as string);
36-
return redirect('/');
37+
// Redirect with the plan type from query parameters
38+
return redirect(`/${organizationId}/frameworks?checkoutComplete=${planType}`);
3739
}
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
'use client';
2+
3+
import { Badge } from '@comp/ui/badge';
4+
import { Button } from '@comp/ui/button';
5+
import { Card } from '@comp/ui/card';
6+
import {
7+
Dialog,
8+
DialogContent,
9+
DialogDescription,
10+
DialogFooter,
11+
DialogHeader,
12+
DialogTitle,
13+
} from '@comp/ui/dialog';
14+
import confetti from 'canvas-confetti';
15+
import {
16+
Brain,
17+
CheckCircle2,
18+
FileText,
19+
Headphones,
20+
LucideIcon,
21+
MessageSquare,
22+
Rocket,
23+
Shield,
24+
Sparkles,
25+
Users,
26+
Zap,
27+
} from 'lucide-react';
28+
import { useQueryState } from 'nuqs';
29+
import { useEffect, useState } from 'react';
30+
31+
type PlanType = 'starter' | 'done-for-you';
32+
33+
interface Feature {
34+
icon: LucideIcon;
35+
title: string;
36+
description: string;
37+
}
38+
39+
interface PlanContent {
40+
title: string;
41+
description: string;
42+
badge: string;
43+
badgeDescription: string;
44+
badgeClass: string;
45+
cardClass: string;
46+
iconClass: string;
47+
iconColor: string;
48+
features: Feature[];
49+
buttonText: string;
50+
footerText: string;
51+
}
52+
53+
export function CheckoutCompleteDialog() {
54+
const [checkoutComplete, setCheckoutComplete] = useQueryState('checkoutComplete', {
55+
defaultValue: '',
56+
clearOnDefault: true,
57+
});
58+
const [open, setOpen] = useState(false);
59+
const [planType, setPlanType] = useState<PlanType | null>(null);
60+
61+
useEffect(() => {
62+
if (checkoutComplete === 'starter' || checkoutComplete === 'done-for-you') {
63+
const detectedPlanType = checkoutComplete as PlanType;
64+
65+
// Store the plan type before clearing the query param
66+
setPlanType(detectedPlanType);
67+
68+
// Show the dialog
69+
setOpen(true);
70+
71+
// Trigger confetti animation
72+
const duration = 3 * 1000;
73+
const animationEnd = Date.now() + duration;
74+
const defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: 0 };
75+
76+
function randomInRange(min: number, max: number) {
77+
return Math.random() * (max - min) + min;
78+
}
79+
80+
const interval: any = setInterval(function () {
81+
const timeLeft = animationEnd - Date.now();
82+
83+
if (timeLeft <= 0) {
84+
return clearInterval(interval);
85+
}
86+
87+
const particleCount = 50 * (timeLeft / duration);
88+
// Use different colors based on plan type
89+
const colors =
90+
detectedPlanType === 'done-for-you'
91+
? ['#10b981', '#34d399', '#6ee7b7', '#a7f3d0', '#d1fae5'] // Green for paid
92+
: ['#3b82f6', '#60a5fa', '#93bbfc', '#bfdbfe', '#dbeafe']; // Blue for starter
93+
94+
confetti({
95+
...defaults,
96+
particleCount,
97+
origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 },
98+
colors,
99+
});
100+
confetti({
101+
...defaults,
102+
particleCount,
103+
origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 },
104+
colors,
105+
});
106+
}, 250);
107+
108+
// Clear the query parameter immediately so it doesn't linger in the URL
109+
setCheckoutComplete('');
110+
}
111+
}, [checkoutComplete, setCheckoutComplete]);
112+
113+
const handleClose = () => {
114+
setOpen(false);
115+
};
116+
117+
// Different content based on plan type
118+
const content: Record<PlanType, PlanContent> = {
119+
'done-for-you': {
120+
title: 'Welcome to Done For You!',
121+
description: 'Your subscription is active and your compliance journey begins now.',
122+
badge: '14 Day Money Back Guarantee',
123+
badgeDescription: "If you're not completely satisfied, we'll refund you in full",
124+
badgeClass: 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300',
125+
cardClass: 'bg-green-50/50 dark:bg-green-950/20 border-green-200 dark:border-green-900/50',
126+
iconClass: 'bg-green-100 dark:bg-green-900/30',
127+
iconColor: 'text-green-600 dark:text-green-400',
128+
features: [
129+
{
130+
icon: Shield,
131+
title: 'SOC 2 or ISO 27001 Done For You',
132+
description: 'Complete compliance in 14 days or less',
133+
},
134+
{
135+
icon: Users,
136+
title: 'Dedicated Success Team',
137+
description: 'Your compliance experts are ready to help',
138+
},
139+
{
140+
icon: Sparkles,
141+
title: '3rd Party Audit Included',
142+
description: 'No hidden fees or surprise costs',
143+
},
144+
{
145+
icon: Headphones,
146+
title: '24x7x365 Support & SLA',
147+
description: 'Priority support with guaranteed response times',
148+
},
149+
],
150+
buttonText: 'Get Started',
151+
footerText: 'Your success team will reach out within 24 hours',
152+
},
153+
starter: {
154+
title: 'Welcome to Starter!',
155+
description:
156+
"Everything you need to get compliant, fast. Let's begin your DIY compliance journey!",
157+
badge: 'DIY (Do It Yourself) Compliance',
158+
badgeDescription: 'Build your compliance program at your own pace',
159+
badgeClass: 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300',
160+
cardClass: 'bg-blue-50/50 dark:bg-blue-950/20 border-blue-200 dark:border-blue-900/50',
161+
iconClass: 'bg-blue-100 dark:bg-blue-900/30',
162+
iconColor: 'text-blue-600 dark:text-blue-400',
163+
features: [
164+
{
165+
icon: Rocket,
166+
title: 'Access to all frameworks',
167+
description: 'SOC 2, ISO 27001, HIPAA, GDPR, and more',
168+
},
169+
{
170+
icon: Brain,
171+
title: 'AI Vendor & Risk Management',
172+
description: 'Streamline your vendor assessments and risk tracking',
173+
},
174+
{
175+
icon: FileText,
176+
title: 'Trust & Security Portal',
177+
description: 'Share your compliance status with customers',
178+
},
179+
{
180+
icon: Zap,
181+
title: 'Unlimited team members',
182+
description: 'Collaborate with your entire team at no extra cost',
183+
},
184+
],
185+
buttonText: 'Start Building',
186+
footerText: 'Upgrade to Done For You anytime for expert assistance',
187+
},
188+
};
189+
190+
// Only render content if we have a valid plan type stored
191+
if (!planType) {
192+
return null;
193+
}
194+
195+
const currentContent = content[planType];
196+
197+
return (
198+
<Dialog open={open} onOpenChange={handleClose}>
199+
<DialogContent className="sm:max-w-lg">
200+
<DialogHeader className="text-center pb-2">
201+
<div
202+
className={`mx-auto mb-4 h-12 w-12 rounded-full ${currentContent.iconClass} flex items-center justify-center`}
203+
>
204+
<CheckCircle2 className={`h-6 w-6 ${currentContent.iconColor}`} />
205+
</div>
206+
<DialogTitle className="text-2xl font-semibold text-center">
207+
{currentContent.title}
208+
</DialogTitle>
209+
<DialogDescription className="text-center mt-2">
210+
{currentContent.description}
211+
</DialogDescription>
212+
</DialogHeader>
213+
214+
<div className="space-y-4 py-4">
215+
<Card className={currentContent.cardClass}>
216+
<div className="p-4 text-center">
217+
<Badge className={`${currentContent.badgeClass} mb-2`}>{currentContent.badge}</Badge>
218+
<p className="text-sm text-muted-foreground mt-1">
219+
{currentContent.badgeDescription}
220+
</p>
221+
</div>
222+
</Card>
223+
224+
<div className="space-y-3">
225+
<h3 className="text-sm font-semibold text-muted-foreground">
226+
{planType === 'starter' ? 'What you get:' : "What's included:"}
227+
</h3>
228+
<div className="grid gap-3">
229+
{currentContent.features.map((feature: Feature) => {
230+
const Icon = feature.icon;
231+
return (
232+
<div key={feature.title} className="flex gap-3">
233+
<div className="flex-shrink-0">
234+
<div className="h-8 w-8 rounded-full bg-muted flex items-center justify-center">
235+
<Icon className="h-4 w-4 text-muted-foreground" />
236+
</div>
237+
</div>
238+
<div className="flex-1">
239+
<p className="text-sm font-medium">{feature.title}</p>
240+
<p className="text-xs text-muted-foreground">{feature.description}</p>
241+
</div>
242+
</div>
243+
);
244+
})}
245+
</div>
246+
</div>
247+
248+
{planType === 'starter' && (
249+
<div className="mt-4 p-3 bg-muted/50 rounded-lg">
250+
<p className="text-xs text-muted-foreground text-center">
251+
<MessageSquare className="h-3 w-3 inline mr-1" />
252+
Join our community for support • Pay for your audit when ready
253+
</p>
254+
</div>
255+
)}
256+
</div>
257+
258+
<DialogFooter className="flex-col gap-2 sm:flex-col">
259+
<Button onClick={handleClose} className="w-full" size="default">
260+
{currentContent.buttonText}
261+
</Button>
262+
<p className="text-xs text-center text-muted-foreground">{currentContent.footerText}</p>
263+
</DialogFooter>
264+
</DialogContent>
265+
</Dialog>
266+
);
267+
}

bun.lock

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,13 +96,15 @@
9696
"@tiptap/extension-table-row": "^2.22.3",
9797
"@trigger.dev/react-hooks": "3.3.17",
9898
"@trigger.dev/sdk": "3.3.17",
99+
"@types/canvas-confetti": "^1.9.0",
99100
"@types/three": "^0.177.0",
100101
"@uploadthing/react": "^7.3.0",
101102
"@upstash/ratelimit": "^2.0.5",
102103
"@vercel/sdk": "^1.7.1",
103104
"ai": "^4.3.16",
104105
"axios": "^1.9.0",
105106
"better-auth": "^1.2.8",
107+
"canvas-confetti": "^1.9.3",
106108
"d3": "^7.9.0",
107109
"dub": "^0.63.5",
108110
"framer-motion": "^12.18.1",
@@ -1613,6 +1615,8 @@
16131615

16141616
"@types/bun": ["@types/[email protected]", "", { "dependencies": { "bun-types": "1.2.16" } }, "sha512-1aCZJ/6nSiViw339RsaNhkNoEloLaPzZhxMOYEa7OzRzO41IGg5n/7I43/ZIAW/c+Q6cT12Vf7fOZOoVIzb5BQ=="],
16151617

1618+
"@types/canvas-confetti": ["@types/[email protected]", "", {}, "sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg=="],
1619+
16161620
"@types/connect": ["@types/[email protected]", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
16171621

16181622
"@types/conventional-commits-parser": ["@types/[email protected]", "", { "dependencies": { "@types/node": "*" } }, "sha512-7uz5EHdzz2TqoMfV7ee61Egf5y6NkcO4FB/1iCCQnbeiI1F3xzv3vK5dBCXUCLQgGYS+mUeigK1iKQzvED+QnQ=="],
@@ -2063,6 +2067,8 @@
20632067

20642068
"caniuse-lite": ["[email protected]", "", {}, "sha512-1R/elMjtehrFejxwmexeXAtae5UO9iSyFn6G/I806CYC/BLyyBk1EPhrKBkWhy6wM6Xnm47dSJQec+tLJ39WHw=="],
20652069

2070+
"canvas-confetti": ["[email protected]", "", {}, "sha512-rFfTURMvmVEX1gyXFgn5QMn81bYk70qa0HLzcIOSVEyl57n6o9ItHeBtUSWdvKAPY0xlvBHno4/v3QPrT83q9g=="],
2071+
20662072
"caseless": ["[email protected]", "", {}, "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw=="],
20672073

20682074
"ccount": ["[email protected]", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],

0 commit comments

Comments
 (0)