|
| 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 | +} |
0 commit comments