Billingsdk
blockcancel-subscription-cardsource: ok · hit
Cancel Subscription Card
A cancel subscription card component
npx shadcn add "https://billingsdk.com/r/cancel-subscription-card.json"free
No selection tags yet — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lighticonslucide-react
Registry dependencies
buttonbadgecardutils
Files3 · source cached 10/8/2026, 6:44:04 PM
src/registry/billingsdk/cancel-subscription-card.tsx
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent } from "@/components/ui/card";
import { type Plan } from "@/lib/billingsdk-config";
import { cn } from "@/lib/utils";
import { Circle } from "lucide-react";
export interface CancelSubscriptionCardProps {
title: string;
description: string;
plan: Plan;
leftPanelImageUrl?: string;
warningTitle?: string;
warningText?: string;
keepButtonText?: string;
continueButtonText?: string;
finalTitle?: string;
finalSubtitle?: string;
finalWarningText?: string;
goBackButtonText?: string;
confirmButtonText?: string;
onCancel: (planId: string) => Promise<void> | void;
onKeepSubscription?: (planId: string) => Promise<void> | void;
className?: string;
}
export function CancelSubscriptionCard({
title,
description,
plan,
leftPanelImageUrl,
warningTitle,
warningText,
keepButtonText,
continueButtonText,
finalTitle,
finalSubtitle,
finalWarningText,
goBackButtonText,
confirmButtonText,
onCancel,
onKeepSubscription,
className,
}: CancelSubscriptionCardProps) {
const [showConfirmation, setShowConfirmation] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleContinueCancellation = () => {
setShowConfirmation(true);
setError(null);
};
const handleConfirmCancellation = async () => {
try {
setIsLoading(true);
setError(null);
await onCancel(plan.id);
} catch (err) {
setError(
err instanceof Error ? err.message : "Failed to cancel subscription",
);
} finally {
setIsLoading(false);
}
};
const handleKeepSubscription = async () => {
try {
setIsLoading(true);
setError(null);
if (onKeepSubscription) {
await onKeepSubscription(plan.id);
}
} catch (err) {
setError(
err instanceof Error ? err.message : "Failed to keep subscription",
);
} finally {
setIsLoading(false);
}
};
const handleGoBack = () => {
setShowConfirmation(false);
setError(null);
};
return (
<Card
className={cn(
"flex w-full flex-col overflow-hidden p-0 sm:max-w-[1000px] md:flex-row",
leftPanelImageUrl ? "" : "sm:max-w-[500px]",
className,
)}
>
{leftPanelImageUrl && (
<div className="relative hidden min-h-[500px] w-full overflow-hidden md:block md:w-1/2">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={leftPanelImageUrl}
alt="Cancel Subscription"
className="absolute inset-0 h-full w-full object-cover"
/>
<div className="via-background/30 to-background/90 absolute inset-0 hidden bg-gradient-to-r from-transparent dark:block"></div>
<div className="from-background/80 to-background/20 absolute inset-0 hidden bg-gradient-to-t via-transparent dark:block"></div>
</div>
)}
<CardContent
className={cn(
"flex flex-col gap-4 px-4 py-6",
leftPanelImageUrl ? "w-full md:w-1/2" : "w-full",
)}
>
<div className="flex flex-col gap-2 text-center md:text-left">
<h2 className="text-xl font-semibold md:text-2xl">{title}</h2>
<p className="text-muted-foreground text-xs md:text-sm">
{description}
</p>
{error && (
<div className="bg-destructive/10 border-destructive/20 rounded-md border p-3">
<p className="text-destructive text-sm">{error}</p>
</div>
)}
</div>
{/* Plan Details */}
{!showConfirmation && (
<div className="bg-muted/50 flex flex-col gap-4 rounded-lg p-4">
<div className="flex items-center justify-between">
<div className="flex flex-col gap-1">
<span className="text-lg font-semibold">{plan.title} Plan</span>
<span className="text-muted-foreground text-sm">
Current subscription
</span>
</div>
<Badge variant="secondary">
{parseFloat(plan.monthlyPrice) >= 0
? `${plan.currency}${plan.monthlyPrice}/monthly`
: `${plan.monthlyPrice}/monthly`}
</Badge>
</div>
<div className="flex flex-col gap-2">
{plan.features.slice(0, 4).map((feature, index) => (
<div key={index} className="flex items-center gap-2">
<Circle className="fill-primary text-primary h-2 w-2" />
<span className="text-muted-foreground text-sm">
{feature.name}
</span>
</div>
))}
</div>
</div>
)}
{/* Warning Section */}
{!showConfirmation && (warningTitle || warningText) && (
<div className="bg-muted/30 border-border rounded-lg border p-4">
{warningTitle && (
<h3 className="text-foreground mb-2 font-semibold">
{warningTitle}
</h3>
)}
{warningText && (
<p className="text-muted-foreground text-sm">{warningText}</p>
)}
</div>
)}
{/* Action Buttons */}
{!showConfirmation ? (
<div className="mt-auto flex flex-col gap-3 lg:flex-row">
<Button
className="flex-1"
onClick={handleKeepSubscription}
disabled={isLoading}
>
{isLoading
? "Processing..."
: keepButtonText || "Keep My Subscription"}
</Button>
<Button
variant="destructive"
className="flex-1"
onClick={handleContinueCancellation}
disabled={isLoading}
>
{continueButtonText || "Continue Cancellation"}
</Button>
</div>
) : (
<div className="mt-auto flex flex-col gap-4">
<div className="bg-muted/50 rounded-lg p-4 text-center">
<h3 className="text-foreground mb-2 font-semibold">
{finalTitle || "Final Confirmation"}
</h3>
<p className="text-muted-foreground mb-2 text-sm">
{finalSubtitle ||
"Are you sure you want to cancel your subscription?"}
</p>
<p className="text-destructive text-sm">
{finalWarningText ||
"This action cannot be undone and you'll lose access to all premium features."}
</p>
</div>
<div className="flex flex-col gap-3 lg:flex-row">
<Button
variant="outline"
className="flex-1"
onClick={handleGoBack}
disabled={isLoading}
>
{goBackButtonText || "Go Back"}
</Button>
<Button
variant="destructive"
className="flex-1"
onClick={handleConfirmCancellation}
disabled={isLoading}
>
{isLoading
? "Cancelling..."
: confirmButtonText || "Yes, Cancel Subscription"}
</Button>
</div>
</div>
)}
</CardContent>
</Card>
);
}
src/registry/billingsdk/demo/cancel-subscription-card-demo.tsx
"use client";
import { CancelSubscriptionCard } from "@/components/billingsdk/cancel-subscription-card";
import { plans } from "@/lib/billingsdk-config";
export function CancelSubscriptionCardDemo() {
return (
<div className="flex w-full flex-col">
<CancelSubscriptionCard
title="We're sorry to see you go..."
description={`Before you cancel, we hope you'll consider upgrading to a ${plans[1].title} plan again.`}
plan={plans[1]}
leftPanelImageUrl="https://framerusercontent.com/images/GWE8vop9hubsuh3uWWn0vyuxEg.webp"
warningTitle="You will lose access to your account"
warningText="If you cancel your subscription, you will lose access to your account and all your data will be deleted."
keepButtonText={`Keep My ${plans[1].title} Plan`}
continueButtonText="Continue with Cancellation"
finalTitle="Final Step - Confirm Cancellation"
finalSubtitle="This action will immediately cancel your subscription"
finalWarningText="You'll lose access to all Pro features and your data will be permanently deleted after 30 days."
goBackButtonText="Wait, Go Back"
confirmButtonText="Yes, Cancel My Subscription"
onCancel={async (planId) => {
console.log("Cancelling subscription for plan:", planId);
return new Promise((resolve) => {
setTimeout(() => {
resolve(void 0);
}, 1000);
});
}}
onKeepSubscription={async (planId) => {
console.log("Keeping subscription for plan:", planId);
}}
className="max-w-4xl"
/>
</div>
);
}
src/registry/lib/billingsdk-config.ts
export interface Plan {
id: string;
title: string;
description: string;
highlight?: boolean;
type?: "monthly" | "yearly";
currency?: string;
monthlyPrice: string;
yearlyPrice: string;
buttonText: string;
badge?: string;
features: {
name: string;
icon: string;
iconColor?: string;
}[];
}
export interface CurrentPlan {
plan: Plan;
type: "monthly" | "yearly" | "custom";
price?: string;
nextBillingDate: string;
paymentMethod: string;
status: "active" | "inactive" | "past_due" | "cancelled";
}
export const plans: Plan[] = [
{
id: "starter",
title: "Starter",
description: "For developers testing out Liveblocks locally.",
currency: "$",
monthlyPrice: "0",
yearlyPrice: "0",
buttonText: "Start today for free",
features: [
{
name: "Presence",
icon: "check",
iconColor: "text-green-500",
},
{
name: "Comments",
icon: "check",
iconColor: "text-orange-500",
},
{
name: "Notifications",
icon: "check",
iconColor: "text-teal-500",
},
{
name: "Text Editor",
icon: "check",
iconColor: "text-blue-500",
},
{
name: "Sync Datastore",
icon: "check",
iconColor: "text-zinc-500",
},
],
},
{
id: "pro",
title: "Pro",
description: "For companies adding collaboration in production.",
currency: "$",
monthlyPrice: "20",
yearlyPrice: "199",
buttonText: "Sign up",
badge: "Most popular",
highlight: true,
features: [
{
name: "Presence",
icon: "check",
iconColor: "text-green-500",
},
{
name: "Comments",
icon: "check",
iconColor: "text-orange-500",
},
{
name: "Notifications",
icon: "check",
iconColor: "text-teal-500",
},
{
name: "Text Editor",
icon: "check",
iconColor: "text-blue-500",
},
{
name: "Sync Datastore",
icon: "check",
iconColor: "text-zinc-500",
},
],
},
{
id: "enterprise",
title: "Enterprise",
description:
"For organizations that need more support and compliance features.",
currency: "$",
monthlyPrice: "Custom",
yearlyPrice: "Custom",
buttonText: "Contact sales",
features: [
{
name: "Presence",
icon: "check",
iconColor: "text-green-500",
},
{
name: "Comments",
icon: "check",
iconColor: "text-orange-500",
},
{
name: "Notifications",
icon: "check",
iconColor: "text-teal-500",
},
{
name: "Text Editor",
icon: "check",
iconColor: "text-blue-500",
},
{
name: "Sync Datastore",
icon: "check",
iconColor: "text-zinc-500",
},
],
},
];