Skip to content

Polish: Two-tier pricing (Free/Pro) — shadcn/ui + tokens, Pro badge, Stripe fallback#92

Merged
Jackson57279 merged 2 commits intoopen-lovable-guifrom
capy/polish-two-tier-pric-5e9bb8c0
Sep 9, 2025
Merged

Polish: Two-tier pricing (Free/Pro) — shadcn/ui + tokens, Pro badge, Stripe fallback#92
Jackson57279 merged 2 commits intoopen-lovable-guifrom
capy/polish-two-tier-pric-5e9bb8c0

Conversation

@Jackson57279
Copy link
Owner

@Jackson57279 Jackson57279 commented Sep 8, 2025

Summary

  • Unifies pricing across the app to a clean, modern two-tier design (Free + Pro) using existing shadcn/ui components and Tailwind tokens.
  • Removes Enterprise/Team from all pricing UIs, aligns copy and iconography, and adds a subtle Pro highlight with a small “Most popular” badge.
  • Keeps functional checkout wiring ready for Autumn per SCO-001; provides Stripe priceId fallback for Pro.

Changes

  • app/pricing/page.tsx
    • Replaced previous table with a responsive two-card grid (1 col on mobile, 2 cols ≥ md).
    • Free: Sparkles icon, $0, “No credit card required”, features list, CTA → sign-in.
    • Pro: Crown icon, $20/mo, “per month, cancel anytime”, features list, small “Most popular” badge, subtle ring (ring-1 ring-primary/20), CTA → Stripe fallback via CheckoutButton.
    • Uses shadcn Card/Button, tokens from globals, accessible headings (sr-only h2 + CardTitle as h3), semantic ul list, visible focus states; dark-mode friendly classes.
  • components/PricingModal.tsx
    • Reduced to two plans (Free + Pro). Removed annual toggle for now.
    • Mirrored copy/iconography/features; neutral Free and subtly highlighted Pro with badge + ring.
    • CTAs: Free → onUpgrade('free') (caller routes to onboarding/sign-in), Pro → onUpgrade('pro').
  • components/stripe/SubscriptionPlans.tsx
    • Default plans now only Free ($0) and Pro ($20). Removed Team.
    • Updated iconography (Sparkles/Crown), features, and CTA labels per spec.
    • Free → Link to /sign-in. Pro → CheckoutButton with Stripe priceId fallback (NEXT_PUBLIC_STRIPE_PRO_PRICE_ID or "price_pro_monthly").
  • app/components/AutumnFallback.tsx
    • PricingTable shows only Free/Pro with same copy/visuals, small badge and subtle ring.
    • Pro CTA uses CheckoutButton (Stripe fallback) so UI remains functional without Autumn.

Behavior

  • Free CTA: routes users to sign-in/onboarding (no billing).
  • Pro CTA: triggers Stripe fallback checkout via CheckoutButton. Autumn-specific checkout remains delegated to SCO-001 integration; this UI is compatible and ready.

Design & Accessibility

  • Uses existing shadcn/ui Card/Button and Tailwind tokens (no hard-coded hex); primary accent matches app.
  • Subtle animations only (transition, hover shadow, scale-[1.01]).
  • Accessible semantics and keyboard navigation; visible focus rings.
  • Responsive grid with appropriate spacing; dark mode legible via token-aware classes.

Acceptance mapping

  • Exactly two plans across pricing UIs (page + modal + fallback + billing cards via SubscriptionPlans).
  • Pro has subtle highlight and small “Most popular” badge.
  • CTAs: Free → onboarding/sign-in; Pro → checkout via Stripe fallback (Autumn-ready).
  • Responsive 1/2-column layout, proper spacing, WCAG-conscious contrast and focus rings.
  • No Enterprise/Team references remain in pricing UI files.

Notes

  • Env: set NEXT_PUBLIC_STRIPE_PRO_PRICE_ID for production; falls back to "price_pro_monthly" for development.
  • SCO-001 can wire Autumn checkout by swapping the Pro button handler while preserving this visual polish.

₍ᐢ•(ܫ)•ᐢ₎ Generated by Capy (view task)

Summary by CodeRabbit

  • New Features

    • Simplified pricing to two plans: Free and Pro with updated features and clear monthly pricing.
    • Integrated checkout for Pro subscriptions; cancel anytime.
    • Added a concise FAQ covering limits, plan changes, and Free eligibility.
  • Style

    • Redesigned pricing page and modal with improved layout, accessibility, refreshed icons, and a “Most popular” badge.
    • Updated actions: Free directs to sign-in; Pro uses a checkout button.
    • Cleaner cards with clearer descriptions, feature lists, and responsive two-column layout.

…; 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)
@Jackson57279 Jackson57279 added the capy PR created by Capy label Sep 8, 2025
@vercel
Copy link

vercel bot commented Sep 8, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
zapdev Ready Ready Preview Comment Sep 8, 2025 0:33am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 8, 2025

Walkthrough

Consolidates 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

Cohort / File(s) Change summary
Pricing page rewrite
app/pricing/page.tsx
Replaced multi-section pricing with a two-plan layout (Free, Pro), added Stripe checkout for Pro using env price ID, routed Free to /sign-in, refreshed features, visuals, and added a concise FAQ.
Pricing components overhaul
components/stripe/SubscriptionPlans.tsx, components/PricingModal.tsx, app/components/AutumnFallback.tsx
Unified two-plan cards with Sparkles/Crown icons, updated feature lists and badges, switched Free actions to Link/Button, Pro to CheckoutButton (subscription). Made plans prop optional with defaults, removed Enterprise/Team tiers, simplified modal to call onUpgrade('free'/'pro'), refreshed styling and accessibility.

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
Loading
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()
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

I nibble on plans in a tidy row,
Free like clover, Pro’s a golden glow.
A crown for checkout, a sparkle to sign,
Two tidy tiers in a carrot-straight line.
Hop, click, subscribe—then off I go! 🥕✨

Warning

Review ran into problems

🔥 Problems

Errors were encountered while retrieving linked issues.

Errors (1)
  • SCO-001: Entity not found: Issue - Could not find referenced Issue.
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch capy/polish-two-tier-pric-5e9bb8c0

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.ComponentType relies on a global React namespace and can fail type-checking. Import the types explicitly. Also consider deriving buttonVariant from 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 buttonVariant from Button (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.

📥 Commits

Reviewing files that changed from the base of the PR and between f8d6f60 and aa1b9ba.

📒 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.tsx
  • app/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.tsx
  • components/PricingModal.tsx
  • app/components/AutumnFallback.tsx
  • components/stripe/SubscriptionPlans.tsx
components/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place shared React components under components/

Files:

  • components/PricingModal.tsx
  • components/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
The orange variant 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_ID is 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 the customAmount path.


122-127: Confirm sign-in route.

Validate that /sign-in exists (NextAuth setups often use /login or /auth/signin).


88-93: A11y: good use of aria-hidden on decorative icons and clear headings.

No changes requested.

Also applies to: 115-117

@Jackson57279 Jackson57279 merged commit 4184c12 into open-lovable-gui Sep 9, 2025
6 checks passed
@Jackson57279 Jackson57279 deleted the capy/polish-two-tier-pric-5e9bb8c0 branch September 9, 2025 03:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

capy PR created by Capy

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant