Billing Payment Failed
Billing Payment Failed 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 Payment Failed 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 Payment Failed
"use client";
import {
AlertTriangle,
CreditCard,
Loader2,
Mail,
RefreshCw,
} from "lucide-react";
import { useState } from "react";
import { cn } from "@/lib/utils";
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 { Separator } from "@/registry/new-york/ui/separator";
export interface PaymentFailureDetails {
invoiceId?: string;
invoiceNumber?: string;
amount: number;
currency?: string;
failedAt: Date;
reason:
| "insufficient_funds"
| "card_declined"
| "expired_card"
| "invalid_card"
| "processing_error"
| "fraud_detected"
| "other";
reasonMessage?: string;
paymentMethod?: {
type: string;
last4?: string;
brand?: string;
expiresAt?: Date;
};
gracePeriodEndsAt?: Date;
retryAttempts?: number;
maxRetryAttempts?: number;
}
export interface BillingPaymentFailedProps {
failure: PaymentFailureDetails;
onRetry?: () => Promise<void>;
onUpdatePaymentMethod?: () => void;
onContactSupport?: () => 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);
}
function getReasonConfig(reason: PaymentFailureDetails["reason"]) {
switch (reason) {
case "insufficient_funds":
return {
title: "Insufficient Funds",
message:
"Your payment was declined due to insufficient funds in your account.",
action:
"Please ensure you have sufficient funds or use a different payment method.",
};
case "card_declined":
return {
title: "Card Declined",
message: "Your card was declined by your bank.",
action: "Please contact your bank or use a different payment method.",
};
case "expired_card":
return {
title: "Expired Card",
message: "Your payment card has expired.",
action: "Please update your payment method with a valid card.",
};
case "invalid_card":
return {
title: "Invalid Card",
message: "The card information provided is invalid.",
action:
"Please verify your card details or use a different payment method.",
};
case "processing_error":
return {
title: "Processing Error",
message: "An error occurred while processing your payment.",
action: "Please try again or contact support if the issue persists.",
};
case "fraud_detected":
return {
title: "Fraud Detection",
message: "Your payment was flagged for security reasons.",
action: "Please contact support to resolve this issue.",
};
case "other":
return {
title: "Payment Failed",
message: "Your payment could not be processed.",
action: "Please try again or contact support for assistance.",
};
default:
return {
title: "Payment Failed",
message: "Your payment could not be processed.",
action: "Please try again or contact support for assistance.",
};
}
}
export default function BillingPaymentFailed({
failure,
onRetry,
onUpdatePaymentMethod,
onContactSupport,
className,
currency = "USD",
}: BillingPaymentFailedProps) {
const [isRetrying, setIsRetrying] = useState(false);
const reasonConfig = getReasonConfig(failure.reason);
const failureCurrency = failure.currency || currency;
const isCardExpired =
failure.reason === "expired_card" ||
(failure.paymentMethod?.expiresAt &&
new Date(failure.paymentMethod.expiresAt) < new Date());
const handleRetry = async () => {
if (!onRetry) return;
setIsRetrying(true);
try {
await onRetry();
} finally {
setIsRetrying(false);
}
};
return (
<Card className={cn("w-full border-destructive/50 shadow-xs", className)}>
<CardHeader>
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div className="flex min-w-0 flex-1 flex-col gap-2">
<div className="flex flex-wrap items-center gap-2">
<AlertTriangle className="size-5 shrink-0 text-destructive" />
<CardTitle className="wrap-break-word text-destructive">
{reasonConfig.title}
</CardTitle>
</div>
<CardDescription className="wrap-break-word">
{failure.reasonMessage || reasonConfig.message}
</CardDescription>
</div>
<Badge className="shrink-0 text-xs" variant="destructive">
Failed
</Badge>
</div>
</CardHeader>
<CardContent>
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-4 rounded-lg border bg-destructive/5 p-4">
<div className="flex flex-col gap-2">
<h3 className="font-medium text-sm">Payment Details</h3>
<div className="flex flex-col gap-2 text-muted-foreground text-sm">
{failure.invoiceNumber && (
<div className="flex flex-wrap items-center gap-2">
<span>Invoice:</span>
<span className="font-medium text-foreground">
{failure.invoiceNumber}
</span>
</div>
)}
<div className="flex flex-wrap items-center gap-2">
<span>Amount:</span>
<span className="font-medium text-foreground">
{formatPrice(failure.amount, failureCurrency)}
</span>
</div>
<div className="flex flex-wrap items-center gap-2">
<span>Failed on:</span>
<span className="text-foreground">
{formatDate(failure.failedAt)}
</span>
</div>
{failure.paymentMethod && (
<div className="flex flex-wrap items-center gap-2">
<span>Payment Method:</span>
<div className="flex flex-wrap items-center gap-2 text-foreground">
<span className="capitalize">
{failure.paymentMethod.type}
</span>
{failure.paymentMethod.brand && (
<>
<span aria-hidden="true">•</span>
<span className="capitalize">
{failure.paymentMethod.brand}
</span>
</>
)}
{failure.paymentMethod.last4 && (
<>
<span aria-hidden="true">•</span>
<span>•••• {failure.paymentMethod.last4}</span>
</>
)}
{isCardExpired && (
<>
<span aria-hidden="true">•</span>
<Badge className="text-xs" variant="destructive">
Expired
</Badge>
</>
)}
</div>
</div>
)}
{failure.retryAttempts !== undefined &&
failure.maxRetryAttempts !== undefined && (
<div className="flex flex-wrap items-center gap-2">
<span>Retry Attempts:</span>
<span className="text-foreground">
{failure.retryAttempts} / {failure.maxRetryAttempts}
</span>
</div>
)}
</div>
</div>
{failure.gracePeriodEndsAt && (
<>
<Separator />
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2 font-medium text-sm text-yellow-600">
<AlertTriangle className="size-4" />
<span>Grace Period</span>
</div>
<p className="text-muted-foreground text-sm">
Your service will continue until{" "}
<span className="font-medium text-foreground">
{formatDate(failure.gracePeriodEndsAt)}
</span>
. Please update your payment method before this date to
avoid service interruption.
</p>
</div>
</>
)}
</div>
<div className="flex flex-col gap-2">
<p className="wrap-break-word text-muted-foreground text-sm">
{reasonConfig.action}
</p>
</div>
<Separator />
<div className="flex flex-col gap-2">
{onRetry && (
<Button
aria-busy={isRetrying}
className="w-full"
data-loading={isRetrying}
onClick={handleRetry}
type="button"
>
{isRetrying ? (
<>
<Loader2 className="size-4 animate-spin" />
Retrying…
</>
) : (
<>
<RefreshCw className="size-4" />
Retry Payment
</>
)}
</Button>
)}
{onUpdatePaymentMethod && (
<Button
className="w-full"
onClick={onUpdatePaymentMethod}
type="button"
variant="outline"
>
<CreditCard className="size-4" />
Update Payment Method
</Button>
)}
{onContactSupport && (
<Button
className="w-full"
onClick={onContactSupport}
type="button"
variant="ghost"
>
<Mail className="size-4" />
Contact Support
</Button>
)}
</div>
</div>
</CardContent>
</Card>
);
}