Build Custom Billing UI with Autumn API#134
Conversation
Replace default Autumn pricing table with custom implementation featuring gradient accents, responsive grid layout, annual/monthly toggle, and enhanced visual hierarchy for better user experience.
CodeCapy Review ₍ᐢ•(ܫ)•ᐢ₎
Codebase SummaryZapDev is an AI-powered development platform enabling users to create web applications in real time. This project uses Next.js, React, TypeScript, Tailwind CSS, and various backend services for AI code generation and subscription management, among others. The recent changes update the billing page by replacing the default pricing table with a custom pricing table integrated with Autumn API. PR ChangesThe PR introduces a custom billing UI by replacing the default Autumn pricing table with a new CustomPricingTable component. This component features gradient accents, responsive grid layout, an annual/monthly toggle, and enhanced visual hierarchy. Changes include adjustments in the page layout, image size, and new UI interactions within the pricing table such as a toggle for pricing intervals, checkout action, and visual cues for recommended plans. Setup Instructions
Generated Test Cases1: Verify Custom Pricing Table Loading State ❗️❗️❗️Description: Tests that when the pricing table data is being fetched, a loading spinner and appropriate loading text are displayed, ensuring users are informed the pricing plans are being loaded. Prerequisites:
Steps:
Expected Result: The pricing table section shows a centered spinner (Loader2) along with the text 'Loading pricing plans...' ensuring that users are aware that data is still loading. 2: Verify Custom Pricing Table Error State ❗️❗️❗️Description: Ensures that if there is an error loading pricing plans, the UI displays an appropriate error message to inform the user and prompt them to try again later. Prerequisites:
Steps:
Expected Result: The custom pricing table shows an error message with text 'Unable to load pricing plans' in red or destructive style and a supplementary message 'Please try again later'. 3: Toggle Between Annual and Monthly Pricing ❗️❗️❗️Description: Tests the functionality of the annual/monthly toggle in the pricing table, ensuring that the correct set of pricing cards are displayed based on the selected interval. Prerequisites:
Steps:
Expected Result: When 'Annual' is selected, pricing details update accordingly and the button indicates it is active (with a primary background and a 'Save 20%' badge). Switching back to 'Monthly' updates the pricing cards to display monthly pricing information. 4: Verify Checkout Functionality on Pricing Card ❗️❗️❗️Description: Checks that clicking on a pricing card's checkout button initiates the checkout process, including a loading state and proper callback handling. Prerequisites:
Steps:
Expected Result: Upon clicking, the pricing card button shows a loader and then triggers a checkout process, either by opening a modal dialog for checkout (if integrated) or by navigating to an external URL, confirming that the process works as intended. 5: Verify Recommended Plan Badge Display ❗️❗️Description: Tests that pricing cards marked as recommended display a badge with appropriate styling and icon, providing visual emphasis on recommended plans. Prerequisites:
Steps:
Expected Result: The pricing card for a recommended plan should visibly show a badge with the recommended text, the Sparkles icon, and enhanced styling (e.g., scale effect and shadow) to draw user attention. 6: Verify Visual Layout and Responsive Design of Pricing Page ❗️❗️Description: Ensures that the updated pricing page has the new layout, incorporating gradient-accented headers, proper spacing and a responsive grid layout for pricing cards. Prerequisites:
Steps:
Expected Result: The pricing page displays a visibly appealing header with the correct gradient effect and theme. The pricing cards should be arranged in a grid that adjusts based on screen size and the number of available products, ensuring a consistent and responsive design. Raw Changes AnalyzedFile: src/app/(home)/pricing/page-content.tsx
Changes:
@@ -1,26 +1,30 @@
"use client";
import Image from "next/image";
-import PricingTable from "@/components/autumn/pricing-table";
+import CustomPricingTable from "@/components/autumn/custom-pricing-table";
export function PricingPageContent() {
return (
- <div className="flex flex-col max-w-3xl mx-auto w-full">
- <section className="space-y-6 pt-[16vh] 2xl:pt-48">
- <div className="flex flex-col items-center">
+ <div className="flex flex-col max-w-7xl mx-auto w-full px-4 sm:px-6 lg:px-8">
+ <section className="space-y-12 pt-[16vh] 2xl:pt-48 pb-24">
+ <div className="flex flex-col items-center space-y-6">
<Image
src="/logo.svg"
alt="ZapDev - AI Development Platform"
- width={50}
- height={50}
+ width={60}
+ height={60}
className="hidden md:block"
/>
+ <div className="text-center space-y-4 max-w-3xl">
+ <h1 className="text-4xl md:text-5xl font-bold bg-gradient-to-r from-foreground to-foreground/70 bg-clip-text text-transparent">
+ Simple, Transparent Pricing
+ </h1>
+ <p className="text-muted-foreground text-lg md:text-xl">
+ Choose the perfect plan for your development needs. Upgrade or downgrade at any time.
+ </p>
+ </div>
</div>
- <h1 className="text-xl md:text-3xl font-bold text-center">Pricing</h1>
- <p className="text-muted-foreground text-center text-sm md:text-base">
- Choose the plan that fits your needs
- </p>
- <PricingTable />
+ <CustomPricingTable />
</section>
</div>
);
File: src/components/autumn/custom-pricing-table.tsx
Changes:
@@ -0,0 +1,313 @@
+'use client';
+
+import React, { useState } from "react";
+import { useCustomer, usePricingTable, type ProductDetails } from "autumn-js/react";
+import type { Product, ProductItem } from "autumn-js";
+import { cn } from "@/lib/utils";
+import { Button } from "@/components/ui/button";
+import { Switch } from "@/components/ui/switch";
+import { Badge } from "@/components/ui/badge";
+import CheckoutDialog from "@/components/autumn/checkout-dialog";
+import { getPricingTableContent } from "@/lib/autumn/pricing-table-content";
+import { Check, Loader2, Sparkles, Zap } from "lucide-react";
+
+interface CustomPricingTableProps {
+ productDetails?: ProductDetails[];
+ className?: string;
+}
+
+export default function CustomPricingTable({ productDetails, className }: CustomPricingTableProps) {
+ const { customer, checkout } = useCustomer({ errorOnNotFound: false });
+ const [isAnnual, setIsAnnual] = useState(false);
+ const { products, isLoading, error } = usePricingTable({ productDetails });
+
+ if (isLoading) {
+ return (
+ <div className="w-full h-full flex justify-center items-center min-h-[400px]">
+ <div className="flex flex-col items-center gap-3">
+ <Loader2 className="w-8 h-8 text-primary animate-spin" />
+ <p className="text-sm text-muted-foreground">Loading pricing plans...</p>
+ </div>
+ </div>
+ );
+ }
+
+ if (error) {
+ return (
+ <div className="w-full h-full flex justify-center items-center min-h-[400px]">
+ <div className="text-center space-y-2">
+ <p className="text-destructive font-medium">Unable to load pricing plans</p>
+ <p className="text-sm text-muted-foreground">Please try again later</p>
+ </div>
+ </div>
+ );
+ }
+
+ if (!products || products.length === 0) {
+ return null;
+ }
+
+ // Check if we have multiple intervals (monthly/annual)
+ const intervalGroups = products
+ .map((p) => p.properties?.interval_group)
+ .filter((intervalGroup): intervalGroup is string => Boolean(intervalGroup));
+ const intervals = Array.from(new Set(intervalGroups));
+ const multiInterval = intervals.length > 1;
+
+ // Filter products based on selected interval
+ const intervalFilter = (product: Product) => {
+ if (!product.properties?.interval_group) {
+ return true;
+ }
+
+ if (multiInterval) {
+ if (isAnnual) {
+ return product.properties?.interval_group === "year";
+ } else {
+ return product.properties?.interval_group === "month";
+ }
+ }
+
+ return true;
+ };
+
+ const filteredProducts = products.filter(intervalFilter);
+ const hasRecommended = filteredProducts.some((p) => p.display?.recommend_text);
+
+ return (
+ <div className={cn("w-full", className)}>
+ {/* Annual/Monthly Toggle */}
+ {multiInterval && (
+ <div className="flex justify-center mb-12">
+ <div className="inline-flex items-center gap-3 p-1 rounded-full bg-secondary/50 border">
+ <button
+ onClick={() => setIsAnnual(false)}
+ className={cn(
+ "px-6 py-2 rounded-full text-sm font-medium transition-all",
+ !isAnnual
+ ? "bg-primary text-primary-foreground shadow-sm"
+ : "text-muted-foreground hover:text-foreground"
+ )}
+ >
+ Monthly
+ </button>
+ <button
+ onClick={() => setIsAnnual(true)}
+ className={cn(
+ "px-6 py-2 rounded-full text-sm font-medium transition-all relative",
+ isAnnual
+ ? "bg-primary text-primary-foreground shadow-sm"
+ : "text-muted-foreground hover:text-foreground"
+ )}
+ >
+ Annual
+ <Badge className="ml-2 bg-green-500 text-white border-0 text-[10px] py-0 px-1.5">
+ Save 20%
+ </Badge>
+ </button>
+ </div>
+ </div>
+ )}
+
+ {/* Pricing Cards Grid */}
+ <div className={cn(
+ "grid gap-8 max-w-7xl mx-auto",
+ filteredProducts.length === 1 && "grid-cols-1 max-w-md",
+ filteredProducts.length === 2 && "grid-cols-1 md:grid-cols-2",
+ filteredProducts.length === 3 && "grid-cols-1 md:grid-cols-3",
+ filteredProducts.length > 3 && "grid-cols-1 md:grid-cols-2 lg:grid-cols-4"
+ )}>
+ {filteredProducts.map((product, index) => (
+ <PricingCard
+ key={product.id ?? index}
+ product={product}
+ customer={customer}
+ checkout={checkout}
+ isRecommended={hasRecommended && Boolean(product.display?.recommend_text)}
+ />
+ ))}
+ </div>
+ </div>
+ );
+}
+
+interface PricingCardProps {
+ product: Product;
+ customer: any;
+ checkout: any;
+ isRecommended: boolean;
+}
+
+function PricingCard({ product, customer, checkout, isRecommended }: PricingCardProps) {
+ const [loading, setLoading] = useState(false);
+ const { buttonText } = getPricingTableContent(product);
+
+ const handleClick = async () => {
+ if (loading) return;
+
+ setLoading(true);
+ try {
+ if (product.id && customer) {
+ await checkout({
+ productId: product.id,
+ dialog: CheckoutDialog,
+ });
+ } else if (product.display?.button_url) {
+ window.open(product.display?.button_url, "_blank", "noopener,noreferrer");
+ }
+ } catch (error) {
+ console.error("Checkout error:", error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const isDisabled =
+ loading ||
+ (product.scenario === "active" && !product.properties.updateable) ||
+ product.scenario === "scheduled";
+
+ // Get main price
+ const mainPriceDisplay = product.properties?.is_free
+ ? {
+ primary_text: "Free",
+ secondary_text: "",
+ }
+ : product.items?.[0]?.display ?? {
+ primary_text: "Custom",
+ secondary_text: "",
+ };
+
+ // Get features (skip first item if it's the price)
+ const featureItems = product.properties?.is_free
+ ? product.items ?? []
+ : (product.items?.length ?? 0) > 1
+ ? product.items.slice(1)
+ : [];
+
+ // Determine if this is the current plan
+ const isCurrentPlan = product.scenario === "active";
+
+ return (
+ <div
+ className={cn(
+ "relative flex flex-col rounded-2xl border bg-card transition-all duration-300",
+ isRecommended && [
+ "md:scale-105 shadow-xl border-primary/50",
+ "bg-gradient-to-b from-primary/5 to-card"
+ ],
+ !isRecommended && "shadow-sm hover:shadow-md",
+ isCurrentPlan && "ring-2 ring-primary/20"
+ )}
+ >
+ {/* Recommended Badge */}
+ {isRecommended && product.display?.recommend_text && (
+ <div className="absolute -top-4 left-1/2 -translate-x-1/2 z-10">
+ <Badge className="bg-gradient-to-r from-primary to-primary/80 text-primary-foreground border-0 shadow-lg px-4 py-1">
+ <Sparkles className="w-3 h-3 mr-1" />
+ {product.display.recommend_text}
+ </Badge>
+ </div>
+ )}
+
+ <div className="flex-1 p-8 space-y-6">
+ {/* Plan Name & Description */}
+ <div className="space-y-2">
+ <h3 className="text-2xl font-bold">
+ {product.display?.name || product.name}
+ </h3>
+ {product.display?.description && (
+ <p className="text-sm text-muted-foreground line-clamp-2">
+ {product.display.description}
+ </p>
+ )}
+ </div>
+
+ {/* Price */}
+ <div className="space-y-1">
+ <div className="flex items-baseline gap-1">
+ {mainPriceDisplay.primary_text && (
+ <span className="text-4xl font-bold tracking-tight">
+ {mainPriceDisplay.primary_text}
+ </span>
+ )}
+ {mainPriceDisplay.secondary_text && (
+ <span className="text-muted-foreground">
+ {mainPriceDisplay.secondary_text}
+ </span>
+ )}
+ </div>
+ </div>
+
+ {/* Features List */}
+ {featureItems.length > 0 && (
+ <div className="space-y-3 pt-6 border-t">
+ {product.display?.everything_from && (
+ <p className="text-sm text-muted-foreground mb-3">
+ Everything from {product.display.everything_from}, plus:
+ </p>
+ )}
+ <ul className="space-y-3">
+ {featureItems.map((item, index) => (
+ <li key={index} className="flex items-start gap-3">
+ <div className="mt-0.5 shrink-0">
+ <div className={cn(
+ "w-5 h-5 rounded-full flex items-center justify-center",
+ isRecommended
+ ? "bg-primary/10 text-primary"
+ : "bg-muted text-muted-foreground"
+ )}>
+ <Check className="w-3 h-3" />
+ </div>
+ </div>
+ <div className="flex-1 space-y-0.5">
+ <p className="text-sm font-medium leading-tight">
+ {item.display?.primary_text}
+ </p>
+ {item.display?.secondary_text && (
+ <p className="text-xs text-muted-foreground">
+ {item.display.secondary_text}
+ </p>
+ )}
+ </div>
+ </li>
+ ))}
+ </ul>
+ </div>
+ )}
+ </div>
+
+ {/* CTA Button */}
+ <div className="p-8 pt-0">
+ <Button
+ onClick={handleClick}
+ disabled={isDisabled}
+ className={cn(
+ "w-full h-11 text-base font-semibold transition-all group",
+ isRecommended && "bg-primary hover:bg-primary/90 shadow-lg hover:shadow-xl",
+ isCurrentPlan && "bg-secondary hover:bg-secondary/90 text-secondary-foreground"
+ )}
+ variant={isRecommended ? "default" : isCurrentPlan ? "secondary" : "outline"}
+ >
+ {loading ? (
+ <Loader2 className="w-4 h-4 animate-spin" />
+ ) : (
+ <span className="flex items-center gap-2">
+ {isCurrentPlan && <Check className="w-4 h-4" />}
+ {product.display?.button_text || buttonText}
+ {isRecommended && !isCurrentPlan && (
+ <Zap className="w-4 h-4 transition-transform group-hover:translate-x-0.5" />
+ )}
+ </span>
+ )}
+ </Button>
+
+ {isCurrentPlan && (
+ <p className="text-xs text-center text-muted-foreground mt-3">
+ Your current plan
+ </p>
+ )}
+ </div>
+ </div>
+ );
+}
|
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughThe pricing page is redesigned with a larger container, new hero text block, and increased logo size. The static pricing header is replaced with a new CustomPricingTable component that fetches pricing data, supports monthly/annual interval toggling, handles loading and error states, and integrates with a checkout flow via dialog or URL fallback. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant CustomPricingTable
participant usePricingTable
participant PricingCard
participant checkout/URL
User->>CustomPricingTable: Load pricing page
activate CustomPricingTable
CustomPricingTable->>usePricingTable: Fetch pricing data
activate usePricingTable
usePricingTable-->>CustomPricingTable: Return products & intervals
deactivate usePricingTable
rect rgb(200, 220, 255)
Note over CustomPricingTable: Render cards with interval toggle
CustomPricingTable->>CustomPricingTable: Toggle interval (monthly/annual)
end
User->>PricingCard: Click CTA button
activate PricingCard
rect rgb(255, 220, 200)
alt Multiple intervals available
PricingCard->>checkout/URL: checkout() → CheckoutDialog or URL
else Single interval
PricingCard->>checkout/URL: Direct URL navigation
end
end
deactivate PricingCard
deactivate CustomPricingTable
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes
Possibly related PRs
Suggested labels
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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 |
|
🚀 Scrapybara Ubuntu instance started! |
|
🔧 Setting up test environment... Agent Steps |
|
❌ Something went wrong: |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Actionable comments posted: 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
src/app/(home)/pricing/page-content.tsx(1 hunks)src/components/autumn/custom-pricing-table.tsx(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/components/autumn/custom-pricing-table.tsx (4)
src/lib/utils.ts (1)
cn(6-8)src/components/autumn/pricing-table.tsx (1)
PricingCard(185-285)src/lib/autumn/pricing-table-content.tsx (1)
getPricingTableContent(3-66)src/components/autumn/checkout-dialog.tsx (1)
CheckoutDialog(55-151)
src/app/(home)/pricing/page-content.tsx (1)
src/components/autumn/custom-pricing-table.tsx (1)
CustomPricingTable(19-132)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Codacy Security Scan
- GitHub Check: claude-review
| if (product.id && customer) { | ||
| await checkout({ | ||
| productId: product.id, | ||
| dialog: CheckoutDialog, | ||
| }); | ||
| } else if (product.display?.button_url) { | ||
| window.open(product.display?.button_url, "_blank", "noopener,noreferrer"); | ||
| } | ||
| } catch (error) { |
There was a problem hiding this comment.
Remove the customer gate before invoking checkout()
useCustomer().checkout() is documented to work without a populated customer object—it will create or fetch the customer on demand. Gating the call on customer means the CTA silently does nothing whenever the hook hasn’t resolved yet or when a brand-new visitor (no stored customer) clicks it and the product lacks a button_url, effectively breaking the purchase flow.(docs.useautumn.com)
Please drop the customer check so we always call checkout when product.id is available:
- if (product.id && customer) {
+ if (product.id) {
await checkout({
productId: product.id,
dialog: CheckoutDialog,
});
} else if (product.display?.button_url) {
window.open(product.display?.button_url, "_blank", "noopener,noreferrer");
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (product.id && customer) { | |
| await checkout({ | |
| productId: product.id, | |
| dialog: CheckoutDialog, | |
| }); | |
| } else if (product.display?.button_url) { | |
| window.open(product.display?.button_url, "_blank", "noopener,noreferrer"); | |
| } | |
| } catch (error) { | |
| if (product.id) { | |
| await checkout({ | |
| productId: product.id, | |
| dialog: CheckoutDialog, | |
| }); | |
| } else if (product.display?.button_url) { | |
| window.open(product.display?.button_url, "_blank", "noopener,noreferrer"); | |
| } | |
| } catch (error) { |
🤖 Prompt for AI Agents
In src/components/autumn/custom-pricing-table.tsx around lines 150 to 158, the
current logic prevents calling checkout() unless a `customer` exists; remove the
`customer` check so that whenever `product.id` is present we call `checkout({
productId: product.id, dialog: CheckoutDialog })` unconditionally, and keep the
existing else branch that opens `product.display?.button_url` when `product.id`
is falsy; update the conditional to only test `product.id` (and ensure error
handling remains unchanged).
Code Review: Custom Billing UI with Autumn API✅ Strengths
|
Replace default Autumn pricing table with custom implementation featuring gradient accents, responsive grid layout, annual/monthly toggle, and enhanced visual hierarchy for better user experience.
Summary by CodeRabbit
New Features