@@ -59,49 +59,47 @@ interface PricingCardsProps {
5959
6060interface PricingCardProps {
6161 planType : 'starter' | 'managed' ;
62- onCheckout : ( ) => void ;
62+ onCheckoutUpfront : ( ) => void ;
63+ onCheckoutMonthly : ( ) => void ;
6364 title : string ;
6465 description : string ;
65- price : number ;
66- priceLabel : string ;
66+ annualPrice : number ;
67+ monthlyPrice : number ;
6768 subtitle ?: string ;
6869 features : string [ ] ;
6970 badge ?: string ;
70- footerText ?: string ;
71- yearlyPrice ?: number ;
72- isYearly ?: boolean ;
73- isExecuting ?: boolean ;
74- buttonText ?: string ;
71+ isExecutingUpfront ?: boolean ;
72+ isExecutingMonthly ?: boolean ;
7573 isCurrentPlan ?: boolean ;
74+ isLoadingSubscription ?: boolean ;
7675}
7776
7877const PricingCard = ( {
7978 planType,
80- onCheckout,
79+ onCheckoutUpfront,
80+ onCheckoutMonthly,
8181 title,
8282 description,
83- price ,
84- priceLabel ,
83+ annualPrice ,
84+ monthlyPrice ,
8585 subtitle,
8686 features,
8787 badge,
88- footerText,
89- yearlyPrice,
90- isYearly,
91- isExecuting,
92- buttonText,
88+ isExecutingUpfront,
89+ isExecutingMonthly,
9390 isCurrentPlan,
91+ isLoadingSubscription,
9492} : PricingCardProps ) => {
9593 const isPopular = planType === 'managed' ;
9694
9795 return (
9896 < Card
9997 className = { `relative transition-all h-full flex flex-col border ${
10098 isPopular
101- ? 'ring-2 ring-green-500 shadow-lg bg-green-50/30 dark:bg-green-950/20 border-green-500/50 scale-105 hover:shadow-xl '
99+ ? 'ring-2 ring-green-500 shadow-lg bg-green-50/30 dark:bg-green-950/20 border-green-500/50 scale-105'
102100 : isCurrentPlan
103101 ? 'opacity-75'
104- : 'hover:shadow-md bg-card border-border'
102+ : 'hover:border-gray-300 dark:hover:border-gray-600 bg-card border-border'
105103 } `}
106104 >
107105 { isPopular && (
@@ -118,11 +116,9 @@ const PricingCard = ({
118116 { badge && ! isPopular && (
119117 < Badge
120118 className = {
121- badge === '14-day trial'
122- ? 'bg-purple-100 text-purple-700 dark:bg-purple-900 dark:text-purple-300 text-xs px-1.5 py-0'
123- : badge === 'Current Plan'
124- ? 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300 text-xs px-1.5 py-0'
125- : 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300 text-xs px-1.5 py-0'
119+ badge === 'Current Plan'
120+ ? 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300 text-xs px-1.5 py-0'
121+ : 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300 text-xs px-1.5 py-0'
126122 }
127123 >
128124 { badge }
@@ -133,14 +129,12 @@ const PricingCard = ({
133129 </ div >
134130 < div className = "mt-4" >
135131 < div className = "flex items-baseline gap-1" >
136- < span className = "text-3xl font-bold" > ${ price . toLocaleString ( ) } </ span >
137- < span className = "text-sm text-muted-foreground" > /{ priceLabel } </ span >
132+ < span className = "text-3xl font-bold" > ${ annualPrice . toLocaleString ( ) } </ span >
133+ < span className = "text-sm text-muted-foreground" > /year </ span >
138134 </ div >
139- { isYearly && yearlyPrice && (
140- < p className = "text-sm text-muted-foreground mt-1" >
141- Billed as ${ yearlyPrice . toLocaleString ( ) } yearly
142- </ p >
143- ) }
135+ < p className = "text-sm text-muted-foreground mt-1" >
136+ or 12 payments of ${ monthlyPrice . toLocaleString ( ) }
137+ </ p >
144138 { subtitle && (
145139 < p className = "text-sm text-green-600 dark:text-green-400 mt-1" > { subtitle } </ p >
146140 ) }
@@ -184,30 +178,81 @@ const PricingCard = ({
184178 ) ;
185179 } ) }
186180 </ ul >
187- < div className = { `border-t ${ isPopular ? 'border-green-500/30' : 'border-border' } ` } >
188- < p className = "text-xs text-center text-muted-foreground" > { footerText } </ p >
181+
182+ { /* Money Back Guarantee Section */ }
183+ < div className = "mt-4 p-3 bg-green-50 dark:bg-green-950/20 rounded-md border border-green-200 dark:border-green-800" >
184+ < div className = "flex items-center gap-2" >
185+ < div className = "flex-shrink-0" >
186+ < svg
187+ className = "h-5 w-5 text-green-600 dark:text-green-400"
188+ fill = "none"
189+ viewBox = "0 0 24 24"
190+ stroke = "currentColor"
191+ >
192+ < path
193+ strokeLinecap = "round"
194+ strokeLinejoin = "round"
195+ strokeWidth = { 2 }
196+ d = "M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
197+ />
198+ </ svg >
199+ </ div >
200+ < div className = "flex-1" >
201+ < p className = "text-sm font-medium text-green-900 dark:text-green-100" >
202+ 14-Day Money Back Guarantee
203+ </ p >
204+ < p className = "text-xs text-green-700 dark:text-green-300 mt-0.5" >
205+ Try risk-free. Full refund if not satisfied.
206+ </ p >
207+ </ div >
208+ </ div >
189209 </ div >
190210 </ CardContent >
191211
192212 < CardFooter className = "px-6 pt-0 pb-6" >
193- < Button
194- onClick = { onCheckout }
195- className = "w-full"
196- variant = { isPopular ? 'default' : 'outline' }
197- size = { isPopular ? 'lg' : 'default' }
198- disabled = { isExecuting || isCurrentPlan }
199- >
200- { isExecuting ? (
201- < Loader2 className = "h-4 w-4 animate-spin" />
202- ) : (
203- < >
204- { buttonText || 'Go to Checkout' }
205- { ! isCurrentPlan && (
206- < ArrowRight className = { `ml-2 ${ isPopular ? 'h-5 w-5' : 'h-4 w-4' } ` } />
213+ { isCurrentPlan ? (
214+ < Button className = "w-full" variant = "outline" disabled >
215+ Your Current Plan
216+ </ Button >
217+ ) : (
218+ < div className = "flex flex-col gap-2 w-full" >
219+ < Button
220+ onClick = { onCheckoutUpfront }
221+ className = "w-full"
222+ variant = { isPopular ? 'default' : 'outline' }
223+ size = { isPopular ? 'lg' : 'default' }
224+ disabled = { isExecutingUpfront || isExecutingMonthly || isLoadingSubscription }
225+ >
226+ { isExecutingUpfront ? (
227+ < Loader2 className = "h-4 w-4 animate-spin" />
228+ ) : (
229+ < >
230+ Pay in Full
231+ < span className = "ml-1.5 px-2 py-0.5 text-xs font-medium bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300 rounded-full" >
232+ Save 20%
233+ </ span >
234+ < ArrowRight className = { `ml-1.5 ${ isPopular ? 'h-5 w-5' : 'h-4 w-4' } ` } />
235+ </ >
207236 ) }
208- </ >
209- ) }
210- </ Button >
237+ </ Button >
238+ < Button
239+ onClick = { onCheckoutMonthly }
240+ className = "w-full text-muted-foreground hover:text-foreground underline-offset-4 hover:underline"
241+ variant = "link"
242+ size = "sm"
243+ disabled = { isExecutingMonthly || isExecutingUpfront || isLoadingSubscription }
244+ >
245+ { isExecutingMonthly ? (
246+ < Loader2 className = "h-4 w-4 animate-spin" />
247+ ) : (
248+ < >
249+ or pay in 12 installments
250+ < ArrowRight className = "ml-1 h-3 w-3" />
251+ </ >
252+ ) }
253+ </ Button >
254+ </ div >
255+ ) }
211256 </ CardFooter >
212257 </ Card >
213258 ) ;
@@ -228,11 +273,9 @@ const managedFeatures = [
228273 'SOC 2 or ISO 27001 Done For You' ,
229274 '3rd Party Audit Included' ,
230275 'Compliant in 14 Days or Less' ,
231- '14 Day Money Back Guarantee' ,
232276 'Dedicated Success Team' ,
233277 '24x7x365 Support & SLA' ,
234278 'Slack Channel with Comp AI' ,
235- '12-month minimum term' ,
236279] ;
237280
238281export function PricingCards ( {
@@ -242,7 +285,7 @@ export function PricingCards({
242285 subscriptionType,
243286} : PricingCardsProps ) {
244287 const router = useRouter ( ) ;
245- const [ isYearly , setIsYearly ] = useState ( true ) ;
288+ const [ executingButton , setExecutingButton ] = useState < string | null > ( null ) ;
246289
247290 // Check if user has an active starter subscription
248291 const hasStarterSubscription = ( ( ) => {
@@ -285,9 +328,11 @@ export function PricingCards({
285328 if ( data ?. checkoutUrl ) {
286329 router . push ( data . checkoutUrl ) ;
287330 }
331+ setExecutingButton ( null ) ;
288332 } ,
289333 onError : ( { error } ) => {
290334 toast . error ( error . serverError || 'Failed to create checkout session' ) ;
335+ setExecutingButton ( null ) ;
291336 } ,
292337 } ) ;
293338
@@ -296,30 +341,31 @@ export function PricingCards({
296341 ? `${ window . location . protocol } //${ window . location . host } `
297342 : process . env . NEXT_PUBLIC_APP_URL || 'http://localhost:3000' ;
298343
299- const handleSubscribe = ( plan : 'starter' | 'managed' ) => {
344+ const handleSubscribe = ( plan : 'starter' | 'managed' , paymentType : 'upfront' | 'monthly' ) => {
300345 // Don't allow subscribing to starter if already on starter
301346 if ( plan === 'starter' && hasStarterSubscription ) {
302347 return ;
303348 }
304349
350+ // Set which button is executing
351+ setExecutingButton ( `${ plan } -${ paymentType } ` ) ;
352+
305353 let priceId : string | undefined ;
306354 let planType : string ;
307- let trialPeriodDays : number | undefined ;
355+ const isYearly = paymentType === 'upfront' ;
308356
309357 if ( plan === 'starter' ) {
310- // Use starter prices with 14-day trial
358+ // Use starter prices
311359 priceId = isYearly
312360 ? priceDetails . starterYearlyPrice ?. id
313361 : priceDetails . starterMonthlyPrice ?. id ;
314362 planType = 'starter' ;
315- trialPeriodDays = 14 ;
316363 } else {
317364 // Use managed (Done For You) prices
318365 priceId = isYearly
319366 ? priceDetails . managedYearlyPrice ?. id
320367 : priceDetails . managedMonthlyPrice ?. id ;
321368 planType = 'done-for-you' ;
322- trialPeriodDays = undefined ;
323369 }
324370
325371 if ( ! priceId ) {
@@ -354,7 +400,6 @@ export function PricingCards({
354400 successUrl : `${ baseUrl } /api/stripe/success?organizationId=${ organizationId } &planType=${ planType } ` ,
355401 cancelUrl : `${ baseUrl } /upgrade/${ organizationId } ` ,
356402 allowPromotionCodes : true ,
357- trialPeriodDays,
358403 metadata : {
359404 organizationId,
360405 plan,
@@ -400,93 +445,48 @@ export function PricingCards({
400445 </ Alert >
401446 ) }
402447
403- { /* Pricing Toggle */ }
404- < div className = "flex flex-col items-center gap-2" >
405- < div className = "bg-muted/50 p-1 rounded-lg flex items-center justify-center gap-1" >
406- < button
407- onClick = { ( ) => setIsYearly ( false ) }
408- className = { `px-4 py-2 text-sm rounded-md transition-all ${
409- ! isYearly
410- ? 'bg-background font-medium shadow-sm'
411- : 'text-muted-foreground hover:text-foreground'
412- } `}
413- >
414- Monthly
415- </ button >
416- < button
417- onClick = { ( ) => setIsYearly ( true ) }
418- className = { `px-4 py-2 text-sm rounded-md transition-all flex items-center gap-2 ${
419- isYearly
420- ? 'bg-background font-medium shadow-sm'
421- : 'text-muted-foreground hover:text-foreground'
422- } `}
423- >
424- Yearly
425- < Badge className = "bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300 text-xs px-1.5 py-0" >
426- Save 20%
427- </ Badge >
428- </ button >
429- </ div >
430- </ div >
431-
432448 { /* Main Grid */ }
433449 < div className = "grid lg:grid-cols-3 gap-6" >
434450 { /* Plan Selection */ }
435451 < div className = "lg:col-span-2 grid md:grid-cols-2 gap-6 mt-6" >
436452 < PricingCard
437453 planType = "starter"
438- onCheckout = { ( ) => handleSubscribe ( 'starter' ) }
454+ onCheckoutUpfront = { ( ) => handleSubscribe ( 'starter' , 'upfront' ) }
455+ onCheckoutMonthly = { ( ) => handleSubscribe ( 'starter' , 'monthly' ) }
439456 title = "Starter"
440457 description = "Everything you need to get compliant, fast."
441- price = { isYearly ? starterYearlyPriceMonthly : starterMonthlyPrice }
442- priceLabel = "month"
458+ annualPrice = { starterYearlyPriceTotal }
459+ monthlyPrice = { starterMonthlyPrice }
443460 subtitle = "DIY (Do It Yourself) Compliance"
444461 features = { starterFeatures }
445462 badge = {
446463 hasStarterSubscription
447464 ? isSubscriptionCanceling
448465 ? 'Canceling'
449466 : 'Current Plan'
450- : '14-day trial'
451- }
452- yearlyPrice = { isYearly ? starterYearlyPriceTotal : undefined }
453- isYearly = { isYearly }
454- isExecuting = { ( isExecuting && ! hasStarterSubscription ) || isLoadingSubscription }
455- buttonText = {
456- isLoadingSubscription
457- ? 'Loading...'
458- : hasStarterSubscription
459- ? isSubscriptionCanceling
460- ? 'Plan Canceling'
461- : 'Your Current Plan'
462- : 'Start 14-Day Free Trial'
467+ : 'Self-Serve'
463468 }
469+ isExecutingUpfront = { executingButton === 'starter-upfront' }
470+ isExecutingMonthly = { executingButton === 'starter-monthly' }
464471 isCurrentPlan = { hasStarterSubscription }
472+ isLoadingSubscription = { isLoadingSubscription }
465473 />
466474
467475 < PricingCard
468476 planType = "managed"
469- onCheckout = { ( ) => handleSubscribe ( 'managed' ) }
477+ onCheckoutUpfront = { ( ) => handleSubscribe ( 'managed' , 'upfront' ) }
478+ onCheckoutMonthly = { ( ) => handleSubscribe ( 'managed' , 'monthly' ) }
470479 title = "Done For You"
471480 description = "For companies up to 25 people."
472- price = { isYearly ? managedYearlyPriceMonthly : managedMonthlyPrice }
473- priceLabel = "month"
481+ annualPrice = { managedYearlyPriceTotal }
482+ monthlyPrice = { managedMonthlyPrice }
474483 subtitle = "White-glove compliance service"
475484 features = { managedFeatures }
476485 badge = "Popular"
477- yearlyPrice = { isYearly ? managedYearlyPriceTotal : undefined }
478- isYearly = { isYearly }
479- isExecuting = { isExecuting || isLoadingSubscription }
480- buttonText = {
481- isLoadingSubscription
482- ? 'Loading...'
483- : hasStarterSubscription
484- ? isSubscriptionCanceling
485- ? 'Upgrade Instead of Canceling'
486- : 'Upgrade to Done For You'
487- : 'Continue'
488- }
486+ isExecutingUpfront = { executingButton === 'managed-upfront' }
487+ isExecutingMonthly = { executingButton === 'managed-monthly' }
489488 isCurrentPlan = { false }
489+ isLoadingSubscription = { isLoadingSubscription }
490490 />
491491 </ div >
492492
0 commit comments