Billingsdk
blockbilling-settingssource: ok · hit
Billing Settings
A comprehensive billing settings component with tabs for general, payment methods, invoices, and usage limits
npx shadcn add "https://billingsdk.com/r/billing-settings.json"free
Where it goes
settings-form
Use cases
dashboard
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lighticonslucide-react
toastssonner
Registry dependencies
buttoncarddialogswitchbadgedropdown-menudialoginputlabel
Files2 · source cached 10/9/2026, 12:44:33 PM
src/registry/billingsdk/billing-settings.tsx
"use client";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Switch } from "@/components/ui/switch";
import { Badge } from "@/components/ui/badge";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { ChevronDown, CreditCard, Plus } from "lucide-react";
const tabs = [
{ id: "general", label: "General" },
{ id: "payment", label: "Payment" },
{ id: "invoices", label: "Invoices" },
{ id: "limits", label: "Limits" },
];
interface CardInfo {
id: string;
last4: string;
brand: string;
expiry: string;
primary?: boolean;
}
interface BillingSettingsProps {
activeTab: string;
onTabChange: (tab: string) => void;
emailNotifications: boolean;
onEmailNotificationsChange: (value: boolean) => void;
usageAlerts: boolean;
onUsageAlertsChange: (value: boolean) => void;
invoiceReminders: boolean;
onInvoiceRemindersChange: (value: boolean) => void;
cards: CardInfo[];
onAddCard: () => void;
invoiceFormat: "PDF" | "HTML";
onInvoiceFormatChange: (format: "PDF" | "HTML") => void;
onEditBillingAddress: () => void;
overageProtection: boolean;
onOverageProtectionChange: (value: boolean) => void;
usageLimitAlerts: boolean;
onUsageLimitAlertsChange: (value: boolean) => void;
className?: string;
}
interface SettingItemProps {
title: string;
description: string;
checked: boolean;
onCheckedChange: (checked: boolean) => void;
}
function SettingItem({
title,
description,
checked,
onCheckedChange,
}: SettingItemProps) {
const switchId = `switch-${title.toLowerCase().replace(/\s+/g, "-")}`;
return (
<div className="flex items-center justify-between gap-4 py-4">
<div className="min-w-0 flex-1 space-y-0.5">
<label
htmlFor={switchId}
className="text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
{title}
</label>
<p className="text-muted-foreground text-sm">{description}</p>
</div>
<Switch
id={switchId}
checked={checked}
onCheckedChange={onCheckedChange}
className="shrink-0"
/>
</div>
);
}
interface TabNavigationProps {
activeTab: string;
onTabChange: (tab: string) => void;
}
function TabNavigation({ activeTab, onTabChange }: TabNavigationProps) {
return (
<div className="bg-muted text-muted-foreground inline-flex h-9 w-full items-center justify-center gap-1 rounded-lg p-1">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => onTabChange(tab.id)}
className={`ring-offset-background focus-visible:ring-ring inline-flex flex-1 items-center justify-center rounded-md px-2 py-1 text-xs font-medium whitespace-nowrap transition-all focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 sm:px-3 sm:text-sm ${
activeTab === tab.id
? "bg-background text-foreground shadow"
: "hover:bg-background/50 hover:text-foreground"
}`}
>
{tab.label}
</button>
))}
</div>
);
}
export function BillingSettings({
activeTab,
onTabChange,
emailNotifications,
onEmailNotificationsChange,
usageAlerts,
onUsageAlertsChange,
invoiceReminders,
onInvoiceRemindersChange,
cards,
onAddCard,
invoiceFormat,
onInvoiceFormatChange,
onEditBillingAddress,
overageProtection,
onOverageProtectionChange,
usageLimitAlerts,
onUsageLimitAlertsChange,
className,
}: BillingSettingsProps) {
const renderGeneralContent = () => (
<div className="divide-border space-y-0 divide-y">
<SettingItem
title="Email notifications"
description="Receive billing updates via email"
checked={emailNotifications}
onCheckedChange={onEmailNotificationsChange}
/>
<SettingItem
title="Usage alerts"
description="Get notified when approaching limits"
checked={usageAlerts}
onCheckedChange={onUsageAlertsChange}
/>
<SettingItem
title="Invoice reminders"
description="Remind me before auto-renewal"
checked={invoiceReminders}
onCheckedChange={onInvoiceRemindersChange}
/>
</div>
);
const renderPaymentContent = () => (
<div className="space-y-4">
{cards.map((card) => (
<div
key={card.id}
className="flex flex-col justify-between gap-3 rounded-lg border p-3 sm:flex-row sm:items-center sm:p-4"
>
<div className="flex min-w-0 items-center gap-3">
<CreditCard className="text-muted-foreground h-5 w-5 shrink-0" />
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-2">
<span className="font-mono text-sm">•••• {card.last4}</span>
{card.primary && <Badge variant="secondary">Primary</Badge>}
</div>
<p className="text-muted-foreground text-sm">
{card.brand} • Expires {card.expiry}
</p>
</div>
</div>
</div>
))}
<Button variant="outline" className="w-full" onClick={onAddCard}>
<Plus className="mr-2 h-4 w-4" />
Add new card
</Button>
</div>
);
const renderInvoicesContent = () => (
<div className="space-y-6">
<div className="flex flex-col justify-between gap-3 sm:flex-row sm:items-center">
<div className="flex-1 space-y-0.5">
<h3 className="text-sm font-medium">Invoice format</h3>
<p className="text-muted-foreground text-sm">
Choose PDF or HTML format
</p>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="w-full sm:w-auto">
{invoiceFormat}
<ChevronDown className="ml-2 h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => onInvoiceFormatChange("PDF")}>
PDF
</DropdownMenuItem>
<DropdownMenuItem onClick={() => onInvoiceFormatChange("HTML")}>
HTML
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="flex flex-col justify-between gap-3 sm:flex-row sm:items-center">
<div className="flex-1 space-y-0.5">
<h3 className="text-sm font-medium">Billing address</h3>
<p className="text-muted-foreground text-sm">
Update your billing address
</p>
</div>
<Button
variant="outline"
size="sm"
className="w-full sm:w-auto"
onClick={onEditBillingAddress}
>
Edit
</Button>
</div>
</div>
);
const renderLimitsContent = () => (
<div className="divide-border space-y-0 divide-y">
<SettingItem
title="Overage protection"
description="Prevent accidental overages"
checked={overageProtection}
onCheckedChange={onOverageProtectionChange}
/>
<SettingItem
title="Usage limit alerts"
description="Alert at 80% and 95% usage"
checked={usageLimitAlerts}
onCheckedChange={onUsageLimitAlertsChange}
/>
</div>
);
const renderTabContent = () => {
switch (activeTab) {
case "general":
return renderGeneralContent();
case "payment":
return renderPaymentContent();
case "invoices":
return renderInvoicesContent();
case "limits":
return renderLimitsContent();
default:
return renderGeneralContent();
}
};
return (
<div className={`w-full px-4 sm:px-0 ${className || ""}`}>
<Card className="mx-auto w-full max-w-2xl">
<CardHeader className="px-4 sm:px-6">
<CardTitle>Billing Settings</CardTitle>
</CardHeader>
<CardContent className="space-y-6 px-4 sm:px-6">
<TabNavigation activeTab={activeTab} onTabChange={onTabChange} />
{renderTabContent()}
</CardContent>
</Card>
</div>
);
}
src/registry/billingsdk/demo/billing-settings-demo.tsx
"use client";
import { useState } from "react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Toaster, toast } from "sonner";
import { BillingSettings } from "@/components/billingsdk/billing-settings";
import { CreditCard } from "lucide-react";
type InvoiceFormat = "PDF" | "HTML";
interface Card {
id: string;
last4: string;
brand: "Visa" | "MasterCard" | "Amex" | "Other";
expiry: string;
primary: boolean;
}
interface NewCardForm {
number: string;
expiry: string;
cvc: string;
}
export default function BillingSettingsDemo() {
const [activeTab, setActiveTab] = useState<
"general" | "payment" | "invoices" | "limits"
>("general");
const [emailNotifications, setEmailNotifications] = useState<boolean>(true);
const [usageAlerts, setUsageAlerts] = useState<boolean>(true);
const [invoiceReminders, setInvoiceReminders] = useState<boolean>(false);
const [cards, setCards] = useState<Card[]>([
{ id: "1", last4: "4242", brand: "Visa", expiry: "12/25", primary: true },
]);
const [invoiceFormat, setInvoiceFormat] = useState<InvoiceFormat>("PDF");
const [overageProtection, setOverageProtection] = useState<boolean>(true);
const [usageLimitAlerts, setUsageLimitAlerts] = useState<boolean>(true);
const [newCard, setNewCard] = useState<NewCardForm>({
number: "",
expiry: "",
cvc: "",
});
const [open, setOpen] = useState<boolean>(false);
const handleToggleEmailNotifications = (checked: boolean) => {
console.log(`Email notifications toggled to: ${checked}`);
setEmailNotifications(checked);
};
const handleToggleUsageAlerts = (checked: boolean) => {
console.log(`Usage alerts toggled to: ${checked}`);
setUsageAlerts(checked);
};
const handleToggleInvoiceReminders = (checked: boolean) => {
console.log(`Invoice reminders toggled to: ${checked}`);
setInvoiceReminders(checked);
};
const handleChangeInvoiceFormat = (format: InvoiceFormat) => {
console.log(`Invoice format changed to: ${format}`);
setInvoiceFormat(format);
};
const handleEditBillingAddress = () => {
console.log("Edit billing address button clicked");
toast.info("Edit billing address clicked!");
};
const handleToggleOverageProtection = (checked: boolean) => {
console.log(`Overage protection toggled to: ${checked}`);
setOverageProtection(checked);
};
const handleToggleUsageLimitAlerts = (checked: boolean) => {
console.log(`Usage limit alerts toggled to: ${checked}`);
setUsageLimitAlerts(checked);
};
const isValidCardNumber = (number: string): boolean => {
const normalized = number.replace(/\s/g, "");
return (
normalized.length >= 13 &&
normalized.length <= 19 &&
/^\d+$/.test(normalized)
);
};
const isValidExpiry = (expiry: string): boolean => {
if (!/^(0[1-9]|1[0-2])\/?([0-9]{2})$/.test(expiry)) {
return false;
}
const [month, year] = expiry.split("/").map(Number);
const currentYear = Number(String(new Date().getFullYear()).slice(-2));
const currentMonth = new Date().getMonth() + 1;
if (year < currentYear || (year === currentYear && month < currentMonth)) {
return false;
}
return true;
};
const isValidCvc = (cvc: string): boolean => {
return /^\d{3,4}$/.test(cvc);
};
const detectCardBrand = (number: string): Card["brand"] => {
if (number.startsWith("4")) return "Visa";
if (/^5[1-5]/.test(number)) return "MasterCard";
if (/^3[47]/.test(number)) return "Amex";
return "Other";
};
const handleAddCard = (): void => {
if (!isValidCardNumber(newCard.number)) {
toast.error("Please enter a valid card number.");
return;
}
if (!isValidExpiry(newCard.expiry)) {
toast.error(
"Please enter a valid expiry date (MM/YY) that's not in the past.",
);
return;
}
if (!isValidCvc(newCard.cvc)) {
toast.error("Please enter a valid CVC.");
return;
}
const last4 = newCard.number.slice(-4);
const brand = detectCardBrand(newCard.number);
const newCardData: Card = {
id: String(cards.length + 1),
last4,
brand,
expiry: newCard.expiry,
primary: cards.length === 0,
};
setCards([...cards, newCardData]);
setNewCard({ number: "", expiry: "", cvc: "" });
setOpen(false);
toast.success("Card added successfully!");
};
const formatCardNumber = (value: string): string => {
const rawValue = value.replace(/\D/g, "");
const formattedValue = rawValue.match(/.{1,4}/g)?.join(" ") || "";
return formattedValue;
};
return (
<div className="p-6">
<BillingSettings
activeTab={activeTab}
onTabChange={(tab: string) =>
setActiveTab(tab as "general" | "payment" | "invoices" | "limits")
}
emailNotifications={emailNotifications}
onEmailNotificationsChange={handleToggleEmailNotifications}
usageAlerts={usageAlerts}
onUsageAlertsChange={handleToggleUsageAlerts}
invoiceReminders={invoiceReminders}
onInvoiceRemindersChange={handleToggleInvoiceReminders}
cards={cards}
onAddCard={() => setOpen(true)}
invoiceFormat={invoiceFormat}
onInvoiceFormatChange={handleChangeInvoiceFormat}
onEditBillingAddress={handleEditBillingAddress}
overageProtection={overageProtection}
onOverageProtectionChange={handleToggleOverageProtection}
usageLimitAlerts={usageLimitAlerts}
onUsageLimitAlertsChange={handleToggleUsageLimitAlerts}
/>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Add Payment Card</DialogTitle>
<DialogDescription>
Enter your card details to add a new payment method
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="number">Card Number</Label>
<div className="relative">
<Input
id="number"
value={formatCardNumber(newCard.number)}
onChange={(e) => {
const rawValue = e.target.value.replace(/\s/g, "");
if (rawValue.length <= 19) {
setNewCard({ ...newCard, number: rawValue });
}
}}
placeholder="1234 5678 9012 3456"
className="pr-10"
maxLength={19}
/>
<CreditCard className="text-muted-foreground absolute top-1/2 right-3 h-4 w-4 -translate-y-1/2" />
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="expiry">Expiry Date</Label>
<Input
id="expiry"
value={newCard.expiry}
onChange={(e) => {
let value = e.target.value.replace(/\D/g, "");
if (value.length >= 2) {
value = value.slice(0, 2) + "/" + value.slice(2, 4);
}
if (value.length <= 5) {
setNewCard({ ...newCard, expiry: value });
}
}}
placeholder="MM/YY"
maxLength={5}
/>
</div>
<div className="space-y-2">
<Label htmlFor="cvc">CVC</Label>
<Input
id="cvc"
type="password"
value={newCard.cvc}
onChange={(e) => {
const value = e.target.value.replace(/\D/g, "");
if (value.length <= 4) {
setNewCard({ ...newCard, cvc: value });
}
}}
placeholder="123"
maxLength={4}
/>
</div>
</div>
</div>
<DialogFooter className="gap-2 sm:gap-0">
<Button variant="outline" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button onClick={handleAddCard}>Add Card</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Toaster />
</div>
);
}