-
Notifications
You must be signed in to change notification settings - Fork 1
Build Custom Billing UI with Autumn API #134
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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) { | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+150
to
+158
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove the customer gate before invoking
Please drop the - 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||
| 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> | ||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.