Billing Subscription Settings
Billing Subscription Settings is a React and Tailwind CSS component from HextaUI, licensed MIT. Copy it and paste it into your project.
HextaUI
MIT
other
What it is
Billing Subscription Settings is a React and Tailwind CSS component from HextaUI, licensed MIT. Copy it and paste it into your project.
How to use it
Open it in the catalogue, press copy, and paste it into your project. The prompt you copy carries the code and the rules that tell your agent to reproduce it without changing anything.
Five free copies a month. No card.
No packages beyond the project baseline
Licence
This component comes from HextaUI and is redistributed under the MIT licence, which permits commercial use. The copyright notice travels with the code.
Billing Subscription Settings
"use client";
import { Calendar, Loader2, Pause, Play, RefreshCw, X } from "lucide-react";
import { useState } from "react";
import { cn } from "@/lib/utils";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/registry/new-york/ui/alert-dialog";
import { Badge } from "@/registry/new-york/ui/badge";
import { Button } from "@/registry/new-york/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/registry/new-york/ui/card";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/registry/new-york/ui/select";
import { Separator } from "@/registry/new-york/ui/separator";
import { Switch } from "@/registry/new-york/ui/switch";
export interface SubscriptionSettings {
id: string;
planName: string;
status: "active" | "paused" | "canceled" | "expired";
billingPeriod: "monthly" | "annual";
currentBillingDate: Date;
nextBillingDate?: Date;
pausedUntil?: Date;
canceledAt?: Date;
cancelAtPeriodEnd?: boolean;
autoRenew: boolean;
prorationPreview?: {
newAmount: number;
creditAmount: number;
nextBillingDate: Date;
};
}
export interface BillingSubscriptionSettingsProps {
subscription: SubscriptionSettings;
onPause?: (resumeDate?: Date) => Promise<void>;
onResume?: () => Promise<void>;
onChangeBillingPeriod?: (period: "monthly" | "annual") => Promise<void>;
onUpdateBillingDate?: (date: Date) => Promise<void>;
onCancel?: (feedback?: string) => Promise<void>;
onReactivate?: () => Promise<void>;
onToggleAutoRenew?: (enabled: boolean) => Promise<void>;
className?: string;
currency?: string;
}
function formatDate(date: Date): string {
const year = date.getFullYear();
const month = date.toLocaleString("en-US", { month: "short" });
const day = date.getDate();
return `${month} ${day}, ${year}`;
}
function formatPrice(amount: number, currency = "USD"): string {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency,
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(amount);
}
export default function BillingSubscriptionSettings({
subscription,
onPause,
onResume,
onChangeBillingPeriod,
onUpdateBillingDate,
onCancel,
onReactivate,
onToggleAutoRenew,
className,
currency = "USD",
}: BillingSubscriptionSettingsProps) {
const [isLoading, setIsLoading] = useState(false);
const [showCancelDialog, setShowCancelDialog] = useState(false);
const [cancelFeedback, setCancelFeedback] = useState("");
const [showPauseDialog, setShowPauseDialog] = useState(false);
const [pauseDuration, setPauseDuration] = useState<
"1" | "3" | "6" | "custom"
>("1");
const isActive = subscription.status === "active";
const isPaused = subscription.status === "paused";
const isCanceled = subscription.status === "canceled";
const handlePause = async () => {
if (!onPause) return;
setIsLoading(true);
try {
let resumeDate: Date | undefined;
if (pauseDuration === "1") {
resumeDate = new Date();
resumeDate.setMonth(resumeDate.getMonth() + 1);
} else if (pauseDuration === "3") {
resumeDate = new Date();
resumeDate.setMonth(resumeDate.getMonth() + 3);
} else if (pauseDuration === "6") {
resumeDate = new Date();
resumeDate.setMonth(resumeDate.getMonth() + 6);
}
await onPause(resumeDate);
setShowPauseDialog(false);
} finally {
setIsLoading(false);
}
};
const handleResume = async () => {
if (!onResume) return;
setIsLoading(true);
try {
await onResume();
} finally {
setIsLoading(false);
}
};
const handleChangeBillingPeriod = async (period: "monthly" | "annual") => {
if (!onChangeBillingPeriod) return;
setIsLoading(true);
try {
await onChangeBillingPeriod(period);
} finally {
setIsLoading(false);
}
};
const handleCancel = async () => {
if (!onCancel) return;
setIsLoading(true);
try {
await onCancel(cancelFeedback || undefined);
setShowCancelDialog(false);
setCancelFeedback("");
} finally {
setIsLoading(false);
}
};
const handleReactivate = async () => {
if (!onReactivate) return;
setIsLoading(true);
try {
await onReactivate();
} finally {
setIsLoading(false);
}
};
const handleToggleAutoRenew = async (enabled: boolean) => {
if (!onToggleAutoRenew) return;
setIsLoading(true);
try {
await onToggleAutoRenew(enabled);
} finally {
setIsLoading(false);
}
};
return (
<>
<Card className={cn("w-full shadow-xs", className)}>
<CardHeader>
<div className="flex flex-col gap-1">
<CardTitle>Subscription Settings</CardTitle>
<CardDescription>
Manage your subscription preferences and billing
</CardDescription>
</div>
</CardHeader>
<CardContent>
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<div className="flex flex-col gap-1">
<span className="font-medium text-sm">Plan</span>
<span className="text-muted-foreground text-sm">
{subscription.planName}
</span>
</div>
<Badge
className="shrink-0 text-xs"
variant={
isActive
? "default"
: isPaused
? "secondary"
: "destructive"
}
>
{subscription.status.charAt(0).toUpperCase() +
subscription.status.slice(1)}
</Badge>
</div>
</div>
<Separator />
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<div className="flex flex-col gap-1">
<span className="font-medium text-sm">Billing Period</span>
<span className="text-muted-foreground text-sm">
{subscription.billingPeriod === "monthly"
? "Monthly"
: "Annual"}
</span>
</div>
{onChangeBillingPeriod && isActive && (
<Select
disabled={isLoading}
onValueChange={(value) =>
handleChangeBillingPeriod(value as "monthly" | "annual")
}
value={subscription.billingPeriod ?? "monthly"}
>
<SelectTrigger className="w-[140px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="monthly">Monthly</SelectItem>
<SelectItem value="annual">Annual</SelectItem>
</SelectContent>
</Select>
)}
</div>
</div>
<Separator />
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<Calendar className="size-4 text-muted-foreground" />
<span className="font-medium text-sm">Billing Date</span>
</div>
<span className="text-muted-foreground text-sm">
{subscription.nextBillingDate
? formatDate(subscription.nextBillingDate)
: formatDate(subscription.currentBillingDate)}
</span>
</div>
</div>
</div>
{subscription.pausedUntil && (
<>
<Separator />
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<div className="flex flex-col gap-1">
<span className="font-medium text-sm">
Paused Until
</span>
<span className="text-muted-foreground text-sm">
{formatDate(subscription.pausedUntil)}
</span>
</div>
</div>
</div>
</>
)}
{subscription.canceledAt && (
<>
<Separator />
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<div className="flex flex-col gap-1">
<span className="font-medium text-sm">Canceled On</span>
<span className="text-muted-foreground text-sm">
{formatDate(subscription.canceledAt)}
</span>
</div>
</div>
{subscription.cancelAtPeriodEnd && (
<p className="text-muted-foreground text-xs">
Your subscription will end on{" "}
{subscription.nextBillingDate
? formatDate(subscription.nextBillingDate)
: formatDate(subscription.currentBillingDate)}
</p>
)}
</div>
</>
)}
<Separator />
<div className="flex items-center justify-between">
<div className="flex flex-col gap-1">
<span className="font-medium text-sm">Auto-renewal</span>
<span className="text-muted-foreground text-xs">
{subscription.autoRenew
? "Your subscription will renew automatically"
: "Your subscription will not renew"}
</span>
</div>
{onToggleAutoRenew && isActive && (
<Switch
checked={subscription.autoRenew}
disabled={isLoading}
onCheckedChange={handleToggleAutoRenew}
/>
)}
</div>
{subscription.prorationPreview && (
<>
<Separator />
<div className="flex flex-col gap-2 rounded-lg border bg-muted/50 p-4">
<h4 className="font-medium text-sm">Proration Preview</h4>
<div className="flex flex-col gap-2 text-muted-foreground text-sm">
<div className="flex items-center justify-between">
<span>New Amount:</span>
<span className="font-medium text-foreground">
{formatPrice(
subscription.prorationPreview.newAmount,
currency
)}
</span>
</div>
{subscription.prorationPreview.creditAmount > 0 && (
<div className="flex items-center justify-between">
<span>Credit Applied:</span>
<span className="font-medium text-green-600">
-
{formatPrice(
subscription.prorationPreview.creditAmount,
currency
)}
</span>
</div>
)}
<div className="flex items-center justify-between">
<span>Next Billing Date:</span>
<span className="text-foreground">
{formatDate(
subscription.prorationPreview.nextBillingDate
)}
</span>
</div>
</div>
</div>
</>
)}
</div>
<Separator />
<div className="flex flex-col gap-2">
{isPaused && onResume && (
<Button
aria-busy={isLoading}
className="w-full"
data-loading={isLoading}
onClick={handleResume}
type="button"
>
{isLoading ? (
<>
<Loader2 className="size-4 animate-spin" />
Resuming…
</>
) : (
<>
<Play className="size-4" />
Resume Subscription
</>
)}
</Button>
)}
{isActive && onPause && (
<Button
className="w-full"
onClick={() => setShowPauseDialog(true)}
type="button"
variant="outline"
>
<Pause className="size-4" />
Pause Subscription
</Button>
)}
{isCanceled && onReactivate && (
<Button
aria-busy={isLoading}
className="w-full"
data-loading={isLoading}
onClick={handleReactivate}
type="button"
>
{isLoading ? (
<>
<Loader2 className="size-4 animate-spin" />
Reactivating…
</>
) : (
<>
<RefreshCw className="size-4" />
Reactivate Subscription
</>
)}
</Button>
)}
{isActive && onCancel && (
<Button
className="w-full"
onClick={() => setShowCancelDialog(true)}
type="button"
variant="destructive"
>
<X className="size-4" />
Cancel Subscription
</Button>
)}
</div>
</div>
</CardContent>
</Card>
<AlertDialog onOpenChange={setShowCancelDialog} open={showCancelDialog}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Cancel Subscription</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to cancel your subscription? You'll
lose access to all premium features at the end of your billing
period.
</AlertDialogDescription>
</AlertDialogHeader>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<label className="font-medium text-sm" htmlFor="cancel-feedback">
Reason for canceling (optional)
</label>
<textarea
className="min-h-[80px] w-full rounded-md border bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
id="cancel-feedback"
onChange={(e) => setCancelFeedback(e.target.value)}
placeholder="Help us improve by sharing why you're canceling..."
value={cancelFeedback}
/>
</div>
</div>
<AlertDialogFooter>
<AlertDialogCancel>Keep Subscription</AlertDialogCancel>
<AlertDialogAction
aria-busy={isLoading}
data-loading={isLoading}
onClick={handleCancel}
type="button"
>
{isLoading ? (
<>
<Loader2 className="size-4 animate-spin" />
Canceling…
</>
) : (
"Cancel Subscription"
)}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<AlertDialog onOpenChange={setShowPauseDialog} open={showPauseDialog}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Pause Subscription</AlertDialogTitle>
<AlertDialogDescription>
Your subscription will be paused and you won't be charged
during the pause period. You can resume at any time.
</AlertDialogDescription>
</AlertDialogHeader>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<label className="font-medium text-sm" htmlFor="pause-duration">
Pause Duration
</label>
<Select
onValueChange={(value) =>
setPauseDuration(value as "1" | "3" | "6" | "custom")
}
value={pauseDuration}
>
<SelectTrigger id="pause-duration">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">1 Month</SelectItem>
<SelectItem value="3">3 Months</SelectItem>
<SelectItem value="6">6 Months</SelectItem>
<SelectItem value="custom">Custom Date</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
aria-busy={isLoading}
data-loading={isLoading}
onClick={handlePause}
type="button"
>
{isLoading ? (
<>
<Loader2 className="size-4 animate-spin" />
Pausing…
</>
) : (
"Pause Subscription"
)}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
);
}