Skip to content

Commit 2ac788d

Browse files
committed
refactor(pricing): update pricing card functionality and add money-back guarantee
- Modified the PricingCard component to support separate checkout functions for upfront and monthly payments. - Updated pricing display to show annual and monthly prices clearly. - Added a section highlighting the 14-day money-back guarantee for enhanced customer assurance. - Refactored related state management to improve clarity and maintainability.
1 parent 862e4ed commit 2ac788d

3 files changed

Lines changed: 148 additions & 161 deletions

File tree

apps/app/src/app/(app)/upgrade/[orgId]/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const pricingFaqData = [
2323
id: 'free-trial',
2424
question: 'Is there a free trial available?',
2525
answer:
26-
'Our Starter plan is completely free. For our Done For You plan, we offer competitive pricing without free trials since our services are already affordable.',
26+
"We offer a 14-day money back guarantee on all plans. This gives you the confidence to try our services risk-free, knowing you can get a full refund if you're not completely satisfied within the first 14 days.",
2727
},
2828
{
2929
id: 'how-it-works',

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

Lines changed: 118 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -59,49 +59,47 @@ interface PricingCardsProps {
5959

6060
interface 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

7877
const 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

238281
export 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

Comments
 (0)