All components

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.

Open in the catalogue

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

registry/new-york/blocks/billing/billing-payment-failed.tsx
"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>
  );
}

Similar components