|
| 1 | +"use client"; |
| 2 | + |
| 3 | +import { CreditCardIcon, WifiHighIcon } from "@phosphor-icons/react"; |
| 4 | +import { usePersistentState } from "@/hooks/use-persistent-state"; |
| 5 | +import { cn } from "@/lib/utils"; |
| 6 | +import type { CustomerWithPaymentMethod } from "../types/billing"; |
| 7 | + |
| 8 | +type CreditCardDisplayProps = { |
| 9 | + customer: CustomerWithPaymentMethod | null; |
| 10 | +}; |
| 11 | + |
| 12 | +export function CreditCardDisplay({ customer }: CreditCardDisplayProps) { |
| 13 | + const [showCardDetails, setShowCardDetails] = usePersistentState<boolean>( |
| 14 | + "billing-card-details-visible", |
| 15 | + true |
| 16 | + ); |
| 17 | + |
| 18 | + const paymentMethod = customer?.payment_method; |
| 19 | + const card = paymentMethod?.card; |
| 20 | + |
| 21 | + if (!card) { |
| 22 | + return ( |
| 23 | + <div className="flex aspect-[1.586/1] w-full flex-col items-center justify-center rounded-xl border border-dashed bg-background"> |
| 24 | + <CreditCardIcon |
| 25 | + className="mb-2 text-muted-foreground" |
| 26 | + size={28} |
| 27 | + weight="duotone" |
| 28 | + /> |
| 29 | + <span className="text-muted-foreground text-sm">No payment method</span> |
| 30 | + </div> |
| 31 | + ); |
| 32 | + } |
| 33 | + |
| 34 | + const cardHolder = |
| 35 | + paymentMethod?.billing_details?.name || customer?.name || "CARD HOLDER"; |
| 36 | + const last4 = card.last4 || "****"; |
| 37 | + const expMonth = card.exp_month?.toString().padStart(2, "0") || "00"; |
| 38 | + const expYear = card.exp_year?.toString().slice(-2) || "00"; |
| 39 | + const cardNumber = `•••• •••• •••• ${last4}`; |
| 40 | + const expiration = `${expMonth}/${expYear}`; |
| 41 | + const brand = (card.brand || "card").toLowerCase(); |
| 42 | + |
| 43 | + return ( |
| 44 | + <div className="relative aspect-[1.586/1] w-full"> |
| 45 | + <div |
| 46 | + className={cn( |
| 47 | + "absolute inset-0 flex flex-col justify-between overflow-hidden rounded-xl p-4", |
| 48 | + "bg-linear-to-tr from-foreground to-foreground/80", |
| 49 | + "before:pointer-events-none before:absolute before:inset-0 before:z-1 before:rounded-[inherit] before:ring-1 before:ring-white/20 before:ring-inset" |
| 50 | + )} |
| 51 | + > |
| 52 | + <div className="relative z-2 flex items-start justify-between"> |
| 53 | + <WifiHighIcon |
| 54 | + className="rotate-90 text-white/80" |
| 55 | + size={20} |
| 56 | + weight="bold" |
| 57 | + /> |
| 58 | + <span className="font-semibold text-white/60 text-xs uppercase tracking-wider"> |
| 59 | + {brand} |
| 60 | + </span> |
| 61 | + </div> |
| 62 | + |
| 63 | + <div className="relative z-2 flex flex-col gap-2"> |
| 64 | + {showCardDetails ? ( |
| 65 | + <> |
| 66 | + <div className="flex items-end gap-2"> |
| 67 | + <p className="font-semibold text-white/80 text-xs uppercase tracking-wide"> |
| 68 | + {cardHolder} |
| 69 | + </p> |
| 70 | + <p className="ml-auto font-semibold text-white/80 text-xs tabular-nums"> |
| 71 | + {expiration} |
| 72 | + </p> |
| 73 | + </div> |
| 74 | + <div className="flex items-end justify-between gap-3"> |
| 75 | + <button |
| 76 | + aria-label="Hide card details" |
| 77 | + className="cursor-pointer font-semibold text-white tabular-nums tracking-wider transition-opacity hover:opacity-80" |
| 78 | + onClick={() => setShowCardDetails(false)} |
| 79 | + type="button" |
| 80 | + > |
| 81 | + {cardNumber} |
| 82 | + </button> |
| 83 | + <CardBrandLogo brand={brand} /> |
| 84 | + </div> |
| 85 | + </> |
| 86 | + ) : ( |
| 87 | + <> |
| 88 | + <div className="flex items-end gap-2"> |
| 89 | + <p className="font-semibold text-white/40 text-xs uppercase tracking-wide"> |
| 90 | + •••• •••• |
| 91 | + </p> |
| 92 | + <p className="ml-auto font-semibold text-white/40 text-xs tabular-nums"> |
| 93 | + ••/•• |
| 94 | + </p> |
| 95 | + </div> |
| 96 | + <div className="flex items-end justify-between gap-3"> |
| 97 | + <button |
| 98 | + aria-label="Show card details" |
| 99 | + className="cursor-pointer font-semibold text-white/40 tabular-nums tracking-wider transition-opacity hover:opacity-80" |
| 100 | + onClick={() => setShowCardDetails(true)} |
| 101 | + type="button" |
| 102 | + > |
| 103 | + •••• •••• •••• •••• |
| 104 | + </button> |
| 105 | + <CardBrandLogo brand={brand} /> |
| 106 | + </div> |
| 107 | + </> |
| 108 | + )} |
| 109 | + </div> |
| 110 | + </div> |
| 111 | + </div> |
| 112 | + ); |
| 113 | +} |
| 114 | + |
| 115 | +function CardBrandLogo({ brand }: { brand: string }) { |
| 116 | + if (brand === "visa") { |
| 117 | + return ( |
| 118 | + <div className="flex h-6 w-10 items-center justify-center rounded bg-white/10 font-bold text-white text-xs italic"> |
| 119 | + VISA |
| 120 | + </div> |
| 121 | + ); |
| 122 | + } |
| 123 | + if (brand === "mastercard") { |
| 124 | + return ( |
| 125 | + <div className="flex h-6 w-10 items-center justify-center"> |
| 126 | + <div className="relative flex"> |
| 127 | + <div className="h-5 w-5 rounded-full bg-red-500/90" /> |
| 128 | + <div className="-ml-2 h-5 w-5 rounded-full bg-yellow-500/90" /> |
| 129 | + </div> |
| 130 | + </div> |
| 131 | + ); |
| 132 | + } |
| 133 | + if (brand === "amex") { |
| 134 | + return ( |
| 135 | + <div className="flex h-6 w-10 items-center justify-center rounded bg-white/10 font-bold text-[8px] text-white"> |
| 136 | + AMEX |
| 137 | + </div> |
| 138 | + ); |
| 139 | + } |
| 140 | + return ( |
| 141 | + <div className="flex h-6 w-10 items-center justify-center rounded bg-white/10"> |
| 142 | + <CreditCardIcon className="text-white/80" size={16} weight="duotone" /> |
| 143 | + </div> |
| 144 | + ); |
| 145 | +} |
0 commit comments