Polish: Two-tier pricing (Free/Pro) — shadcn/ui + tokens, Pro badge, Stripe fallback#92
Conversation
…; remove Enterprise/Team from pricing components; align copy and tokens; Pro badge & ring; Free CTA to sign-in; Pro CTA wired to Stripe fallback (Autumn-ready)
… Stripe fallback flow
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughConsolidates pricing into Free and Pro across page and components, removes Enterprise/Team tiers, updates icons/UI, and integrates Stripe subscription checkout via CheckoutButton with environment-driven price IDs. Free actions route to /sign-in; modal now calls onUpgrade('free' | 'pro') without billing toggles. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor U as User
participant P as Pricing Page
participant R as Next Router
participant CB as CheckoutButton
participant S as Stripe
U->>P: View plans
alt Choose Free
U->>P: Click "Get started"
P->>R: push("/sign-in")
else Choose Pro
U->>CB: Click "Upgrade to Pro"
CB->>S: Create checkout session (mode=subscription, priceId)
S-->>U: Redirect to hosted checkout
end
sequenceDiagram
autonumber
actor U as User
participant M as PricingModal
participant C as Caller (parent)
U->>M: Open modal
alt Select Free
U->>M: Click "Get started"
M-->>C: onUpgrade('free')
else Select Pro
U->>M: Click "Upgrade to Pro"
M-->>C: onUpgrade('pro')
end
U->>M: Close (X)
M-->>C: onClose()
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
Warning Review ran into problems🔥 ProblemsErrors were encountered while retrieving linked issues. Errors (1)
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
components/PricingModal.tsx (1)
15-16: Enable exit animations and improve dialog a11y (role, aria, labelling).Returning null disables AnimatePresence exit. Also add dialog semantics and labels for screen readers.
-export default function PricingModal({ isOpen, onClose, onUpgrade }: PricingModalProps) { - if (!isOpen) return null; - - return ( - <AnimatePresence> - <motion.div +export default function PricingModal({ isOpen, onClose, onUpgrade }: PricingModalProps) { + return ( + <AnimatePresence> + {isOpen && ( + <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} - className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4" + className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4" + role="dialog" + aria-modal="true" + aria-labelledby="pricing-title" + aria-describedby="pricing-desc" onClick={onClose} > <motion.div initial={{ scale: 0.98, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ scale: 0.98, opacity: 0 }} className="w-full max-w-5xl" onClick={(e) => e.stopPropagation()} > - <Card className="border bg-background text-foreground"> + <Card className="border bg-background text-foreground"> <div className="flex items-center justify-between p-6 pb-0"> <div> - <h2 className="text-2xl font-bold">Upgrade your plan</h2> - <p className="text-sm text-muted-foreground mt-1"> + <h2 id="pricing-title" className="text-2xl font-bold">Upgrade your plan</h2> + <p id="pricing-desc" className="text-sm text-muted-foreground mt-1"> You've reached the Free plan limit. Unlock more with Pro. </p> </div> <button aria-label="Close pricing" onClick={onClose} className="rounded-md p-2 hover:bg-accent hover:text-accent-foreground transition" + type="button" > <X className="h-5 w-5" /> </button> </div> ... - </motion.div> + </motion.div> + )} </AnimatePresence> ); }Also applies to: 18-24, 27-31, 33-40
components/stripe/SubscriptionPlans.tsx (1)
18-21: Avoid React namespace types; import ComponentType (and optionally align buttonVariant to Button).Using
React.ComponentTyperelies on a global React namespace and can fail type-checking. Import the types explicitly. Also consider derivingbuttonVariantfrom the Button component to prevent invalid variants.Apply within this block:
- icon?: React.ComponentType<{ className?: string }>; - buttonVariant?: 'default' | 'secondary' | 'outline' | 'destructive' | 'code' | 'orange' | 'ghost'; + icon?: ComponentType<{ className?: string }>; + buttonVariant?: ComponentProps<typeof Button>['variant'];Add at the top of the file:
import type { ComponentType, ComponentProps } from 'react';
🧹 Nitpick comments (8)
app/pricing/page.tsx (2)
1-1: Prefer server component + Link-asChild; drop client router usage.This page can be a server component (smaller bundle, better RSC) by replacing the client-side push with Link-asChild and removing the client directive and useRouter import/usage.
- "use client"; - -import { useRouter } from "next/navigation"; +import Link from "next/link"; ... -export default function PricingPage() { - const router = useRouter(); +export default function PricingPage() { ... - <div className="mt-6"> - <Button - variant="outline" - className="w-full" - aria-label="Get started with Free" - onClick={() => router.push("/sign-in")} - > - Get started - </Button> - </div> + <div className="mt-6"> + <Button asChild variant="outline" className="w-full" aria-label="Get started with Free"> + <Link href="/sign-in">Get started</Link> + </Button> + </div>Also applies to: 4-4, 17-18, 72-79
123-131: Add explicit success/cancel URLs to CheckoutButton for predictable redirects.Prevents relying on API defaults and makes local/dev flows deterministic.
<CheckoutButton priceId={proPriceId} mode="subscription" variant="orange" size="lg" className="w-full" + successUrl="/billing?status=success" + cancelUrl="/pricing?status=cancel" >If these routes differ in your app, swap accordingly. Verify the API route supports overriding URLs.
app/components/AutumnFallback.tsx (2)
145-150: Use stable keys for features (avoid index).Prevents unnecessary re-renders on reordering.
-{plan.features.map((feature, index) => ( - <li key={index} className="flex items-start"> +{plan.features.map((feature) => ( + <li key={feature} className="flex items-start">
86-109: DRY up plan definitions across pricing surfaces.Plan copy/features/prices appear here and in app/pricing/page.tsx and PricingModal. Consider a shared config (e.g., constants/pricing.ts) to keep a single source of truth.
I can extract a shared pricing config and wire all three call sites.
components/stripe/SubscriptionPlans.tsx (4)
101-105: Use the plan’s interval instead of hardcoding “month.”Reflect yearly plans correctly and keep copy in sync.
- {plan.price > 0 ? ( - <span className="text-muted-foreground">/month</span> - ) : ( + {plan.price > 0 ? ( + <span className="text-muted-foreground">/{plan.interval}</span> + ) : ( <span className="text-muted-foreground">No credit card required</span> )}- {isPro && ( - <div className="text-xs text-muted-foreground mt-1">per month, cancel anytime</div> - )} + {isPro && ( + <div className="text-xs text-muted-foreground mt-1">per {plan.interval}, cancel anytime</div> + )}Also applies to: 107-109
113-117: Avoid index as list key.Use the feature string for stable keys.
- {plan.features.map((feature, index) => ( - <li key={index} className="flex items-start"> + {plan.features.map((feature) => ( + <li key={feature} className="flex items-start">
20-20: Confirm custom “orange/code” variants exist on Button/CheckoutButton.If these variants aren’t defined, styles will degrade silently. Consider typing
buttonVariantfromButton(see earlier comment) or switch to a known variant.Also applies to: 52-53, 132-133
66-66: Optional: use a classnames helper to compose className.Slightly cleaner and avoids accidental extra spaces.
Example:
import { cn } from '@/lib/utils'; // ... <div className={cn('grid grid-cols-1 md:grid-cols-2 gap-8', className)}>
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (4)
app/components/AutumnFallback.tsx(4 hunks)app/pricing/page.tsx(1 hunks)components/PricingModal.tsx(2 hunks)components/stripe/SubscriptionPlans.tsx(4 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
app/**
📄 CodeRabbit inference engine (CLAUDE.md)
Adopt the Next.js App Router with the app/ directory as the primary application structure
Files:
app/pricing/page.tsxapp/components/AutumnFallback.tsx
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Use TypeScript for application code in this Next.js 15 project
Files:
app/pricing/page.tsxcomponents/PricingModal.tsxapp/components/AutumnFallback.tsxcomponents/stripe/SubscriptionPlans.tsx
components/**
📄 CodeRabbit inference engine (CLAUDE.md)
Place shared React components under components/
Files:
components/PricingModal.tsxcomponents/stripe/SubscriptionPlans.tsx
🧬 Code graph analysis (3)
app/pricing/page.tsx (1)
components/stripe/CheckoutButton.tsx (1)
CheckoutButton(28-113)
components/PricingModal.tsx (2)
components/ui/card.tsx (5)
Card(78-78)CardContent(78-78)CardHeader(78-78)CardTitle(78-78)CardDescription(78-78)components/ui/button.tsx (1)
Button(52-52)
app/components/AutumnFallback.tsx (1)
components/stripe/CheckoutButton.tsx (1)
CheckoutButton(28-113)
🔇 Additional comments (8)
app/pricing/page.tsx (2)
31-36: LGTM: Accessible structure and tokenized styling.Good use of sr-only headings, aria-labelledby sections, shadcn/ui Cards/Buttons, and design tokens (bg/text/ring) with dark-mode support.
Also applies to: 84-96, 138-166
126-127: Button supports ‘orange’ variant
The ‘orange’ variant is defined in components/ui/button.tsx (line 16).app/components/AutumnFallback.tsx (1)
84-85: LGTM: Environment-driven Stripe price ID with safe fallback.Using NEXT_PUBLIC_STRIPE_PRO_PRICE_ID || 'price_pro_monthly' is appropriate for client usage.
components/PricingModal.tsx (2)
50-89: LGTM: Clear two-tier content and consistent features/copy with the pricing page.Good alignment on iconography, badges, and feature lists. onUpgrade('free'|'pro') wiring looks correct.
Also applies to: 91-131
121-128: Confirmed: “orange” variant supported
Theorangevariant is defined in components/ui/button.tsx (line 16). No changes needed.components/stripe/SubscriptionPlans.tsx (3)
48-49: Verify Stripe price ID fallback.Ensure
NEXT_PUBLIC_STRIPE_PRO_PRICE_IDis set in prod and that"price_pro_monthly"exists in your Stripe account; otherwise checkout will 400. If unsure, consider omitting the fallback to trigger thecustomAmountpath.
122-127: Confirm sign-in route.Validate that
/sign-inexists (NextAuth setups often use/loginor/auth/signin).
88-93: A11y: good use of aria-hidden on decorative icons and clear headings.No changes requested.
Also applies to: 115-117
Summary
Changes
Behavior
Design & Accessibility
Acceptance mapping
Notes
₍ᐢ•(ܫ)•ᐢ₎ Generated by Capy (view task)
Summary by CodeRabbit
New Features
Style