{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "subscription-payment-card",
  "type": "registry:component",
  "title": "Subscription payment card",
  "description": "Payment method and next billing summary card. Auto-resolves payment method display labels from Paddle type/brand/last4 fields. Shows next payment amount, date, and portal link to update payment details.",
  "dependencies": ["lucide-react"],
  "registryDependencies": [
    "@paddle/paddle-helpers",
    "card",
    "skeleton",
    "separator"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/subscription-payment-card/components/subscription-payment-card.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { CreditCard, Calendar, ExternalLink } from \"lucide-react\"\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/registry/new-york/ui/card\"\nimport { Skeleton } from \"@/registry/new-york/ui/skeleton\"\nimport { Separator } from \"@/registry/new-york/ui/separator\"\nimport { cn } from \"@/lib/utils\"\nimport type {\n  NextPaymentData,\n  PaymentMethodData,\n} from \"@/registry/new-york/blocks/paddle-helpers/lib/paddle-types\"\nimport { getPaymentMethodDisplay } from \"@/registry/new-york/blocks/paddle-helpers/lib/paddle-payment-method-display\"\nimport { getPaymentMethodIcon } from \"@/registry/new-york/blocks/paddle-helpers/lib/payment-method-icons\"\nimport {\n  formatMoney,\n  formatDate,\n} from \"@/registry/new-york/blocks/paddle-helpers/lib/paddle-format\"\n\nexport type SubscriptionPaymentCardProps = {\n  /**\n   * Next payment details. Absent when subscription is paused or canceled.\n   *\n   * Pass `undefined` for all three data props (`nextPayment`, `paymentMethod`,\n   * `updatePaymentMethodUrl`) to render the skeleton loading state.\n   */\n  nextPayment?: NextPaymentData\n  /**\n   * Payment method details. Pass raw fields from `transaction.payments[0].method_details`\n   * — the component resolves the display label automatically.\n   * Absent when not available from transaction history.\n   */\n  paymentMethod?: PaymentMethodData\n  /** Portal deep link to update payment method. Absent for manual collection. */\n  updatePaymentMethodUrl?: string\n  className?: string\n}\n\nexport function SubscriptionPaymentCard({\n  nextPayment,\n  paymentMethod,\n  updatePaymentMethodUrl,\n  className,\n}: SubscriptionPaymentCardProps) {\n  const isLoading =\n    nextPayment === undefined && paymentMethod === undefined && updatePaymentMethodUrl === undefined\n\n  if (isLoading) {\n    return <SubscriptionPaymentCardSkeleton className={className} />\n  }\n\n  const displayLabel = paymentMethod\n    ? (paymentMethod.label ??\n      getPaymentMethodDisplay(paymentMethod.type, paymentMethod.cardBrand, paymentMethod.last4))\n    : undefined\n\n  const PaymentMethodIcon = paymentMethod\n    ? (getPaymentMethodIcon(paymentMethod.type, paymentMethod.cardBrand) ?? CreditCard)\n    : CreditCard\n\n  const expiryLabel =\n    paymentMethod?.expiryMonth !== undefined && paymentMethod?.expiryYear !== undefined\n      ? `Expires ${String(paymentMethod.expiryMonth).padStart(2, \"0\")}/${String(paymentMethod.expiryYear).slice(-2)}`\n      : undefined\n\n  return (\n    <Card className={cn(\"gap-4\", className)}>\n      <CardHeader>\n        <CardTitle className=\"text-base font-semibold\">Payment</CardTitle>\n      </CardHeader>\n\n      <CardContent className=\"flex flex-col gap-4\">\n        <PaymentInfoRow icon={Calendar} label=\"Next payment\">\n          {nextPayment ? (\n            <>\n              <div className=\"font-semibold\">\n                {formatMoney(nextPayment.amount, nextPayment.currency)}\n              </div>\n              <div className=\"text-sm text-muted-foreground\">{formatDate(nextPayment.date)}</div>\n            </>\n          ) : (\n            <div className=\"text-sm font-medium text-muted-foreground\">No upcoming payment</div>\n          )}\n        </PaymentInfoRow>\n\n        {displayLabel && (\n          <>\n            <Separator />\n            <div className=\"flex items-center justify-between gap-4\">\n              <PaymentInfoRow icon={PaymentMethodIcon} label=\"Payment method\">\n                <div className=\"text-sm font-medium truncate\">{displayLabel}</div>\n                {expiryLabel && <div className=\"text-xs text-muted-foreground\">{expiryLabel}</div>}\n              </PaymentInfoRow>\n\n              {updatePaymentMethodUrl && (\n                <a\n                  href={updatePaymentMethodUrl}\n                  target=\"_blank\"\n                  rel=\"noopener noreferrer\"\n                  className=\"flex items-center gap-1 text-sm font-medium text-primary hover:underline shrink-0\"\n                >\n                  Update\n                  <ExternalLink className=\"size-3\" />\n                </a>\n              )}\n            </div>\n          </>\n        )}\n\n        {/* Update link when no payment method but URL is available */}\n        {!displayLabel && updatePaymentMethodUrl && (\n          <>\n            <Separator />\n            <a\n              href={updatePaymentMethodUrl}\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              className=\"flex items-center gap-1 text-sm font-medium text-primary hover:underline\"\n            >\n              <CreditCard className=\"size-4\" />\n              Update payment method\n              <ExternalLink className=\"size-3 ml-0.5\" />\n            </a>\n          </>\n        )}\n      </CardContent>\n    </Card>\n  )\n}\n\nfunction PaymentInfoRow({\n  icon: Icon,\n  label,\n  children,\n}: {\n  icon: React.ElementType\n  label: string\n  children: React.ReactNode\n}) {\n  return (\n    <div className=\"flex items-start gap-3\">\n      <div className=\"flex size-8 shrink-0 items-center justify-center rounded-md bg-muted\">\n        <Icon className=\"size-4 text-muted-foreground\" />\n      </div>\n      <div className=\"min-w-0\">\n        <div className=\"text-sm text-muted-foreground\">{label}</div>\n        {children}\n      </div>\n    </div>\n  )\n}\n\nfunction SubscriptionPaymentCardSkeleton({ className }: { className?: string }) {\n  return (\n    <Card className={cn(\"gap-4\", className)}>\n      <CardHeader>\n        <Skeleton className=\"h-4 w-20\" />\n      </CardHeader>\n      <CardContent className=\"flex flex-col gap-4\">\n        <div className=\"flex items-start gap-3\">\n          <Skeleton className=\"size-8 rounded-md shrink-0\" />\n          <div className=\"space-y-1.5\">\n            <Skeleton className=\"h-3 w-24\" />\n            <Skeleton className=\"h-5 w-16\" />\n            <Skeleton className=\"h-3 w-20\" />\n          </div>\n        </div>\n        <Separator />\n        <div className=\"flex items-center justify-between gap-4\">\n          <div className=\"flex items-center gap-3\">\n            <Skeleton className=\"size-8 rounded-md shrink-0\" />\n            <div className=\"space-y-1.5\">\n              <Skeleton className=\"h-3 w-28\" />\n              <Skeleton className=\"h-4 w-20\" />\n            </div>\n          </div>\n          <Skeleton className=\"h-4 w-14\" />\n        </div>\n      </CardContent>\n    </Card>\n  )\n}\n",
      "type": "registry:component"
    },
    {
      "path": "registry/new-york/blocks/subscription-payment-card/components/paddle-subscription-payment-card.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n  mapSubscriptionToNextPayment,\n  mapTransactionToPaymentMethod,\n  getUpdatePaymentMethodUrl,\n} from \"@/registry/new-york/blocks/subscription-payment-card/lib/subscription-payment-card-utils\"\nimport {\n  SubscriptionPaymentCard,\n  type SubscriptionPaymentCardProps,\n} from \"./subscription-payment-card\"\n\ntype SubscriptionInput = Parameters<typeof mapSubscriptionToNextPayment>[0]\ntype NextTransactionInput = Parameters<typeof mapSubscriptionToNextPayment>[1]\n\nexport type PaddleSubscriptionPaymentCardProps = {\n  /** Paddle Subscription */\n  subscription: SubscriptionInput\n  /**\n   * `next_transaction` include from the subscription fetch.\n   * Required for accurate next-bill amount. Fetch with:\n   * `paddle.subscriptions.get(id, { include: [\"next_transaction\"] })`\n   */\n  nextTransaction?: NextTransactionInput\n  /**\n   * Most recent completed transaction for the subscription.\n   * Used to determine the stored payment method. Fetch with:\n   * `paddle.transactions.list({ subscriptionId, status: [\"completed\"], perPage: 1 })`\n   */\n  lastTransaction?: NextTransactionInput\n} & Omit<SubscriptionPaymentCardProps, \"nextPayment\" | \"paymentMethod\" | \"updatePaymentMethodUrl\">\n\n/**\n * Paddle-aware wrapper for `SubscriptionPaymentCard`.\n *\n * Accepts raw Paddle subscription and transaction entities, maps them to\n * `NextPaymentData` and `PaymentMethodData`, and renders the UI component.\n *\n * @example\n * <PaddleSubscriptionPaymentCard\n *   subscription={subscription}\n *   nextTransaction={subscription.nextTransaction}\n *   lastTransaction={transactions.data[0]}\n * />\n */\nexport function PaddleSubscriptionPaymentCard({\n  subscription,\n  nextTransaction,\n  lastTransaction,\n  ...uiProps\n}: PaddleSubscriptionPaymentCardProps) {\n  const nextPayment = mapSubscriptionToNextPayment(subscription, nextTransaction)\n  const paymentMethod = mapTransactionToPaymentMethod(lastTransaction)\n  const updatePaymentMethodUrl = getUpdatePaymentMethodUrl(subscription)\n\n  return (\n    <SubscriptionPaymentCard\n      nextPayment={nextPayment}\n      paymentMethod={paymentMethod}\n      updatePaymentMethodUrl={updatePaymentMethodUrl}\n      {...uiProps}\n    />\n  )\n}\n",
      "type": "registry:component"
    },
    {
      "path": "registry/new-york/blocks/subscription-payment-card/lib/subscription-payment-card-utils.ts",
      "content": "import type {\n  NextPaymentData,\n  PaymentMethodData,\n} from \"@/registry/new-york/blocks/paddle-helpers/lib/paddle-types\"\nimport { parseAmount } from \"@/registry/new-york/blocks/paddle-helpers/lib/paddle-format\"\n\n// ---\n// Input shapes — minimal types matching the Paddle Node SDK / API response.\n// ---\n\ntype MethodDetails = {\n  type: string\n  card?: {\n    type?: string | null\n    last4?: string | null\n    expiryMonth?: number | null\n    expiryYear?: number | null\n  } | null\n}\n\ntype PaymentEntry = {\n  methodDetails?: MethodDetails | null\n}\n\ntype PaddleTransaction = {\n  payments?: PaymentEntry[] | null\n  details?: {\n    totals?: {\n      grandTotal?: string | null\n    } | null\n  } | null\n}\n\ntype PaddleSubscription = {\n  currencyCode: string\n  nextBilledAt?: string | null\n  managementUrls?: {\n    updatePaymentMethod?: string | null\n  } | null\n}\n\n/**\n * Extracts the `NextPaymentData` display contract from a Paddle subscription.\n *\n * Requires the `next_transaction` include on the subscription fetch for the\n * accurate next-bill amount (which may differ from the recurring total if\n * there are prorations or one-time charges). Falls back to the subscription's\n * `next_billed_at` for the date. Returns `undefined` when `next_billed_at`\n * is absent (paused or canceled subscriptions).\n *\n * @param subscription - Paddle Subscription\n * @param nextTransaction - `subscription.nextTransaction` from the `next_transaction` include\n * @returns Next payment display data, or `undefined` if no upcoming payment\n *\n * @example\n * const data = await paddle.subscriptions.get(subscriptionId, {\n *   include: [\"next_transaction\"],\n * })\n * const nextPayment = mapSubscriptionToNextPayment(data, data.nextTransaction)\n */\nexport function mapSubscriptionToNextPayment(\n  subscription: PaddleSubscription,\n  nextTransaction?: PaddleTransaction | null\n): NextPaymentData | undefined {\n  if (!subscription.nextBilledAt) return undefined\n\n  const amount = nextTransaction?.details?.totals?.grandTotal\n    ? parseAmount(nextTransaction.details.totals.grandTotal, subscription.currencyCode)\n    : 0\n\n  return {\n    amount,\n    currency: subscription.currencyCode,\n    date: subscription.nextBilledAt,\n  }\n}\n\n/**\n * Extracts the `PaymentMethodData` display contract from the most recent\n * completed Paddle transaction for a subscription.\n *\n * Sourced from `transaction.payments[0].method_details` — the last recorded\n * payment method. Returns `undefined` if no payment entry is present.\n *\n * @param transaction - Most recent completed Paddle Transaction for the subscription\n * @returns Payment method display data, or `undefined` if unavailable\n *\n * @example\n * // Fetch the most recent completed transaction for the subscription\n * const transactions = await paddle.transactions.list({\n *   subscriptionId,\n *   status: [\"completed\"],\n *   perPage: 1,\n * })\n * const paymentMethod = mapTransactionToPaymentMethod(transactions.data[0])\n */\nexport function mapTransactionToPaymentMethod(\n  transaction?: PaddleTransaction | null\n): PaymentMethodData | undefined {\n  const methodDetails = transaction?.payments?.[0]?.methodDetails\n  if (!methodDetails) return undefined\n\n  return {\n    type: methodDetails.type,\n    cardBrand: methodDetails.card?.type ?? undefined,\n    last4: methodDetails.card?.last4 ?? undefined,\n    expiryMonth: methodDetails.card?.expiryMonth ?? undefined,\n    expiryYear: methodDetails.card?.expiryYear ?? undefined,\n  }\n}\n\n/**\n * Returns the `updatePaymentMethodUrl` prop value from a subscription's\n * management URLs.\n *\n * Returns `undefined` for manually-collected subscriptions (where the portal\n * URL is `null`) so the update link is hidden.\n *\n * @param subscription - Paddle Subscription\n * @returns Portal URL, or `undefined` if unavailable\n */\nexport function getUpdatePaymentMethodUrl(subscription: PaddleSubscription): string | undefined {\n  return subscription.managementUrls?.updatePaymentMethod ?? undefined\n}\n",
      "type": "registry:lib"
    }
  ],
  "categories": ["subscription"]
}
