@carlonicora/nextjs-jsonapi 5.7.3 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist/{BlockNoteEditor-R3745ALQ.mjs → BlockNoteEditor-FZNU4YQT.mjs} +4 -4
  2. package/dist/{BlockNoteEditor-G45WMJ5V.js → BlockNoteEditor-SAU3G6H7.js} +19 -19
  3. package/dist/{BlockNoteEditor-G45WMJ5V.js.map → BlockNoteEditor-SAU3G6H7.js.map} +1 -1
  4. package/dist/{BlockNoteViewer-TNK3OMH5.mjs → BlockNoteViewer-6XDDVTN6.mjs} +2 -2
  5. package/dist/{BlockNoteViewer-5PGOVURC.js → BlockNoteViewer-7BTV7QQA.js} +3 -3
  6. package/dist/{BlockNoteViewer-5PGOVURC.js.map → BlockNoteViewer-7BTV7QQA.js.map} +1 -1
  7. package/dist/billing/index.js +492 -506
  8. package/dist/billing/index.js.map +1 -1
  9. package/dist/billing/index.mjs +5 -19
  10. package/dist/billing/index.mjs.map +1 -1
  11. package/dist/{chunk-CP7BXAFH.js → chunk-F4JB7YAP.js} +8 -8
  12. package/dist/{chunk-CP7BXAFH.js.map → chunk-F4JB7YAP.js.map} +1 -1
  13. package/dist/{chunk-KOA5PZZA.mjs → chunk-GFGKMUPZ.mjs} +2 -2
  14. package/dist/{chunk-EXG4ANC7.js → chunk-IPYXT7MH.js} +868 -868
  15. package/dist/{chunk-EXG4ANC7.js.map → chunk-IPYXT7MH.js.map} +1 -1
  16. package/dist/{chunk-RJMLAW5O.mjs → chunk-JVJS5RHB.mjs} +2 -2
  17. package/dist/{chunk-WU47MIZZ.js → chunk-MTI2UXDA.js} +4 -4
  18. package/dist/{chunk-WU47MIZZ.js.map → chunk-MTI2UXDA.js.map} +1 -1
  19. package/dist/{chunk-6KAYEYFU.mjs → chunk-RYZYKG6W.mjs} +2 -2
  20. package/dist/{chunk-3CSPHMVK.js → chunk-T3YQ47EF.js} +2 -32
  21. package/dist/chunk-T3YQ47EF.js.map +1 -0
  22. package/dist/{chunk-YG4YFAYY.js → chunk-TPZS46IM.js} +3 -3
  23. package/dist/{chunk-YG4YFAYY.js.map → chunk-TPZS46IM.js.map} +1 -1
  24. package/dist/{chunk-JCNB5KTQ.mjs → chunk-U7DR5VDT.mjs} +5 -5
  25. package/dist/{chunk-CJS5WA4E.mjs → chunk-USYZNWKS.mjs} +2 -32
  26. package/dist/chunk-USYZNWKS.mjs.map +1 -0
  27. package/dist/client/index.js +4 -4
  28. package/dist/client/index.mjs +3 -3
  29. package/dist/components/index.js +4 -4
  30. package/dist/components/index.mjs +3 -3
  31. package/dist/contexts/index.js +4 -4
  32. package/dist/contexts/index.mjs +3 -3
  33. package/dist/core/index.d.mts +0 -12
  34. package/dist/core/index.d.ts +0 -12
  35. package/dist/core/index.js +2 -2
  36. package/dist/core/index.mjs +1 -1
  37. package/dist/email-automations/index.js +92 -92
  38. package/dist/email-automations/index.mjs +3 -3
  39. package/dist/features/analytics/index.js +121 -121
  40. package/dist/features/analytics/index.mjs +4 -4
  41. package/dist/features/analytics/server.js +4 -4
  42. package/dist/features/analytics/server.mjs +2 -2
  43. package/dist/features/blog/index.js +150 -150
  44. package/dist/features/blog/index.mjs +3 -3
  45. package/dist/features/blog/server.js +16 -16
  46. package/dist/features/blog/server.mjs +1 -1
  47. package/dist/features/help/index.js +37 -37
  48. package/dist/features/help/index.mjs +3 -3
  49. package/dist/features/tokenusage/index.js +124 -124
  50. package/dist/features/tokenusage/index.mjs +4 -4
  51. package/dist/index.js +3 -3
  52. package/dist/index.mjs +2 -2
  53. package/dist/server/index.js +3 -3
  54. package/dist/server/index.mjs +2 -2
  55. package/messages/en.json +2 -1
  56. package/messages/it.json +2 -1
  57. package/package.json +1 -1
  58. package/src/features/__tests__/i18n-keys.contract.spec.ts +4 -0
  59. package/src/features/billing/components/cards/SubscriptionSummaryCard.tsx +22 -14
  60. package/src/features/billing/i18n-keys.ts +1 -0
  61. package/src/features/billing/stripe-subscription/components/details/SubscriptionDetails.tsx +1 -21
  62. package/src/features/billing/stripe-subscription/data/__tests__/stripe-subscription.spec.ts +59 -0
  63. package/src/features/billing/stripe-subscription/data/stripe-subscription.service.ts +0 -34
  64. package/src/features/billing/stripe-subscription/data/stripe-subscription.ts +1 -1
  65. package/dist/chunk-3CSPHMVK.js.map +0 -1
  66. package/dist/chunk-CJS5WA4E.mjs.map +0 -1
  67. /package/dist/{BlockNoteEditor-R3745ALQ.mjs.map → BlockNoteEditor-FZNU4YQT.mjs.map} +0 -0
  68. /package/dist/{BlockNoteViewer-TNK3OMH5.mjs.map → BlockNoteViewer-6XDDVTN6.mjs.map} +0 -0
  69. /package/dist/{chunk-KOA5PZZA.mjs.map → chunk-GFGKMUPZ.mjs.map} +0 -0
  70. /package/dist/{chunk-RJMLAW5O.mjs.map → chunk-JVJS5RHB.mjs.map} +0 -0
  71. /package/dist/{chunk-6KAYEYFU.mjs.map → chunk-RYZYKG6W.mjs.map} +0 -0
  72. /package/dist/{chunk-JCNB5KTQ.mjs.map → chunk-U7DR5VDT.mjs.map} +0 -0
@@ -1 +1 @@
1
- {"version":3,"sources":["/home/runner/work/nextjs-jsonapi/nextjs-jsonapi/dist/billing/index.js","../../src/features/billing/components/cards/SubscriptionSummaryCard.tsx","../../src/features/billing/components/cards/PaymentMethodSummaryCard.tsx","../../src/features/billing/components/cards/CustomerInfoCard.tsx","../../src/features/billing/components/cards/InvoicesSummaryCard.tsx","../../src/features/billing/components/cards/BillingUsageSummaryCard.tsx","../../src/features/billing/components/containers/BillingDashboardContainer.tsx","../../src/features/billing/stripe-customer/components/containers/PaymentMethodsContainer.tsx","../../src/features/billing/stripe-customer/components/forms/PaymentMethodEditor.tsx","../../src/features/billing/stripe-customer/components/forms/PaymentMethodForm.tsx","../../src/features/billing/stripe-customer/components/details/PaymentMethodCard.tsx","../../src/features/billing/stripe-customer/components/lists/PaymentMethodsList.tsx","../../src/features/billing/stripe-invoice/components/containers/InvoicesContainer.tsx","../../src/features/billing/stripe-invoice/components/lists/InvoicesList.tsx","../../src/features/billing/stripe-invoice/components/details/InvoiceDetails.tsx","../../src/features/billing/stripe-invoice/components/widgets/InvoiceStatusBadge.tsx","../../src/features/billing/stripe-subscription/components/containers/SubscriptionsContainer.tsx","../../src/features/billing/stripe-subscription/components/lists/SubscriptionsList.tsx","../../src/features/billing/stripe-subscription/components/details/SubscriptionDetails.tsx","../../src/features/billing/stripe-subscription/components/forms/CancelSubscriptionDialog.tsx","../../src/features/billing/stripe-subscription/components/widgets/SubscriptionStatusBadge.tsx","../../src/features/billing/stripe-subscription/components/widgets/IntervalToggle.tsx","../../src/features/billing/stripe-subscription/components/widgets/PricingCard.tsx","../../src/features/billing/stripe-subscription/components/widgets/ProductPricingList.tsx","../../src/features/billing/stripe-subscription/components/widgets/ProductPricingRow.tsx","../../src/features/billing/stripe-subscription/components/widgets/ProrationPreview.tsx","../../src/features/billing/stripe-subscription/components/widgets/SubscriptionConfirmation.tsx","../../src/features/billing/stripe-subscription/components/wizards/SubscriptionWizard.tsx","../../src/features/billing/stripe-subscription/hooks/useSubscriptionWizard.ts","../../src/features/billing/stripe-subscription/hooks/wizard-promotion.ts","../../src/features/billing/stripe-subscription/hooks/useConfirmSubscriptionPayment.ts","../../src/features/billing/stripe-subscription/hooks/wizard-payment.ts","../../src/features/billing/stripe-subscription/components/wizards/WizardProgressIndicator.tsx","../../src/features/billing/stripe-subscription/components/wizards/WizardStepPlanSelection.tsx","../../src/features/billing/stripe-subscription/components/wizards/WizardStepReview.tsx","../../src/features/billing/stripe-promotion-code/components/PromoCodeInput.tsx","../../src/features/billing/stripe-subscription/components/wizards/review-card.ts","../../src/features/billing/stripe-subscription/components/wizards/WizardStepPaymentMethod.tsx","../../src/features/billing/stripe-usage/components/containers/UsageContainer.tsx","../../src/features/billing/stripe-usage/components/details/UsageSummaryCard.tsx","../../src/features/billing/stripe-usage/components/widgets/UsageSummaryCards.tsx","../../src/features/billing/stripe-usage/components/lists/UsageHistoryTable.tsx","../../src/features/billing/components/modals/BillingDetailModal.tsx","../../src/features/billing/components/utils/url.ts","../../src/features/billing/components/widgets/BillingAlertBanner.tsx","../../src/features/billing/components/providers/StripeProvider.tsx","../../src/features/billing/contexts/PriceContext.tsx","../../src/features/billing/stripe-price/components/forms/PriceArchiver.tsx","../../src/features/billing/stripe-price/components/forms/PriceEditor.tsx","../../src/features/billing/contexts/ProductContext.tsx","../../src/features/billing/stripe-product/components/forms/ProductArchiver.tsx","../../src/features/billing/stripe-product/components/forms/ProductEditor.tsx","../../src/features/billing/stripe-price/components/details/PriceDetails.tsx","../../src/features/billing/stripe-price/components/containers/PriceContainer.tsx","../../src/features/billing/stripe-price/components/lists/PricesList.tsx","../../src/features/billing/stripe-price/data/stripe-price.transfer.ts","../../src/features/billing/stripe-product/components/containers/ProductContainer.tsx","../../src/features/billing/stripe-product/components/details/ProductDetails.tsx","../../src/features/billing/stripe-product/components/containers/ProductsListContainer.tsx","../../src/features/billing/stripe-coupon/components/lists/CouponsList.tsx","../../src/features/billing/stripe-product/components/lists/ProductsList.tsx","../../src/features/billing/stripe-coupon/components/containers/CouponContainer.tsx","../../src/features/billing/stripe-coupon/components/details/CouponDetails.tsx","../../src/features/billing/stripe-coupon/components/lists/CouponCodesList.tsx","../../src/features/billing/stripe-coupon/components/forms/CouponCodeEditor.tsx"],"names":["useTranslations","getStatusBadgeVariant","useEffect","statusConfig","formatPlanName","hasMonthly","hasYearly","formatInterval","formatDate","hasMeteredSubscriptions","formSchema","z","defaultContextValue","read","document"],"mappings":"AAAA;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,uDAA6B;AAC7B;AACE;AACF,uDAA6B;AAC7B;AACE;AACF,uDAA6B;AAC7B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,uDAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B;AACE;AACF,uDAA6B;AAC7B;AACA;ACzHA,qCAAgC;AAChC,2CAAyC;AAmDjC,+CAAA;AAtCR,IAAM,sBAAA,EAAkC,MAAA,CAAO,MAAA,CAAO,mCAAkB,CAAA;AAExE,SAAS,qBAAA,CAAsB,MAAA,EAAiF;AAC9G,EAAA,OAAA,CAAQ,MAAA,EAAQ;AAAA,IACd,KAAA,qBAAA;AACE,MAAA,OAAO,SAAA;AAAA,IACT,KAAA,yBAAA;AACE,MAAA,OAAO,WAAA;AAAA,IACT,KAAA,yBAAA;AAAA,IACA,KAAA,qBAAA;AAAA,IACA,KAAA,yBAAA;AACE,MAAA,OAAO,aAAA;AAAA,IACT,OAAA;AACE,MAAA,OAAO,SAAA;AAAA,EACX;AACF;AAbS,qCAAA,qBAAA,EAAA,uBAAA,CAAA;AAeT,SAAS,cAAA,CAAe,YAAA,EAA2C,QAAA,EAA0B;AAC3F,EAAA,MAAM,YAAA,kBAAc,YAAA,qBAAa,KAAA,6BAAO,OAAA,6BAAS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","file":"/home/runner/work/nextjs-jsonapi/nextjs-jsonapi/dist/billing/index.js","sourcesContent":[null,"\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { ChevronRight, CreditCard } from \"lucide-react\";\nimport { Badge, Button, Card, CardContent, CardHeader, CardTitle, Skeleton } from \"../../../../shadcnui\";\nimport { StripeSubscriptionInterface, SubscriptionStatus } from \"../../stripe-subscription\";\nimport { formatDate, useBillingFormatOptions } from \"../utils\";\n\ntype SubscriptionSummaryCardProps = {\n subscriptions: StripeSubscriptionInterface[];\n loading?: boolean;\n error?: string;\n onManageClick: () => void;\n};\n\n/** Statuses with a `billing.ui.subscription_status.*` label; anything else Stripe adds is shown raw. */\nconst SUBSCRIPTION_STATUSES: string[] = Object.values(SubscriptionStatus);\n\nfunction getStatusBadgeVariant(status: SubscriptionStatus): \"default\" | \"secondary\" | \"destructive\" | \"outline\" {\n switch (status) {\n case SubscriptionStatus.ACTIVE:\n return \"default\";\n case SubscriptionStatus.TRIALING:\n return \"secondary\";\n case SubscriptionStatus.PAST_DUE:\n case SubscriptionStatus.UNPAID:\n case SubscriptionStatus.CANCELED:\n return \"destructive\";\n default:\n return \"outline\";\n }\n}\n\nfunction formatPlanName(subscription: StripeSubscriptionInterface, fallback: string): string {\n const productName = subscription.price?.product?.name || \"\";\n const nickname = subscription.price?.nickname || \"\";\n\n if (productName && nickname) {\n return `${productName} - ${nickname}`;\n }\n return productName || nickname || fallback;\n}\n\nexport function SubscriptionSummaryCard({\n subscriptions,\n loading,\n error,\n onManageClick,\n}: SubscriptionSummaryCardProps) {\n const t = useTranslations();\n const fmt = useBillingFormatOptions();\n if (loading) {\n return (\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle>{t(\"billing.ui.subscriptions.title\")}</CardTitle>\n <CreditCard className=\"h-4 w-4 text-muted-foreground\" />\n </CardHeader>\n <CardContent>\n <Skeleton className=\"h-6 w-32 mb-2\" />\n <Skeleton className=\"h-4 w-24 mb-1\" />\n <Skeleton className=\"h-4 w-40\" />\n </CardContent>\n </Card>\n );\n }\n\n if (error) {\n return (\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle>{t(\"billing.ui.subscriptions.title\")}</CardTitle>\n <CreditCard className=\"h-4 w-4 text-muted-foreground\" />\n </CardHeader>\n <CardContent>\n <p className=\"text-destructive text-xs/relaxed\">{error}</p>\n </CardContent>\n </Card>\n );\n }\n\n const activeSubscriptions = subscriptions.filter(\n (sub) => sub.status === SubscriptionStatus.ACTIVE || sub.status === SubscriptionStatus.TRIALING,\n );\n const primarySubscription = activeSubscriptions[0];\n\n return (\n <Card className=\"cursor-pointer hover:bg-accent/50 transition-colors\" onClick={onManageClick}>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle>{t(\"billing.ui.subscriptions.title\")}</CardTitle>\n <CreditCard className=\"h-4 w-4 text-muted-foreground\" />\n </CardHeader>\n <CardContent>\n {/* No active or trialing plan (none at all, or only unpaid / ended ones): offer the plans */}\n {!primarySubscription ? (\n <div className=\"space-y-2\">\n <p className=\"text-xl font-bold text-muted-foreground\">{t(\"billing.ui.cards.subscriptions.empty_title\")}</p>\n <p className=\"text-xs text-muted-foreground\">{t(\"billing.ui.cards.subscriptions.empty_description\")}</p>\n <Button\n variant=\"outline\"\n size=\"sm\"\n className=\"mt-2\"\n onClick={(e) => {\n e.stopPropagation();\n onManageClick();\n }}\n >\n {t(\"billing.ui.cards.subscriptions.view_plans\")}\n <ChevronRight className=\"h-4 w-4 ms-1 rtl:rotate-180\" />\n </Button>\n </div>\n ) : (\n <div className=\"space-y-2\">\n <div className=\"flex items-center gap-2\">\n <p className=\"text-xl font-bold\">\n {formatPlanName(primarySubscription, t(\"billing.ui.cards.subscriptions.fallback_name\"))}\n </p>\n <Badge\n variant={\n primarySubscription.cancelAtPeriodEnd\n ? \"secondary\"\n : getStatusBadgeVariant(primarySubscription.status)\n }\n >\n {primarySubscription.cancelAtPeriodEnd\n ? t(\"billing.ui.cards.subscriptions.canceling\")\n : SUBSCRIPTION_STATUSES.includes(primarySubscription.status)\n ? t(`billing.ui.subscription_status.${primarySubscription.status}`)\n : primarySubscription.status}\n </Badge>\n </div>\n <p className=\"text-xs text-muted-foreground\">\n {!primarySubscription.price?.isTrial\n ? t(\"billing.ui.cards.subscriptions.ends_on\", {\n date: formatDate(primarySubscription.currentPeriodEnd, fmt),\n })\n : primarySubscription.cancelAtPeriodEnd\n ? t(\"billing.ui.cards.subscriptions.cancels_on\", {\n date: formatDate(primarySubscription.currentPeriodEnd, fmt),\n })\n : t(\"billing.ui.cards.subscriptions.renews_on\", {\n date: formatDate(primarySubscription.currentPeriodEnd, fmt),\n })}\n </p>\n {activeSubscriptions.length > 1 && (\n <p className=\"text-xs text-muted-foreground\">\n {t(\"billing.ui.cards.subscriptions.more\", { count: activeSubscriptions.length - 1 })}\n </p>\n )}\n </div>\n )}\n </CardContent>\n </Card>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { Wallet, ChevronRight } from \"lucide-react\";\nimport { Card, CardContent, CardHeader, CardTitle, Button, Skeleton } from \"../../../../shadcnui\";\nimport { PaymentMethodInterface } from \"../../stripe-customer\";\n\ntype PaymentMethodSummaryCardProps = {\n paymentMethods: PaymentMethodInterface[];\n defaultPaymentMethodId?: string;\n loading?: boolean;\n error?: string;\n onManageClick: () => void;\n};\n\nfunction getCardBrandIcon(brand: string): string {\n const brandMap: Record<string, string> = {\n visa: \"Visa\",\n mastercard: \"Mastercard\",\n amex: \"Amex\",\n discover: \"Discover\",\n diners: \"Diners\",\n jcb: \"JCB\",\n unionpay: \"UnionPay\",\n };\n return brandMap[brand.toLowerCase()] || brand;\n}\n\nexport function PaymentMethodSummaryCard({\n paymentMethods,\n defaultPaymentMethodId,\n loading,\n error,\n onManageClick,\n}: PaymentMethodSummaryCardProps) {\n const t = useTranslations();\n if (loading) {\n return (\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle>{t(\"billing.ui.payment_method.title\")}</CardTitle>\n <Wallet className=\"h-4 w-4 text-muted-foreground\" />\n </CardHeader>\n <CardContent>\n <Skeleton className=\"h-6 w-32 mb-2\" />\n <Skeleton className=\"h-4 w-24\" />\n </CardContent>\n </Card>\n );\n }\n\n if (error) {\n return (\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle>{t(\"billing.ui.payment_method.title\")}</CardTitle>\n <Wallet className=\"h-4 w-4 text-muted-foreground\" />\n </CardHeader>\n <CardContent>\n <p className=\"text-destructive text-xs/relaxed\">{error}</p>\n </CardContent>\n </Card>\n );\n }\n\n // Find default payment method or use first one\n const defaultMethod = paymentMethods.find((pm) => pm.id === defaultPaymentMethodId) || paymentMethods[0];\n\n return (\n <Card className=\"cursor-pointer hover:bg-accent/50 transition-colors\" onClick={onManageClick}>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle>{t(\"billing.ui.payment_method.title\")}</CardTitle>\n <Wallet className=\"h-4 w-4 text-muted-foreground\" />\n </CardHeader>\n <CardContent>\n {paymentMethods.length === 0 ? (\n <div className=\"space-y-2\">\n <p className=\"text-xl font-bold text-muted-foreground\">\n {t(\"billing.ui.cards.payment_method.empty_title\")}\n </p>\n <p className=\"text-xs text-muted-foreground\">{t(\"billing.ui.cards.payment_method.empty_description\")}</p>\n <Button\n variant=\"outline\"\n size=\"sm\"\n className=\"mt-2\"\n onClick={(e) => {\n e.stopPropagation();\n onManageClick();\n }}\n >\n {t(\"billing.ui.cards.payment_method.add_card\")}\n <ChevronRight className=\"h-4 w-4 ms-1 rtl:rotate-180\" />\n </Button>\n </div>\n ) : defaultMethod?.card ? (\n <div className=\"space-y-2\">\n <p className=\"text-xl font-bold\">\n {getCardBrandIcon(defaultMethod.card.brand)} ****{defaultMethod.card.last4}\n </p>\n <p className=\"text-sm text-muted-foreground\">\n {t(\"billing.ui.payment_method.expires\", {\n date: `${String(defaultMethod.card.expMonth).padStart(2, \"0\")}/${defaultMethod.card.expYear}`,\n })}\n </p>\n {paymentMethods.length > 1 && (\n <p className=\"text-xs text-muted-foreground\">\n {t(\"billing.ui.cards.payment_method.more_cards\", { count: paymentMethods.length - 1 })}\n </p>\n )}\n </div>\n ) : (\n <div className=\"space-y-2\">\n <p className=\"text-xl font-bold\">{defaultMethod?.type || t(\"billing.ui.payment_method.title\")}</p>\n {paymentMethods.length > 1 && (\n <p className=\"text-xs text-muted-foreground\">\n {t(\"billing.ui.cards.payment_method.more_methods\", { count: paymentMethods.length - 1 })}\n </p>\n )}\n </div>\n )}\n </CardContent>\n </Card>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { ExternalLink, User } from \"lucide-react\";\nimport { useState } from \"react\";\nimport { Button, Card, CardContent, CardHeader, CardTitle, Skeleton } from \"../../../../shadcnui\";\nimport { useI18nLocale } from \"../../../../i18n\";\nimport { StripeCustomerInterface, StripeCustomerService } from \"../../stripe-customer\";\n\ntype CustomerInfoCardProps = {\n customer: StripeCustomerInterface | null;\n loading?: boolean;\n error?: string;\n};\n\nfunction formatBalance(balance: number | undefined, currency: string | undefined, locale: string): string {\n const currencyCode = currency?.toUpperCase() || \"USD\";\n // Balance in Stripe is negative when customer has credit\n const displayBalance = balance ? -balance : 0;\n return new Intl.NumberFormat(locale, {\n style: \"currency\",\n currency: currencyCode,\n }).format(displayBalance / 100);\n}\n\nexport function CustomerInfoCard({ customer, loading, error }: CustomerInfoCardProps) {\n const t = useTranslations();\n const locale = useI18nLocale();\n const [portalLoading, setPortalLoading] = useState(false);\n\n const handlePortalClick = async (e: React.MouseEvent) => {\n e.stopPropagation();\n setPortalLoading(true);\n try {\n const { url } = await StripeCustomerService.createPortalSession();\n window.open(url, \"_blank\");\n } catch (err) {\n console.error(\"[CustomerInfoCard] Failed to create portal session:\", err);\n } finally {\n setPortalLoading(false);\n }\n };\n\n if (loading) {\n return (\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle>{t(\"billing.ui.cards.customer.title\")}</CardTitle>\n <User className=\"h-4 w-4 text-muted-foreground\" />\n </CardHeader>\n <CardContent>\n <Skeleton className=\"h-6 w-32 mb-2\" />\n <Skeleton className=\"h-4 w-48 mb-1\" />\n <Skeleton className=\"h-4 w-24\" />\n </CardContent>\n </Card>\n );\n }\n\n if (error) {\n return (\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle>{t(\"billing.ui.cards.customer.title\")}</CardTitle>\n <User className=\"h-4 w-4 text-muted-foreground\" />\n </CardHeader>\n <CardContent>\n <p className=\"text-destructive text-xs/relaxed\">{error}</p>\n </CardContent>\n </Card>\n );\n }\n\n if (!customer) {\n return (\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle>{t(\"billing.ui.cards.customer.title\")}</CardTitle>\n <User className=\"h-4 w-4 text-muted-foreground\" />\n </CardHeader>\n <CardContent>\n <p className=\"text-xl font-bold text-muted-foreground\">{t(\"billing.ui.cards.customer.not_set_up\")}</p>\n <p className=\"text-xs text-muted-foreground\">{t(\"billing.ui.cards.customer.not_set_up_description\")}</p>\n </CardContent>\n </Card>\n );\n }\n\n return (\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle>{t(\"billing.ui.cards.customer.title\")}</CardTitle>\n <User className=\"h-4 w-4 text-muted-foreground\" />\n </CardHeader>\n <CardContent>\n <div className=\"space-y-2\">\n {customer.name && <p className=\"text-xl font-bold\">{customer.name}</p>}\n {customer.email && <p className=\"text-sm text-muted-foreground\">{customer.email}</p>}\n {customer.balance !== undefined && customer.balance !== 0 && (\n <p className=\"text-sm\">\n <span className=\"text-muted-foreground\">{t(\"billing.ui.cards.customer.credit_balance\")} </span>\n <span className={customer.balance < 0 ? \"text-success\" : \"text-destructive\"}>\n {formatBalance(customer.balance, customer.currency, locale)}\n </span>\n </p>\n )}\n <Button variant=\"outline\" size=\"sm\" className=\"mt-2\" onClick={handlePortalClick} disabled={portalLoading}>\n {portalLoading ? t(\"common.loading\") : t(\"billing.ui.cards.customer.manage_portal\")}\n <ExternalLink className=\"h-4 w-4 ms-1\" />\n </Button>\n </div>\n </CardContent>\n </Card>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { ChevronRight, ReceiptIcon } from \"lucide-react\";\nimport { Badge, Card, CardContent, CardHeader, CardTitle, Skeleton } from \"../../../../shadcnui\";\nimport { InvoiceStatus, StripeInvoiceInterface } from \"../../stripe-invoice\";\nimport { formatDate, useBillingFormatOptions } from \"../utils\";\n\ntype InvoicesSummaryCardProps = {\n invoices: StripeInvoiceInterface[];\n loading?: boolean;\n error?: string;\n onViewAllClick: () => void;\n};\n\n/** Statuses with a `billing.ui.invoice_status.*` label; anything else Stripe adds is shown raw. */\nconst INVOICE_STATUSES: string[] = Object.values(InvoiceStatus);\n\nfunction getStatusBadgeVariant(status: InvoiceStatus): \"default\" | \"secondary\" | \"destructive\" | \"outline\" {\n switch (status) {\n case InvoiceStatus.PAID:\n return \"default\";\n case InvoiceStatus.OPEN:\n return \"secondary\";\n case InvoiceStatus.UNCOLLECTIBLE:\n case InvoiceStatus.VOID:\n return \"destructive\";\n default:\n return \"outline\";\n }\n}\n\nfunction formatAmount(amount: number, currency: string, locale: string): string {\n const currencyCode = currency?.toUpperCase() || \"USD\";\n return new Intl.NumberFormat(locale, {\n style: \"currency\",\n currency: currencyCode,\n }).format(amount / 100);\n}\n\nexport function InvoicesSummaryCard({ invoices, loading, error, onViewAllClick }: InvoicesSummaryCardProps) {\n const t = useTranslations();\n const fmt = useBillingFormatOptions();\n if (loading) {\n return (\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle>{t(\"billing.ui.cards.invoices.title\")}</CardTitle>\n <ReceiptIcon className=\"h-4 w-4 text-muted-foreground\" />\n </CardHeader>\n <CardContent>\n <Skeleton className=\"h-6 w-24 mb-2\" />\n <Skeleton className=\"h-4 w-32 mb-1\" />\n <Skeleton className=\"h-4 w-20\" />\n </CardContent>\n </Card>\n );\n }\n\n if (error) {\n return (\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle>{t(\"billing.ui.cards.invoices.title\")}</CardTitle>\n <ReceiptIcon className=\"h-4 w-4 text-muted-foreground\" />\n </CardHeader>\n <CardContent>\n <p className=\"text-destructive text-xs/relaxed\">{error}</p>\n </CardContent>\n </Card>\n );\n }\n\n // The most recent invoice that could charge something: a void one is an unpaid first-payment\n // attempt that was replaced, which never charged the GM (fall back to it only when nothing else exists)\n const latestInvoice = invoices.find((inv) => inv.status !== InvoiceStatus.VOID) ?? invoices[0];\n const paidInvoices = invoices.filter((inv) => inv.status === InvoiceStatus.PAID);\n const openInvoices = invoices.filter((inv) => inv.status === InvoiceStatus.OPEN);\n\n return (\n <Card className=\"cursor-pointer hover:bg-accent/50 transition-colors\" onClick={onViewAllClick}>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle>{t(\"billing.ui.cards.invoices.title\")}</CardTitle>\n <ReceiptIcon className=\"h-4 w-4 text-muted-foreground\" />\n </CardHeader>\n <CardContent>\n {invoices.length === 0 ? (\n <div className=\"space-y-2\">\n <p className=\"text-xl font-bold text-muted-foreground\">{t(\"billing.ui.invoices.empty_title\")}</p>\n <p className=\"text-xs text-muted-foreground\">{t(\"billing.ui.cards.invoices.empty_description\")}</p>\n </div>\n ) : latestInvoice ? (\n <div className=\"space-y-2\">\n <div className=\"flex items-center gap-2\">\n <p className=\"text-xl font-bold\">\n {formatAmount(latestInvoice.total, latestInvoice.currency, fmt.locale)}\n </p>\n <Badge variant={getStatusBadgeVariant(latestInvoice.status)}>\n {INVOICE_STATUSES.includes(latestInvoice.status)\n ? t(`billing.ui.invoice_status.${latestInvoice.status}`)\n : latestInvoice.status}\n </Badge>\n </div>\n <p className=\"text-sm text-muted-foreground\">\n {latestInvoice.stripeInvoiceNumber ||\n t(\"billing.ui.cards.invoices.invoice_from\", { date: formatDate(latestInvoice.periodStart, fmt) })}\n </p>\n <div className=\"flex items-center gap-4 text-xs text-muted-foreground\">\n {paidInvoices.length > 0 && (\n <span>{t(\"billing.ui.cards.invoices.paid_count\", { count: paidInvoices.length })}</span>\n )}\n {openInvoices.length > 0 && (\n <span className=\"text-warning\">\n {t(\"billing.ui.cards.invoices.open_count\", { count: openInvoices.length })}\n </span>\n )}\n <span className=\"flex items-center\">\n {t(\"billing.ui.cards.view_all\")}\n <ChevronRight className=\"h-3 w-3 ms-1 rtl:rotate-180\" />\n </span>\n </div>\n </div>\n ) : null}\n </CardContent>\n </Card>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { Activity, ChevronRight } from \"lucide-react\";\nimport { Card, CardContent, CardHeader, CardTitle, Skeleton } from \"../../../../shadcnui\";\nimport { useI18nLocale } from \"../../../../i18n\";\nimport { MeterInterface, MeterSummaryInterface } from \"../../stripe-usage\";\n\ntype BillingUsageSummaryCardProps = {\n meters: MeterInterface[];\n summaries: Record<string, MeterSummaryInterface | null>;\n loading?: boolean;\n error?: string;\n onViewDetailsClick: () => void;\n};\n\nfunction formatNumber(value: number, locale: string): string {\n return new Intl.NumberFormat(locale, {\n notation: \"compact\",\n compactDisplay: \"short\",\n }).format(value);\n}\n\nexport function BillingUsageSummaryCard({\n meters,\n summaries,\n loading,\n error,\n onViewDetailsClick,\n}: BillingUsageSummaryCardProps) {\n const t = useTranslations();\n const locale = useI18nLocale();\n if (loading) {\n return (\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle>{t(\"billing.ui.cards.usage.title\")}</CardTitle>\n <Activity className=\"h-4 w-4 text-muted-foreground\" />\n </CardHeader>\n <CardContent>\n <Skeleton className=\"h-6 w-24 mb-2\" />\n <Skeleton className=\"h-4 w-32\" />\n </CardContent>\n </Card>\n );\n }\n\n if (error) {\n return (\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle>{t(\"billing.ui.cards.usage.title\")}</CardTitle>\n <Activity className=\"h-4 w-4 text-muted-foreground\" />\n </CardHeader>\n <CardContent>\n <p className=\"text-destructive text-xs/relaxed\">{error}</p>\n </CardContent>\n </Card>\n );\n }\n\n // Calculate total usage across all meters\n const totalUsage = Object.values(summaries).reduce((acc, summary) => {\n return acc + (summary?.aggregatedValue || 0);\n }, 0);\n\n // Get first meter with data for display\n const primaryMeter = meters.find((m) => summaries[m.id]?.aggregatedValue);\n const primarySummary = primaryMeter ? summaries[primaryMeter.id] : null;\n\n return (\n <Card className=\"cursor-pointer hover:bg-accent/50 transition-colors\" onClick={onViewDetailsClick}>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle>{t(\"billing.ui.cards.usage.title\")}</CardTitle>\n <Activity className=\"h-4 w-4 text-muted-foreground\" />\n </CardHeader>\n <CardContent>\n {meters.length === 0 ? (\n <div className=\"space-y-2\">\n <p className=\"text-xl font-bold text-muted-foreground\">{t(\"billing.ui.cards.usage.no_meters\")}</p>\n <p className=\"text-xs text-muted-foreground\">{t(\"billing.ui.cards.usage.no_meters_description\")}</p>\n </div>\n ) : (\n <div className=\"space-y-2\">\n <p className=\"text-xl font-bold\">\n {t(\"billing.ui.cards.usage.units\", { value: formatNumber(totalUsage, locale) })}\n </p>\n {primaryMeter && primarySummary && (\n <p className=\"text-sm text-muted-foreground\">\n {primaryMeter.displayName}: {formatNumber(primarySummary.aggregatedValue, locale)}\n </p>\n )}\n {meters.length > 1 && (\n <p className=\"text-xs text-muted-foreground\">\n {t(\"billing.ui.cards.usage.across_meters\", { count: meters.length })}\n </p>\n )}\n <span className=\"flex items-center text-xs text-muted-foreground\">\n {t(\"billing.ui.cards.view_details\")}\n <ChevronRight className=\"h-3 w-3 ms-1 rtl:rotate-180\" />\n </span>\n </div>\n )}\n </CardContent>\n </Card>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { CreditCard, Loader2, Wallet } from \"lucide-react\";\nimport { useSearchParams } from \"next/navigation\";\nimport { useCallback, useEffect, useMemo, useState } from \"react\";\nimport { Button, Card, CardContent, CardDescription, CardHeader, CardTitle } from \"../../../../shadcnui\";\nimport { PaymentMethodInterface, StripeCustomerInterface, StripeCustomerService } from \"../../stripe-customer\";\nimport { PaymentMethodsContainer } from \"../../stripe-customer/components\";\nimport { StripeInvoiceInterface, StripeInvoiceService } from \"../../stripe-invoice\";\nimport { InvoicesContainer } from \"../../stripe-invoice/components\";\nimport { StripeSubscriptionInterface, StripeSubscriptionService, SubscriptionStatus } from \"../../stripe-subscription\";\nimport { SubscriptionsContainer } from \"../../stripe-subscription/components\";\nimport { SubscriptionWizard } from \"../../stripe-subscription/components/wizards\";\nimport { MeterInterface, MeterSummaryInterface, StripeUsageService } from \"../../stripe-usage\";\nimport { UsageContainer } from \"../../stripe-usage/components\";\nimport {\n BillingUsageSummaryCard,\n CustomerInfoCard,\n InvoicesSummaryCard,\n PaymentMethodSummaryCard,\n SubscriptionSummaryCard,\n} from \"../cards\";\nimport { BillingDetailModal } from \"../modals/BillingDetailModal\";\nimport { urlWithoutParam } from \"../utils/url\";\nimport { BillingAlertBanner } from \"../widgets/BillingAlertBanner\";\n\ntype ModalType = \"subscriptions\" | \"payment-methods\" | \"invoices\" | \"usage\" | null;\n\ntype DataState = {\n customer: StripeCustomerInterface | null;\n subscriptions: StripeSubscriptionInterface[];\n paymentMethods: PaymentMethodInterface[];\n invoices: StripeInvoiceInterface[];\n meters: MeterInterface[];\n meterSummaries: Record<string, MeterSummaryInterface | null>;\n};\n\ntype LoadingState = {\n customer: boolean;\n subscriptions: boolean;\n paymentMethods: boolean;\n invoices: boolean;\n usage: boolean;\n};\n\ntype ErrorState = {\n customer: string | null;\n subscriptions: string | null;\n paymentMethods: string | null;\n invoices: string | null;\n usage: string | null;\n};\n\nexport function BillingDashboardContainer() {\n const t = useTranslations();\n const [data, setData] = useState<DataState>({\n customer: null,\n subscriptions: [],\n paymentMethods: [],\n invoices: [],\n meters: [],\n meterSummaries: {},\n });\n\n const [loading, setLoading] = useState<LoadingState>({\n customer: true,\n subscriptions: true,\n paymentMethods: true,\n invoices: true,\n usage: true,\n });\n\n // Error state holds message keys; they are translated at render time.\n const [errors, setErrors] = useState<ErrorState>({\n customer: null,\n subscriptions: null,\n paymentMethods: null,\n invoices: null,\n usage: null,\n });\n\n const [activeModal, setActiveModal] = useState<ModalType>(null);\n const [noCustomerExists, setNoCustomerExists] = useState(false);\n const [creatingCustomer, setCreatingCustomer] = useState(false);\n const searchParams = useSearchParams();\n\n // Wizard state - lifted from SubscriptionsContainer to avoid nested dialogs\n const [showWizard, setShowWizard] = useState(false);\n const [editingSubscription, setEditingSubscription] = useState<StripeSubscriptionInterface | null>(null);\n\n // Check if company has metered subscriptions\n const hasMeteredSubscriptions = useCallback((): boolean => {\n return data.subscriptions.some((sub) => sub.price?.recurring?.usageType === \"metered\");\n }, [data.subscriptions]);\n\n // Check if user has active recurring subscription (for wizard filtering)\n const hasActiveRecurringSubscription = useMemo(() => {\n return data.subscriptions.some(\n (sub) =>\n (sub.status === SubscriptionStatus.ACTIVE || sub.status === SubscriptionStatus.TRIALING) &&\n sub.price?.priceType === \"recurring\",\n );\n }, [data.subscriptions]);\n\n // Fetch all data - first check customer, then fetch rest if customer exists\n const fetchAllData = useCallback(async () => {\n setNoCustomerExists(false);\n\n // First, try to fetch customer\n let customer: StripeCustomerInterface | null = null;\n try {\n customer = await StripeCustomerService.getCustomer();\n setData((prev) => ({ ...prev, customer }));\n setErrors((prev) => ({ ...prev, customer: null }));\n setNoCustomerExists(false);\n } catch (error: unknown) {\n console.error(\"[BillingDashboard] Failed to load customer:\", error);\n // A 404 means the company has no billing customer yet - a setup prompt, not an error.\n // Match on the status, never on the message: `statusText` is empty over HTTP/2, so the\n // error text carries no reason phrase to match against.\n if ((error as { status?: number })?.status === 404) {\n setNoCustomerExists(true);\n } else {\n setErrors((prev) => ({ ...prev, customer: \"billing.ui.dashboard.errors.load_customer\" }));\n }\n } finally {\n setLoading((prev) => ({ ...prev, customer: false }));\n }\n\n // Without a customer there is nothing downstream to fetch. Release every section here, or the\n // cards that are only cleared inside their own fetchers stay skeletons forever.\n if (!customer) {\n setLoading({\n customer: false,\n subscriptions: false,\n paymentMethods: false,\n invoices: false,\n usage: false,\n });\n return;\n }\n\n // Fetch subscriptions\n const fetchSubscriptions = async () => {\n try {\n const subscriptions = await StripeSubscriptionService.listSubscriptions();\n setData((prev) => ({ ...prev, subscriptions }));\n setErrors((prev) => ({ ...prev, subscriptions: null }));\n return subscriptions;\n } catch (error) {\n console.error(\"[BillingDashboard] Failed to load subscriptions:\", error);\n setErrors((prev) => ({ ...prev, subscriptions: \"billing.ui.dashboard.errors.load_subscriptions\" }));\n return [];\n } finally {\n setLoading((prev) => ({ ...prev, subscriptions: false }));\n }\n };\n\n // Fetch payment methods\n const fetchPaymentMethods = async () => {\n try {\n const paymentMethods = await StripeCustomerService.listPaymentMethods();\n setData((prev) => ({ ...prev, paymentMethods }));\n setErrors((prev) => ({ ...prev, paymentMethods: null }));\n } catch (error) {\n console.error(\"[BillingDashboard] Failed to load payment methods:\", error);\n setErrors((prev) => ({ ...prev, paymentMethods: \"billing.ui.dashboard.errors.load_payment_methods\" }));\n } finally {\n setLoading((prev) => ({ ...prev, paymentMethods: false }));\n }\n };\n\n // Fetch invoices\n const fetchInvoices = async () => {\n try {\n const invoices = await StripeInvoiceService.listInvoices();\n setData((prev) => ({ ...prev, invoices }));\n setErrors((prev) => ({ ...prev, invoices: null }));\n } catch (error) {\n console.error(\"[BillingDashboard] Failed to load invoices:\", error);\n setErrors((prev) => ({ ...prev, invoices: \"billing.ui.dashboard.errors.load_invoices\" }));\n } finally {\n setLoading((prev) => ({ ...prev, invoices: false }));\n }\n };\n\n // Execute all in parallel\n const [subscriptions] = await Promise.all([fetchSubscriptions(), fetchPaymentMethods(), fetchInvoices()]);\n\n // Check if there are metered subscriptions and fetch usage data\n const hasMetered = subscriptions.some(\n (sub: StripeSubscriptionInterface) => sub.price?.recurring?.usageType === \"metered\",\n );\n\n if (hasMetered) {\n await fetchUsageData();\n } else {\n setLoading((prev) => ({ ...prev, usage: false }));\n }\n }, []);\n\n // Create a new Stripe customer for the company\n const handleCreateCustomer = async () => {\n setCreatingCustomer(true);\n try {\n await StripeCustomerService.createCustomer();\n setNoCustomerExists(false);\n // Refresh all data after customer creation\n await fetchAllData();\n } catch (error) {\n console.error(\"[BillingDashboard] Failed to create customer:\", error);\n setErrors((prev) => ({ ...prev, customer: \"billing.ui.dashboard.errors.setup\" }));\n } finally {\n setCreatingCustomer(false);\n }\n };\n\n // Fetch usage data (called conditionally)\n const fetchUsageData = async () => {\n try {\n const meters = await StripeUsageService.listMeters();\n setData((prev) => ({ ...prev, meters }));\n\n // Load summaries for each meter (current month)\n const summariesMap: Record<string, MeterSummaryInterface | null> = {};\n const now = new Date();\n const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);\n const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59, 999);\n\n for (const meter of meters) {\n try {\n const meterSummaries = await StripeUsageService.getMeterSummaries({\n meterId: meter.id,\n startTime: startOfMonth,\n endTime: endOfMonth,\n });\n summariesMap[meter.id] = meterSummaries.length > 0 ? meterSummaries[0] : null;\n } catch (error) {\n console.error(`[BillingDashboard] Failed to load summaries for meter ${meter.id}:`, error);\n summariesMap[meter.id] = null;\n }\n }\n\n setData((prev) => ({ ...prev, meterSummaries: summariesMap }));\n setErrors((prev) => ({ ...prev, usage: null }));\n } catch (error) {\n console.error(\"[BillingDashboard] Failed to load usage data:\", error);\n setErrors((prev) => ({ ...prev, usage: \"billing.ui.dashboard.errors.load_usage\" }));\n } finally {\n setLoading((prev) => ({ ...prev, usage: false }));\n }\n };\n\n // Refresh data after modal actions\n const refreshData = useCallback(async () => {\n // Reset loading states for a refresh\n setLoading({\n customer: true,\n subscriptions: true,\n paymentMethods: true,\n invoices: true,\n usage: true,\n });\n await fetchAllData();\n }, [fetchAllData]);\n\n // Handler to open wizard (can be called from SubscriptionsContainer)\n const handleOpenWizard = useCallback((subscription?: StripeSubscriptionInterface) => {\n setEditingSubscription(subscription || null);\n setShowWizard(true);\n }, []);\n\n // Handler to close wizard\n const handleWizardClose = useCallback(() => {\n setShowWizard(false);\n setEditingSubscription(null);\n refreshData();\n }, [refreshData]);\n\n // Initial data load\n useEffect(() => {\n fetchAllData();\n }, [fetchAllData]);\n\n // Handle URL action parameter for deep linking. `?action=subscribe` records the\n // intent to subscribe; the wizard itself only renders once a customer exists, so a\n // company without one sees the setup card first and the wizard opens right after\n // \"Set Up Billing Account\" succeeds.\n useEffect(() => {\n const action = searchParams.get(\"action\");\n if (action === \"subscribe\") {\n setShowWizard(true);\n // Drop only `action`, so a refresh does not re-trigger it. Every other parameter\n // must survive: a host page (the settings hub) selects this dashboard through\n // `?section=`, and losing it switches the page away and unmounts this component.\n window.history.replaceState(\n window.history.state,\n \"\",\n urlWithoutParam({\n pathname: window.location.pathname,\n search: window.location.search,\n hash: window.location.hash,\n param: \"action\",\n }),\n );\n }\n }, [searchParams]);\n\n // Detect critical subscriptions for alert banners\n const criticalSubscriptions = data.subscriptions.filter(\n (sub) =>\n sub.status === SubscriptionStatus.PAST_DUE ||\n (sub.status === SubscriptionStatus.TRIALING &&\n sub.trialEnd &&\n new Date(sub.trialEnd).getTime() - new Date().getTime() <= 7 * 24 * 60 * 60 * 1000),\n );\n\n // Handle modal close with refresh\n const handleModalClose = (open: boolean) => {\n if (!open) {\n setActiveModal(null);\n refreshData();\n }\n };\n\n // Get modal title based on type\n const getModalTitle = (type: ModalType): string => {\n switch (type) {\n case \"subscriptions\":\n return t(\"billing.ui.dashboard.modal.subscriptions\");\n case \"payment-methods\":\n return t(\"billing.ui.payment_methods.title\");\n case \"invoices\":\n return t(\"billing.ui.dashboard.modal.invoices\");\n case \"usage\":\n return t(\"billing.ui.usage.title\");\n default:\n return \"\";\n }\n };\n\n // Show loading state while checking for customer\n const isInitialLoading = loading.customer && !noCustomerExists && !data.customer;\n\n return (\n <div className=\"flex w-full flex-col gap-y-6\">\n {/* Header */}\n <div className=\"flex items-center gap-x-3\">\n <Wallet className=\"h-8 w-8\" />\n <h1 className=\"text-primary text-3xl font-semibold\">{t(\"billing.title\")}</h1>\n </div>\n\n {/* Initial Loading State */}\n {isInitialLoading && (\n <Card>\n <CardContent className=\"flex items-center justify-center py-12\">\n <Loader2 className=\"h-8 w-8 animate-spin text-muted-foreground\" />\n </CardContent>\n </Card>\n )}\n\n {/* No Customer State - Show Setup Prompt */}\n {noCustomerExists && !isInitialLoading && (\n <Card>\n <CardHeader className=\"text-center\">\n <div className=\"mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-primary/10\">\n <CreditCard className=\"h-8 w-8 text-primary\" />\n </div>\n <CardTitle>{t(\"billing.ui.dashboard.setup_title\")}</CardTitle>\n <CardDescription>{t(\"billing.ui.dashboard.setup_description\")}</CardDescription>\n </CardHeader>\n <CardContent className=\"flex justify-center pb-8\">\n <Button onClick={handleCreateCustomer} disabled={creatingCustomer} size=\"lg\">\n {creatingCustomer ? (\n <>\n <Loader2 className=\"me-2 h-4 w-4 animate-spin\" />\n {t(\"billing.ui.dashboard.setting_up\")}\n </>\n ) : (\n t(\"billing.ui.dashboard.setup_button\")\n )}\n </Button>\n </CardContent>\n {errors.customer && (\n <CardContent className=\"pt-0\">\n <p className=\"text-center text-destructive text-xs/relaxed\">{t(errors.customer)}</p>\n </CardContent>\n )}\n </Card>\n )}\n\n {/* Main Dashboard Content - Only shown when customer exists */}\n {!noCustomerExists && !isInitialLoading && (\n <>\n {/* Alert Banners */}\n {criticalSubscriptions.map((subscription) => (\n <BillingAlertBanner\n key={subscription.id}\n subscription={subscription}\n onUpdatePayment={() => setActiveModal(\"payment-methods\")}\n onAddPayment={() => setActiveModal(\"payment-methods\")}\n />\n ))}\n\n {/* Summary Cards Grid */}\n <div className=\"grid gap-4 md:grid-cols-2\">\n <SubscriptionSummaryCard\n subscriptions={data.subscriptions}\n loading={loading.subscriptions}\n error={errors.subscriptions ? t(errors.subscriptions) : undefined}\n onManageClick={() => {\n if (data.subscriptions.length === 0) {\n // No subscriptions - open wizard directly\n setShowWizard(true);\n } else {\n // Has subscriptions - open manage modal\n setActiveModal(\"subscriptions\");\n }\n }}\n />\n\n <PaymentMethodSummaryCard\n paymentMethods={data.paymentMethods}\n defaultPaymentMethodId={data.customer?.defaultPaymentMethodId}\n loading={loading.paymentMethods}\n error={errors.paymentMethods ? t(errors.paymentMethods) : undefined}\n onManageClick={() => setActiveModal(\"payment-methods\")}\n />\n\n <CustomerInfoCard\n customer={data.customer}\n loading={loading.customer}\n error={errors.customer ? t(errors.customer) : undefined}\n />\n\n <InvoicesSummaryCard\n invoices={data.invoices}\n loading={loading.invoices}\n error={errors.invoices ? t(errors.invoices) : undefined}\n onViewAllClick={() => setActiveModal(\"invoices\")}\n />\n\n {/* Usage Card - only shown when metered subscriptions exist */}\n {hasMeteredSubscriptions() && (\n <BillingUsageSummaryCard\n meters={data.meters}\n summaries={data.meterSummaries}\n loading={loading.usage}\n error={errors.usage ? t(errors.usage) : undefined}\n onViewDetailsClick={() => setActiveModal(\"usage\")}\n />\n )}\n </div>\n\n {/* Detail Modals */}\n <BillingDetailModal\n open={activeModal === \"subscriptions\"}\n onOpenChange={handleModalClose}\n title={getModalTitle(\"subscriptions\")}\n >\n {/* The dashboard's list, so a wizard success (refreshData) refreshes the dialog too */}\n <SubscriptionsContainer\n onOpenWizard={handleOpenWizard}\n hasActiveRecurringSubscription={hasActiveRecurringSubscription}\n subscriptions={data.subscriptions}\n onSubscriptionsChange={refreshData}\n />\n </BillingDetailModal>\n\n <BillingDetailModal\n open={activeModal === \"payment-methods\"}\n onOpenChange={handleModalClose}\n title={getModalTitle(\"payment-methods\")}\n >\n <PaymentMethodsContainer />\n </BillingDetailModal>\n\n <BillingDetailModal\n open={activeModal === \"invoices\"}\n onOpenChange={handleModalClose}\n title={getModalTitle(\"invoices\")}\n >\n <InvoicesContainer />\n </BillingDetailModal>\n\n <BillingDetailModal\n open={activeModal === \"usage\"}\n onOpenChange={handleModalClose}\n title={getModalTitle(\"usage\")}\n >\n <UsageContainer />\n </BillingDetailModal>\n\n {/* Subscription Wizard - rendered at dashboard level to avoid nested dialogs */}\n <SubscriptionWizard\n open={showWizard && !!data.customer}\n onOpenChange={(open) => !open && handleWizardClose()}\n onSuccess={refreshData}\n hasActiveRecurringSubscription={hasActiveRecurringSubscription}\n subscription={editingSubscription ?? undefined}\n />\n </>\n )}\n </div>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { CreditCard } from \"lucide-react\";\nimport { useEffect, useState } from \"react\";\nimport { Button } from \"../../../../../shadcnui\";\nimport { PaymentMethodInterface, StripeCustomerService } from \"../../data\";\nimport { PaymentMethodEditor } from \"../forms/PaymentMethodEditor\";\nimport { PaymentMethodsList } from \"../lists/PaymentMethodsList\";\n\nexport function PaymentMethodsContainer() {\n const t = useTranslations();\n const [paymentMethods, setPaymentMethods] = useState<PaymentMethodInterface[]>([]);\n const [loading, setLoading] = useState<boolean>(true);\n const [showAddPaymentMethod, setShowAddPaymentMethod] = useState<boolean>(false);\n\n const loadPaymentMethods = async () => {\n setLoading(true);\n try {\n const fetchedPaymentMethods = await StripeCustomerService.listPaymentMethods();\n setPaymentMethods(fetchedPaymentMethods);\n } catch (error) {\n console.error(\"[PaymentMethodsContainer] Failed to load payment methods:\", error);\n } finally {\n setLoading(false);\n }\n };\n\n useEffect(() => {\n loadPaymentMethods();\n }, []);\n\n if (loading) {\n return (\n <div className=\"flex h-64 items-center justify-center\">\n <p className=\"text-muted-foreground\">{t(\"billing.ui.payment_methods.loading\")}</p>\n </div>\n );\n }\n\n return (\n <div className=\"flex w-full flex-col gap-y-6\">\n {/* Header */}\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-x-3\">\n <CreditCard className=\"h-8 w-8\" />\n <h1 className=\"text-primary text-3xl font-semibold\">{t(\"billing.ui.payment_methods.title\")}</h1>\n </div>\n <Button onClick={() => setShowAddPaymentMethod(true)}>{t(\"billing.ui.payment_methods.add\")}</Button>\n </div>\n\n {/* Empty State */}\n {paymentMethods.length === 0 && (\n <div className=\"flex flex-col items-center justify-center gap-y-4 rounded-lg border-2 border-dashed border-gray-300 bg-muted/50 p-12\">\n <CreditCard className=\"h-16 w-16 text-muted-foreground\" />\n <div className=\"text-center\">\n <h3 className=\"mb-2 text-sm font-medium\">{t(\"billing.ui.payment_methods.empty_title\")}</h3>\n <p className=\"mb-4 text-muted-foreground\">{t(\"billing.ui.payment_methods.empty_description\")}</p>\n <Button onClick={() => setShowAddPaymentMethod(true)}>{t(\"billing.ui.payment_methods.add_first\")}</Button>\n </div>\n </div>\n )}\n\n {/* Payment Methods List */}\n {paymentMethods.length > 0 && (\n <PaymentMethodsList paymentMethods={paymentMethods} onUpdate={loadPaymentMethods} />\n )}\n\n {/* Add Payment Method Modal */}\n {showAddPaymentMethod && (\n <PaymentMethodEditor\n open={showAddPaymentMethod}\n onOpenChange={setShowAddPaymentMethod}\n onSuccess={loadPaymentMethods}\n />\n )}\n </div>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from \"../../../../../shadcnui\";\nimport { PaymentMethodForm } from \"./PaymentMethodForm\";\n\ntype PaymentMethodEditorProps = {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n onSuccess: () => void;\n};\n\nexport function PaymentMethodEditor({ open, onOpenChange, onSuccess }: PaymentMethodEditorProps) {\n const t = useTranslations();\n const handleSuccess = () => {\n onSuccess();\n onOpenChange(false);\n };\n\n const handleCancel = () => {\n onOpenChange(false);\n };\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent className=\"max-w-md\">\n <DialogHeader>\n <DialogTitle>{t(\"billing.ui.payment_methods.add\")}</DialogTitle>\n <DialogDescription>{t(\"billing.ui.payment_methods.add_description\")}</DialogDescription>\n </DialogHeader>\n {open && <PaymentMethodForm onSuccess={handleSuccess} onCancel={handleCancel} />}\n </DialogContent>\n </Dialog>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { CardElement, useElements, useStripe } from \"@stripe/react-stripe-js\";\nimport { useEffect, useState } from \"react\";\nimport { Alert, AlertDescription, Button, Checkbox, Label } from \"../../../../../shadcnui\";\nimport { StripeCustomerService } from \"../../data\";\n\ntype PaymentMethodFormProps = {\n onSuccess: () => void;\n onCancel: () => void;\n isLoading?: boolean;\n /**\n * Always make the new card the customer's default and hide the checkbox. The\n * subscription wizard needs this: the next attempt must charge the card just added.\n */\n alwaysSetDefault?: boolean;\n};\n\nexport function PaymentMethodForm({\n onSuccess,\n onCancel,\n isLoading = false,\n alwaysSetDefault = false,\n}: PaymentMethodFormProps) {\n const t = useTranslations();\n const stripe = useStripe();\n const elements = useElements();\n\n const [setupIntent, setSetupIntent] = useState<{ clientSecret: string } | null>(null);\n const [loading, setLoading] = useState<boolean>(true);\n const [isSubmitting, setIsSubmitting] = useState<boolean>(false);\n const [error, setError] = useState<string | null>(null);\n const [setAsDefault, setSetAsDefault] = useState<boolean>(true);\n\n // Fetch setup intent on component mount\n useEffect(() => {\n const fetchSetupIntent = async () => {\n setLoading(true);\n try {\n const intent = await StripeCustomerService.createSetupIntent();\n setSetupIntent(intent);\n } catch (err) {\n console.error(\"[PaymentMethodForm] Failed to create setup intent:\", err);\n setError(t(\"billing.ui.payment_form.errors.init\"));\n } finally {\n setLoading(false);\n }\n };\n\n fetchSetupIntent();\n }, []);\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n\n if (!stripe || !elements || !setupIntent) {\n return;\n }\n\n setIsSubmitting(true);\n setError(null);\n\n try {\n const cardElement = elements.getElement(CardElement);\n if (!cardElement) {\n throw new Error(\"Card element not found\");\n }\n\n // Confirm card setup with Stripe\n const { error: stripeError, setupIntent: confirmedSetupIntent } = await stripe.confirmCardSetup(\n setupIntent.clientSecret,\n {\n payment_method: {\n card: cardElement,\n },\n },\n );\n\n if (stripeError) {\n console.error(\"[PaymentMethodForm] Stripe error:\", stripeError);\n setError(stripeError.message || t(\"billing.ui.payment_form.errors.add\"));\n setIsSubmitting(false);\n return;\n }\n\n // Set as default if checkbox is checked\n if ((alwaysSetDefault || setAsDefault) && confirmedSetupIntent?.payment_method) {\n await StripeCustomerService.setDefaultPaymentMethod({\n paymentMethodId:\n typeof confirmedSetupIntent.payment_method === \"string\"\n ? confirmedSetupIntent.payment_method\n : confirmedSetupIntent.payment_method.id,\n });\n }\n\n onSuccess();\n } catch (err: any) {\n console.error(\"[PaymentMethodForm] Error:\", err);\n setError(err.message || t(\"billing.ui.payment_form.errors.unexpected\"));\n } finally {\n setIsSubmitting(false);\n }\n };\n\n if (loading) {\n return (\n <div className=\"flex items-center justify-center py-8\">\n <p className=\"text-muted-foreground\">{t(\"billing.ui.payment_form.loading\")}</p>\n </div>\n );\n }\n\n if (!setupIntent && error) {\n return (\n <Alert variant=\"destructive\" className=\"bg-red-50 border-red-200\">\n <AlertDescription>{error}</AlertDescription>\n </Alert>\n );\n }\n\n return (\n <form onSubmit={handleSubmit} className=\"flex flex-col gap-y-4\">\n {/* Card Element */}\n <div className=\"rounded-md border border-gray-300 p-3\">\n <CardElement\n options={{\n style: {\n base: {\n fontSize: \"16px\",\n color: \"#424770\",\n \"::placeholder\": {\n color: \"#aab7c4\",\n },\n },\n invalid: {\n color: \"#9e2146\",\n },\n },\n }}\n />\n </div>\n\n {/* Set as Default Checkbox */}\n {!alwaysSetDefault && (\n <div className=\"flex items-center gap-x-2\">\n <Checkbox\n id=\"setAsDefault\"\n checked={setAsDefault}\n onCheckedChange={(checked) => setSetAsDefault(!!checked)}\n />\n <Label htmlFor=\"setAsDefault\" className=\"cursor-pointer font-normal\">\n {t(\"billing.ui.payment_form.set_default\")}\n </Label>\n </div>\n )}\n\n {/* Error Alert */}\n {error && (\n <Alert variant=\"destructive\" className=\"bg-red-50 border-red-200\">\n <AlertDescription>{error}</AlertDescription>\n </Alert>\n )}\n\n {/* Action Buttons */}\n <div className=\"flex justify-end gap-x-2\">\n <Button type=\"button\" variant=\"outline\" onClick={onCancel} disabled={isSubmitting || isLoading}>\n {t(\"ui.buttons.cancel\")}\n </Button>\n <Button type=\"submit\" disabled={!stripe || isSubmitting || isLoading}>\n {isSubmitting ? t(\"billing.ui.wizard.review.processing\") : t(\"billing.ui.cards.payment_method.add_card\")}\n </Button>\n </div>\n </form>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { MoreVertical } from \"lucide-react\";\nimport { useEffect, useState } from \"react\";\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n Badge,\n Button,\n Card,\n CardContent,\n CardHeader,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from \"../../../../../shadcnui\";\nimport { PaymentMethodInterface, StripeCustomerInterface, StripeCustomerService } from \"../../data\";\n\ntype PaymentMethodCardProps = {\n paymentMethod: PaymentMethodInterface;\n onUpdate: () => void;\n};\n\n// Card brand icons mapping\nconst brandIcons: Record<string, string> = {\n visa: \"💳\",\n mastercard: \"💳\",\n amex: \"💳\",\n discover: \"💳\",\n};\n\nexport function PaymentMethodCard({ paymentMethod, onUpdate }: PaymentMethodCardProps) {\n const t = useTranslations();\n const [loading, setLoading] = useState<boolean>(false);\n const [customer, setCustomer] = useState<StripeCustomerInterface | null>(null);\n const [showRemoveDialog, setShowRemoveDialog] = useState<boolean>(false);\n\n // Load customer to check default payment method\n useEffect(() => {\n const loadCustomer = async () => {\n try {\n const fetchedCustomer = await StripeCustomerService.getCustomer();\n setCustomer(fetchedCustomer);\n } catch (error) {\n console.error(\"[PaymentMethodCard] Failed to load customer:\", error);\n }\n };\n\n loadCustomer();\n }, []);\n\n const isDefault = customer?.defaultPaymentMethodId === paymentMethod.id;\n const brand = paymentMethod.card?.brand || \"card\";\n const last4 = paymentMethod.card?.last4 || \"****\";\n const expMonth = paymentMethod.card?.expMonth || 0;\n const expYear = paymentMethod.card?.expYear || 0;\n const brandIcon = brandIcons[brand.toLowerCase()] || \"💳\";\n\n const handleSetDefault = async () => {\n setLoading(true);\n try {\n await StripeCustomerService.setDefaultPaymentMethod({ paymentMethodId: paymentMethod.id });\n onUpdate();\n } catch (error) {\n console.error(\"[PaymentMethodCard] Failed to set as default:\", error);\n } finally {\n setLoading(false);\n }\n };\n\n const handleRemove = async () => {\n setLoading(true);\n try {\n await StripeCustomerService.removePaymentMethod({ paymentMethodId: paymentMethod.id });\n setShowRemoveDialog(false);\n onUpdate();\n } catch (error) {\n console.error(\"[PaymentMethodCard] Failed to remove:\", error);\n setLoading(false);\n }\n };\n\n return (\n <>\n <Card className=\"relative\">\n {/* Default Badge */}\n {isDefault && (\n <Badge variant=\"softGreen\" className=\"absolute end-2 top-2\">\n {t(\"billing.ui.payment_methods.default\")}\n </Badge>\n )}\n\n <CardHeader className=\"flex flex-row items-center justify-between pb-2\">\n <div className=\"flex items-center gap-x-2\">\n <span className=\"text-2xl\">{brandIcon}</span>\n <span className=\"text-sm font-medium capitalize\">{brand}</span>\n </div>\n <DropdownMenu>\n <DropdownMenuTrigger>\n <Button\n render={<div />}\n nativeButton={false}\n variant=\"ghost\"\n size=\"sm\"\n disabled={loading}\n className=\"h-8 w-8 p-0\"\n >\n <MoreVertical className=\"h-4 w-4\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n {!isDefault && (\n <DropdownMenuItem onClick={handleSetDefault} disabled={loading}>\n {t(\"billing.ui.payment_methods.set_default\")}\n </DropdownMenuItem>\n )}\n <DropdownMenuItem\n onClick={() => setShowRemoveDialog(true)}\n disabled={loading}\n className=\"text-destructive\"\n >\n {t(\"ui.buttons.remove\")}\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n </CardHeader>\n\n <CardContent>\n <div className=\"flex flex-col gap-y-1\">\n <p className=\"text-lg font-semibold\">•••• {last4}</p>\n <p className=\"text-sm text-muted-foreground\">\n {t(\"billing.ui.payment_method.expires\", { date: `${String(expMonth).padStart(2, \"0\")}/${expYear}` })}\n </p>\n </div>\n </CardContent>\n </Card>\n\n {/* Remove Confirmation Dialog */}\n <AlertDialog open={showRemoveDialog} onOpenChange={setShowRemoveDialog}>\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>{t(\"billing.ui.payment_methods.remove_title\")}</AlertDialogTitle>\n <AlertDialogDescription>\n {t(\"billing.ui.payment_methods.remove_description\")}\n {isDefault && ` ${t(\"billing.ui.payment_methods.remove_default_warning\")}`}\n </AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel disabled={loading}>{t(\"ui.buttons.cancel\")}</AlertDialogCancel>\n <AlertDialogAction onClick={handleRemove} disabled={loading} className=\"bg-red-600 hover:bg-red-700\">\n {loading ? t(\"billing.ui.payment_methods.removing\") : t(\"ui.buttons.remove\")}\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n </>\n );\n}\n","\"use client\";\n\nimport { PaymentMethodInterface } from \"../../data\";\nimport { PaymentMethodCard } from \"../details/PaymentMethodCard\";\n\ntype PaymentMethodsListProps = {\n paymentMethods: PaymentMethodInterface[];\n onUpdate: () => void;\n};\n\nexport function PaymentMethodsList({ paymentMethods, onUpdate }: PaymentMethodsListProps) {\n return (\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3\">\n {paymentMethods.map((paymentMethod) => (\n <PaymentMethodCard key={paymentMethod.id} paymentMethod={paymentMethod} onUpdate={onUpdate} />\n ))}\n </div>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { useEffect, useState } from \"react\";\nimport { Tabs, TabsList, TabsTrigger } from \"../../../../../shadcnui\";\nimport { InvoiceStatus, StripeInvoiceInterface } from \"../../data/stripe-invoice.interface\";\nimport { StripeInvoiceService } from \"../../data/stripe-invoice.service\";\nimport { InvoicesList } from \"../lists/InvoicesList\";\n\ntype StatusFilter = InvoiceStatus | \"all\";\n\nexport function InvoicesContainer() {\n const t = useTranslations();\n const [invoices, setInvoices] = useState<StripeInvoiceInterface[]>([]);\n const [loading, setLoading] = useState<boolean>(true);\n const [statusFilter, setStatusFilter] = useState<StatusFilter>(\"all\");\n\n const loadInvoices = async () => {\n setLoading(true);\n try {\n const params = statusFilter !== \"all\" ? { status: statusFilter } : undefined;\n const data = await StripeInvoiceService.listInvoices(params);\n setInvoices(data);\n } catch (error) {\n console.error(\"[InvoicesContainer] Failed to load invoices:\", error);\n setInvoices([]);\n } finally {\n setLoading(false);\n }\n };\n\n useEffect(() => {\n loadInvoices();\n }, [statusFilter]);\n\n const handleFilterChange = (value: string) => {\n setStatusFilter(value as StatusFilter);\n };\n\n return (\n <div className=\"space-y-4\">\n {/* Status Filter Tabs */}\n <Tabs value={statusFilter} onValueChange={handleFilterChange}>\n <TabsList>\n <TabsTrigger value=\"all\">{t(\"billing.ui.invoices.filter_all\")}</TabsTrigger>\n <TabsTrigger value={InvoiceStatus.PAID}>{t(\"billing.ui.invoice_status.paid\")}</TabsTrigger>\n <TabsTrigger value={InvoiceStatus.OPEN}>{t(\"billing.ui.invoice_status.open\")}</TabsTrigger>\n <TabsTrigger value={InvoiceStatus.VOID}>{t(\"billing.ui.invoice_status.void\")}</TabsTrigger>\n <TabsTrigger value={InvoiceStatus.UNCOLLECTIBLE}>{t(\"billing.ui.invoice_status.uncollectible\")}</TabsTrigger>\n </TabsList>\n </Tabs>\n\n {/* Loading State */}\n {loading && <div className=\"text-center py-8 text-muted-foreground\">{t(\"billing.ui.invoices.loading\")}</div>}\n\n {/* Empty State */}\n {!loading && invoices.length === 0 && (\n <div className=\"border border-dashed border-gray-300 rounded-lg p-8 text-center\">\n <p className=\"text-lg font-medium text-muted-foreground mb-2\">{t(\"billing.ui.invoices.empty_title\")}</p>\n <p className=\"text-sm text-muted-foreground\">{t(\"billing.ui.invoices.empty_description\")}</p>\n </div>\n )}\n\n {/* Invoices List */}\n {!loading && invoices.length > 0 && <InvoicesList invoices={invoices} onInvoicesChange={loadInvoices} />}\n </div>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { useState } from \"react\";\nimport { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from \"../../../../../shadcnui\";\nimport { formatCurrency, formatDate, useBillingFormatOptions } from \"../../../components/utils\";\nimport { StripeInvoiceInterface } from \"../../data/stripe-invoice.interface\";\nimport { InvoiceDetails } from \"../details/InvoiceDetails\";\nimport { InvoiceStatusBadge } from \"../widgets/InvoiceStatusBadge\";\n\ntype InvoicesListProps = {\n invoices: StripeInvoiceInterface[];\n onInvoicesChange: () => void;\n};\n\nexport function InvoicesList({ invoices, onInvoicesChange }: InvoicesListProps) {\n const t = useTranslations();\n const fmt = useBillingFormatOptions();\n const [selectedInvoice, setSelectedInvoice] = useState<StripeInvoiceInterface | null>(null);\n\n const handleRowClick = (invoice: StripeInvoiceInterface) => {\n setSelectedInvoice(invoice);\n };\n\n const getInvoiceNumber = (invoice: StripeInvoiceInterface): string => {\n if (invoice.stripeInvoiceNumber) {\n return invoice.stripeInvoiceNumber;\n }\n // Use last 8 characters of stripeInvoiceId as fallback\n return invoice.stripeInvoiceId.slice(-8);\n };\n\n return (\n <>\n <div className=\"border rounded-lg overflow-clip\">\n <Table>\n <TableHeader className=\"bg-muted\">\n <TableRow>\n <TableHead>{t(\"billing.ui.columns.invoice_number\")}</TableHead>\n <TableHead>{t(\"billing.ui.columns.date\")}</TableHead>\n <TableHead>{t(\"billing.ui.columns.status\")}</TableHead>\n <TableHead className=\"text-end\">{t(\"billing.ui.columns.amount\")}</TableHead>\n <TableHead>{t(\"billing.ui.columns.period\")}</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {invoices.map((invoice) => {\n const invoiceNumber = getInvoiceNumber(invoice);\n const date = formatDate(invoice.periodStart, fmt);\n const amount = formatCurrency(invoice.total, invoice.currency, fmt);\n const period = `${formatDate(invoice.periodStart, fmt)} - ${formatDate(invoice.periodEnd, fmt)}`;\n\n return (\n <TableRow\n key={invoice.id}\n onClick={() => handleRowClick(invoice)}\n className=\"cursor-pointer hover:bg-muted/50\"\n >\n <TableCell className=\"font-medium\">{invoiceNumber}</TableCell>\n <TableCell className=\"text-muted-foreground text-xs\">{date}</TableCell>\n <TableCell>\n <InvoiceStatusBadge status={invoice.status} />\n </TableCell>\n <TableCell className=\"text-end font-medium\">{amount}</TableCell>\n <TableCell className=\"text-muted-foreground text-xs\">{period}</TableCell>\n </TableRow>\n );\n })}\n </TableBody>\n </Table>\n </div>\n\n {/* Invoice Details Dialog */}\n {selectedInvoice && (\n <InvoiceDetails\n invoice={selectedInvoice}\n open={!!selectedInvoice}\n onOpenChange={(open) => !open && setSelectedInvoice(null)}\n onInvoiceChange={() => {\n onInvoicesChange();\n setSelectedInvoice(null);\n }}\n />\n )}\n </>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { Download, ExternalLink, RefreshCw } from \"lucide-react\";\nimport { Button, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from \"../../../../../shadcnui\";\nimport { formatCurrency, formatDate, useBillingFormatOptions } from \"../../../components/utils\";\nimport { InvoiceStatus, StripeInvoiceInterface } from \"../../data/stripe-invoice.interface\";\nimport { InvoiceStatusBadge } from \"../widgets/InvoiceStatusBadge\";\n\ntype InvoiceDetailsProps = {\n invoice: StripeInvoiceInterface;\n open: boolean;\n onOpenChange: (open: boolean) => void;\n onInvoiceChange: () => void;\n};\n\nexport function InvoiceDetails({\n invoice,\n open,\n onOpenChange,\n onInvoiceChange: _onInvoiceChange,\n}: InvoiceDetailsProps) {\n const t = useTranslations();\n const fmt = useBillingFormatOptions();\n const handleDownloadPDF = () => {\n if (invoice.stripePdfUrl) {\n window.open(invoice.stripePdfUrl, \"_blank\");\n }\n };\n\n const handleRetryPayment = async () => {\n // TODO: Implement retry payment logic\n };\n\n const handleViewInStripe = () => {\n if (invoice.stripeHostedInvoiceUrl) {\n window.open(invoice.stripeHostedInvoiceUrl, \"_blank\");\n }\n };\n\n const getInvoiceNumber = (): string => {\n if (invoice.stripeInvoiceNumber) {\n return invoice.stripeInvoiceNumber;\n }\n return invoice.stripeInvoiceId.slice(-8);\n };\n\n const productName = invoice.subscription?.price?.product?.name || t(\"billing.ui.cards.subscriptions.fallback_name\");\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent className=\"max-w-2xl\">\n <DialogHeader>\n <DialogTitle>{t(\"billing.ui.invoice.title\", { number: getInvoiceNumber() })}</DialogTitle>\n <DialogDescription>{formatDate(invoice.periodStart, fmt)}</DialogDescription>\n </DialogHeader>\n\n <div className=\"space-y-6\">\n {/* Status */}\n <div className=\"flex items-center gap-x-3\">\n <span className=\"text-sm font-medium text-muted-foreground\">{t(\"billing.ui.labels.status\")}</span>\n <InvoiceStatusBadge status={invoice.status} />\n </div>\n\n {/* Invoice Info Grid */}\n <div className=\"grid grid-cols-2 gap-4\">\n <div>\n <span className=\"text-muted-foreground text-xs\">{t(\"billing.ui.invoice.billing_period\")}</span>\n <p className=\"text-sm\">\n {formatDate(invoice.periodStart, fmt)} - {formatDate(invoice.periodEnd, fmt)}\n </p>\n </div>\n\n {invoice.dueDate && (\n <div>\n <span className=\"text-muted-foreground text-xs\">{t(\"billing.ui.invoice.due_date\")}</span>\n <p className=\"text-sm\">{formatDate(invoice.dueDate, fmt)}</p>\n </div>\n )}\n\n {invoice.paidAt && (\n <div>\n <span className=\"text-muted-foreground text-xs\">{t(\"billing.ui.invoice.paid_date\")}</span>\n <p className=\"text-sm\">{formatDate(invoice.paidAt, fmt)}</p>\n </div>\n )}\n\n <div>\n <span className=\"text-muted-foreground text-xs\">{t(\"billing.ui.invoice.attempt_count\")}</span>\n <p className=\"text-sm\">{invoice.attemptCount}</p>\n </div>\n </div>\n\n {/* Line Items */}\n <div>\n <h4 className=\"text-sm font-medium text-muted-foreground mb-2\">{t(\"billing.ui.invoice.line_items\")}</h4>\n <div className=\"border rounded-lg overflow-clip\">\n <table className=\"w-full\">\n <thead className=\"bg-muted\">\n <tr>\n <th className=\"text-start p-3 text-sm font-medium\">{t(\"billing.ui.columns.description\")}</th>\n <th className=\"text-end p-3 text-sm font-medium\">{t(\"billing.ui.columns.amount\")}</th>\n </tr>\n </thead>\n <tbody>\n <tr className=\"border-t\">\n <td className=\"p-3\">{productName}</td>\n <td className=\"p-3 text-end\">{formatCurrency(invoice.subtotal, invoice.currency, fmt)}</td>\n </tr>\n </tbody>\n </table>\n </div>\n </div>\n\n {/* Amount Breakdown */}\n <div className=\"space-y-2 border-t pt-4\">\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground text-xs\">{t(\"billing.ui.invoice.subtotal\")}</span>\n <span className=\"text-sm\">{formatCurrency(invoice.subtotal, invoice.currency, fmt)}</span>\n </div>\n\n {invoice.tax !== undefined && invoice.tax > 0 && (\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground text-xs\">{t(\"billing.ui.invoice.tax\")}</span>\n <span className=\"text-sm\">{formatCurrency(invoice.tax, invoice.currency, fmt)}</span>\n </div>\n )}\n\n <div className=\"flex justify-between text-lg font-semibold border-t pt-2\">\n <span>{t(\"billing.ui.invoice.total\")}</span>\n <span>{formatCurrency(invoice.total, invoice.currency, fmt)}</span>\n </div>\n\n {invoice.amountPaid > 0 && (\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground text-xs\">{t(\"billing.ui.invoice.amount_paid\")}</span>\n <span className=\"text-sm text-success\">\n {formatCurrency(invoice.amountPaid, invoice.currency, fmt)}\n </span>\n </div>\n )}\n\n {invoice.amountRemaining > 0 && (\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground text-xs\">{t(\"billing.ui.invoice.amount_due\")}</span>\n <span className=\"text-sm text-destructive\">\n {formatCurrency(invoice.amountRemaining, invoice.currency, fmt)}\n </span>\n </div>\n )}\n </div>\n\n {/* Action Buttons */}\n <div className=\"flex flex-wrap gap-2 pt-4 border-t\">\n {invoice.stripePdfUrl && (\n <Button variant=\"outline\" onClick={handleDownloadPDF}>\n <Download className=\"me-2 h-4 w-4\" />\n {t(\"billing.ui.invoice.download_pdf\")}\n </Button>\n )}\n\n {invoice.status === InvoiceStatus.OPEN && invoice.attempted && (\n <Button variant=\"default\" onClick={handleRetryPayment}>\n <RefreshCw className=\"me-2 h-4 w-4\" />\n {t(\"billing.ui.invoice.retry_payment\")}\n </Button>\n )}\n\n {invoice.stripeHostedInvoiceUrl && (\n <Button variant=\"outline\" onClick={handleViewInStripe}>\n <ExternalLink className=\"me-2 h-4 w-4\" />\n {t(\"billing.ui.invoice.view_in_stripe\")}\n </Button>\n )}\n </div>\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { Badge, type BadgeProps } from \"../../../../../shadcnui\";\nimport { InvoiceStatus } from \"../../data/stripe-invoice.interface\";\n\ntype InvoiceStatusBadgeProps = {\n status: InvoiceStatus;\n};\n\ntype StatusConfig = {\n /** Message key, translated at render time. */\n label: string;\n variant: BadgeProps[\"variant\"];\n};\n\nconst statusConfig: Record<InvoiceStatus, StatusConfig> = {\n [InvoiceStatus.DRAFT]: {\n label: \"billing.ui.invoice_status.draft\",\n variant: \"softGray\",\n },\n [InvoiceStatus.OPEN]: {\n label: \"billing.ui.invoice_status.open\",\n variant: \"softBlue\",\n },\n [InvoiceStatus.PAID]: {\n label: \"billing.ui.invoice_status.paid\",\n variant: \"softGreen\",\n },\n [InvoiceStatus.VOID]: {\n label: \"billing.ui.invoice_status.void\",\n variant: \"softGray\",\n },\n [InvoiceStatus.UNCOLLECTIBLE]: {\n label: \"billing.ui.invoice_status.uncollectible\",\n variant: \"softRed\",\n },\n};\n\nexport function InvoiceStatusBadge({ status }: InvoiceStatusBadgeProps) {\n const t = useTranslations();\n const config = statusConfig[status] || statusConfig[InvoiceStatus.DRAFT];\n\n return <Badge variant={config.variant}>{t(config.label)}</Badge>;\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { CreditCard } from \"lucide-react\";\nimport { useCallback, useEffect, useState } from \"react\";\nimport { Button } from \"../../../../../shadcnui\";\nimport { BillingAlertBanner } from \"../../../components\";\nimport { StripeSubscriptionInterface, StripeSubscriptionService, SubscriptionStatus } from \"../../data\";\nimport { SubscriptionsList } from \"../lists\";\n\ntype SubscriptionsContainerProps = {\n onOpenWizard?: (subscription?: StripeSubscriptionInterface) => void;\n hasActiveRecurringSubscription?: boolean;\n /**\n * The list a parent already holds (the billing dashboard). When given, the container\n * shows it instead of loading its own, and a change asks the parent to refresh through\n * `onSubscriptionsChange`, so the dialog and the cards behind it never disagree.\n */\n subscriptions?: StripeSubscriptionInterface[];\n onSubscriptionsChange?: () => void;\n};\n\nexport function SubscriptionsContainer({\n onOpenWizard,\n hasActiveRecurringSubscription,\n subscriptions: parentSubscriptions,\n onSubscriptionsChange,\n}: SubscriptionsContainerProps) {\n const t = useTranslations();\n const isControlled = parentSubscriptions !== undefined;\n const [ownSubscriptions, setSubscriptions] = useState<StripeSubscriptionInterface[]>([]);\n const [ownLoading, setLoading] = useState<boolean>(!isControlled);\n const subscriptions = parentSubscriptions ?? ownSubscriptions;\n const loading = isControlled ? false : ownLoading;\n\n const loadSubscriptions = useCallback(async () => {\n if (isControlled) {\n onSubscriptionsChange?.();\n return;\n }\n setLoading(true);\n try {\n const fetchedSubscriptions = await StripeSubscriptionService.listSubscriptions();\n setSubscriptions(fetchedSubscriptions);\n } catch (error) {\n console.error(\"[SubscriptionsContainer] Failed to load subscriptions:\", error);\n } finally {\n setLoading(false);\n }\n }, [isControlled, onSubscriptionsChange]);\n\n useEffect(() => {\n if (!isControlled) loadSubscriptions();\n }, []);\n\n // Detect critical subscriptions\n const criticalSubscriptions = subscriptions.filter(\n (sub) =>\n sub.status === SubscriptionStatus.PAST_DUE ||\n (sub.status === SubscriptionStatus.TRIALING &&\n sub.trialEnd &&\n new Date(sub.trialEnd).getTime() - new Date().getTime() <= 7 * 24 * 60 * 60 * 1000),\n );\n\n if (loading) {\n return (\n <div className=\"flex h-64 items-center justify-center\">\n <p className=\"text-muted-foreground\">{t(\"billing.ui.subscriptions.loading\")}</p>\n </div>\n );\n }\n\n return (\n <div className=\"flex w-full flex-col gap-y-6\">\n {/* Header */}\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-x-3\">\n <CreditCard className=\"h-8 w-8\" />\n <h1 className=\"text-primary text-3xl font-semibold\">{t(\"billing.ui.subscriptions.title\")}</h1>\n </div>\n {subscriptions.length > 0 && (\n <Button onClick={() => onOpenWizard?.()}>\n {hasActiveRecurringSubscription\n ? t(\"billing.ui.subscriptions.purchase_addons\")\n : t(\"billing.ui.subscriptions.subscribe\")}\n </Button>\n )}\n </div>\n\n {/* Alert Banners */}\n {criticalSubscriptions.map((subscription) => (\n <BillingAlertBanner key={subscription.id} subscription={subscription} />\n ))}\n\n {/* Empty state CTA */}\n {subscriptions.length === 0 && (\n <div className=\"flex flex-col items-center justify-center py-12 space-y-4\">\n <CreditCard className=\"h-16 w-16 text-muted-foreground\" />\n <div className=\"text-center\">\n <h3 className=\"text-sm font-medium mb-2\">{t(\"billing.ui.subscriptions.empty_title\")}</h3>\n <p className=\"text-muted-foreground mb-6\">{t(\"billing.ui.subscriptions.empty_description\")}</p>\n <Button onClick={() => onOpenWizard?.()}>{t(\"billing.ui.subscriptions.subscribe\")}</Button>\n </div>\n </div>\n )}\n\n {/* Subscriptions List */}\n {subscriptions.length > 0 && (\n <SubscriptionsList\n subscriptions={subscriptions}\n onSubscriptionsChange={loadSubscriptions}\n onChangePlan={(sub) => onOpenWizard?.(sub)}\n />\n )}\n </div>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { useState } from \"react\";\nimport { Button, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from \"../../../../../shadcnui\";\nimport {\n applyDiscount,\n formatCurrency,\n formatDate,\n subscriptionDiscount,\n useBillingFormatOptions,\n} from \"../../../components/utils\";\nimport { StripePriceInterface } from \"../../../stripe-price/data/stripe-price.interface\";\nimport { StripeSubscriptionInterface, SubscriptionStatus } from \"../../data\";\nimport { SubscriptionDetails } from \"../details/SubscriptionDetails\";\nimport { SubscriptionStatusBadge } from \"../widgets/SubscriptionStatusBadge\";\n\n/**\n * Formats the plan name from price data.\n * Format: \"Product Name - Nickname (Interval)\" e.g., \"Only 35 - Pro (Monthly)\"\n */\nfunction formatPlanName(price: StripePriceInterface | undefined, t: ReturnType<typeof useTranslations>): string {\n if (!price) return t(\"billing.ui.not_available\");\n\n const productName = price.product?.name || \"\";\n const nickname = price.nickname || \"\";\n\n // Format interval: \"month\" -> \"Monthly\", \"year\" -> \"Yearly\", etc.\n let interval = \"\";\n if (price.recurring?.interval) {\n const intervalMap: Record<string, string> = {\n day: t(\"billing.admin.prices.interval.day\"),\n week: t(\"billing.admin.prices.interval.week\"),\n month: t(\"billing.admin.prices.interval.month\"),\n year: t(\"billing.admin.prices.interval.year\"),\n };\n interval = intervalMap[price.recurring.interval] || price.recurring.interval;\n }\n\n // Build the plan label: \"Product - Nickname\" or just \"Product\"\n const parts = [productName, nickname].filter(Boolean);\n const planLabel = parts.join(\" - \");\n\n // Add interval in parentheses if available\n return interval ? `${planLabel} (${interval})` : planLabel || t(\"billing.ui.not_available\");\n}\n\ntype SubscriptionsListProps = {\n subscriptions: StripeSubscriptionInterface[];\n onSubscriptionsChange: () => void;\n onChangePlan?: (subscription: StripeSubscriptionInterface) => void;\n};\n\nexport function SubscriptionsList({ subscriptions, onSubscriptionsChange, onChangePlan }: SubscriptionsListProps) {\n const t = useTranslations();\n const fmt = useBillingFormatOptions();\n const [selectedSub, setSelectedSub] = useState<StripeSubscriptionInterface | null>(null);\n\n const handleRowClick = (subscription: StripeSubscriptionInterface) => {\n setSelectedSub(subscription);\n };\n\n return (\n <>\n <div className=\"border rounded-lg overflow-clip\">\n <Table>\n <TableHeader className=\"bg-muted\">\n <TableRow>\n <TableHead>{t(\"billing.ui.columns.status\")}</TableHead>\n <TableHead>{t(\"billing.ui.columns.plan\")}</TableHead>\n <TableHead>{t(\"billing.ui.columns.period\")}</TableHead>\n <TableHead className=\"text-end\">{t(\"billing.ui.columns.amount\")}</TableHead>\n <TableHead className=\"text-end\">{t(\"billing.ui.columns.actions\")}</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {subscriptions.map((subscription) => {\n const price = subscription.price;\n const amount = price?.unitAmount ? formatCurrency(price.unitAmount, price.currency, fmt) : \"0\";\n // What Stripe bills: the price after the discount the subscription carries.\n const discountedAmount = price\n ? applyDiscount(price.unitAmount, subscriptionDiscount(subscription))\n : null;\n const period = `${formatDate(subscription.currentPeriodStart, fmt)} - ${formatDate(subscription.currentPeriodEnd, fmt)}`;\n\n return (\n <TableRow\n key={subscription.id}\n onClick={() => handleRowClick(subscription)}\n className=\"cursor-pointer hover:bg-muted/50\"\n >\n <TableCell>\n <SubscriptionStatusBadge\n status={subscription.status}\n cancelAtPeriodEnd={subscription.cancelAtPeriodEnd}\n />\n </TableCell>\n <TableCell className=\"font-medium\">{formatPlanName(price, t)}</TableCell>\n <TableCell className=\"text-muted-foreground text-sm\">{period}</TableCell>\n <TableCell className=\"text-end font-medium\">\n {discountedAmount !== null && price ? (\n <>\n <span className=\"line-through text-muted-foreground me-2\">{amount}</span>\n <span className=\"text-success\">{formatCurrency(discountedAmount, price.currency, fmt)}</span>\n </>\n ) : (\n amount\n )}\n </TableCell>\n <TableCell className=\"text-end\">\n {(subscription.status === SubscriptionStatus.ACTIVE ||\n subscription.status === SubscriptionStatus.TRIALING) &&\n onChangePlan && (\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={(e) => {\n e.stopPropagation();\n onChangePlan(subscription);\n }}\n >\n {t(\"billing.ui.subscription.upgrade\")}\n </Button>\n )}\n </TableCell>\n </TableRow>\n );\n })}\n </TableBody>\n </Table>\n </div>\n\n {/* Subscription Details Dialog */}\n {selectedSub && (\n <SubscriptionDetails\n subscription={selectedSub}\n open={!!selectedSub}\n onOpenChange={(open) => !open && setSelectedSub(null)}\n onSubscriptionChange={() => {\n onSubscriptionsChange();\n setSelectedSub(null);\n }}\n onChangePlan={\n onChangePlan\n ? (sub) => {\n setSelectedSub(null); // Close details dialog first\n onChangePlan(sub); // Then open wizard at parent level\n }\n : undefined\n }\n />\n )}\n </>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { useState } from \"react\";\nimport { Button, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from \"../../../../../shadcnui\";\nimport {\n type BillingFormatOptions,\n formatCurrency,\n formatDate,\n useBillingFormatOptions,\n} from \"../../../components/utils\";\nimport { StripeCustomerService } from \"../../../stripe-customer\";\nimport { StripePriceInterface } from \"../../../stripe-price/data/stripe-price.interface\";\nimport { StripeSubscriptionInterface, StripeSubscriptionService, SubscriptionStatus } from \"../../data\";\nimport { CancelSubscriptionDialog } from \"../forms/CancelSubscriptionDialog\";\nimport { SubscriptionStatusBadge } from \"../widgets/SubscriptionStatusBadge\";\n\n/**\n * Formats the plan name from price data.\n * Format: \"Product Name - Nickname (Interval)\" e.g., \"Only 35 - Pro (Monthly)\"\n */\nfunction formatPlanName(price: StripePriceInterface | undefined, t: ReturnType<typeof useTranslations>): string {\n if (!price) return t(\"billing.ui.not_available\");\n\n const productName = price.product?.name || \"\";\n const nickname = price.nickname || \"\";\n\n // Format interval: \"month\" -> \"Monthly\", \"year\" -> \"Yearly\", etc.\n let interval = \"\";\n if (price.recurring?.interval) {\n const intervalMap: Record<string, string> = {\n day: t(\"billing.admin.prices.interval.day\"),\n week: t(\"billing.admin.prices.interval.week\"),\n month: t(\"billing.admin.prices.interval.month\"),\n year: t(\"billing.admin.prices.interval.year\"),\n };\n interval = intervalMap[price.recurring.interval] || price.recurring.interval;\n }\n\n // Build the plan label: \"Product - Nickname\" or just \"Product\"\n const parts = [productName, nickname].filter(Boolean);\n const planLabel = parts.join(\" - \");\n\n // Add interval in parentheses if available\n return interval ? `${planLabel} (${interval})` : planLabel || t(\"billing.ui.not_available\");\n}\n\n/**\n * Formats the billing amount from price data.\n */\nfunction formatBillingAmount(\n price: StripePriceInterface | undefined,\n notAvailable: string,\n fmt: BillingFormatOptions,\n): string {\n if (!price?.unitAmount) return notAvailable;\n return formatCurrency(price.unitAmount, price.currency, fmt);\n}\n\ntype SubscriptionDetailsProps = {\n subscription: StripeSubscriptionInterface;\n open: boolean;\n onOpenChange: (open: boolean) => void;\n onSubscriptionChange: () => void;\n onChangePlan?: (subscription: StripeSubscriptionInterface) => void;\n};\n\nexport function SubscriptionDetails({\n subscription,\n open,\n onOpenChange,\n onSubscriptionChange,\n onChangePlan,\n}: SubscriptionDetailsProps) {\n const t = useTranslations();\n const fmt = useBillingFormatOptions();\n const [showCancel, setShowCancel] = useState<boolean>(false);\n const [isProcessing, setIsProcessing] = useState<boolean>(false);\n\n const handleResume = async () => {\n setIsProcessing(true);\n try {\n await StripeSubscriptionService.resumeSubscription({ subscriptionId: subscription.id });\n onSubscriptionChange();\n } catch (error) {\n console.error(\"[SubscriptionDetails] Failed to resume subscription:\", error);\n } finally {\n setIsProcessing(false);\n }\n };\n\n const handleManageViaPortal = async () => {\n try {\n const { url } = await StripeCustomerService.createPortalSession();\n window.open(url, \"_blank\");\n } catch (error) {\n console.error(\"[SubscriptionDetails] Failed to create portal session:\", error);\n }\n };\n\n const canResume = subscription.status === SubscriptionStatus.PAUSED;\n const canCancel =\n subscription.status === SubscriptionStatus.ACTIVE ||\n subscription.status === SubscriptionStatus.TRIALING ||\n subscription.status === SubscriptionStatus.PAUSED;\n\n return (\n <>\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent className=\"max-w-2xl\">\n <DialogHeader>\n <DialogTitle>{t(\"billing.ui.subscription.details_title\")}</DialogTitle>\n <DialogDescription>{t(\"billing.ui.subscription.details_description\")}</DialogDescription>\n </DialogHeader>\n\n <div className=\"space-y-6\">\n {/* Status */}\n <div className=\"flex items-center gap-x-3\">\n <span className=\"text-muted-foreground text-xs\">{t(\"billing.ui.labels.status\")}</span>\n <SubscriptionStatusBadge\n status={subscription.status}\n cancelAtPeriodEnd={subscription.cancelAtPeriodEnd}\n />\n </div>\n\n {/* Plan Info */}\n <div className=\"space-y-2\">\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground text-xs\">{t(\"billing.ui.labels.plan\")}</span>\n <span className=\"text-sm\">{formatPlanName(subscription.price, t)}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground text-xs\">{t(\"billing.ui.subscription.billing_amount\")}</span>\n <span className=\"text-sm\">\n {formatBillingAmount(subscription.price, t(\"billing.ui.not_available\"), fmt)}\n </span>\n </div>\n </div>\n\n {/* Current Period */}\n <div className=\"space-y-2\">\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground text-xs\">{t(\"billing.ui.subscription.current_period\")}</span>\n <span className=\"text-sm\">\n {formatDate(subscription.currentPeriodStart, fmt)} - {formatDate(subscription.currentPeriodEnd, fmt)}\n </span>\n </div>\n </div>\n\n {/* Trial Info */}\n {subscription.trialEnd && (\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground text-xs\">{t(\"billing.ui.subscription.trial_ends\")}</span>\n <span className=\"text-sm\">{formatDate(subscription.trialEnd, fmt)}</span>\n </div>\n )}\n\n {/* Cancellation Warning */}\n {subscription.cancelAtPeriodEnd && (\n <div className=\"bg-warning/15 border-warning/30 rounded-lg border p-3\">\n <p className=\"text-warning text-xs/relaxed\">\n {t(\"billing.ui.subscription.cancel_at_period_end\", {\n date: formatDate(subscription.currentPeriodEnd, fmt),\n })}\n </p>\n </div>\n )}\n\n {/* Action Buttons */}\n <div className=\"flex flex-wrap gap-2 pt-4 border-t\">\n {onChangePlan && (\n <Button variant=\"default\" onClick={() => onChangePlan(subscription)}>\n {t(\"billing.ui.subscription.change_plan\")}\n </Button>\n )}\n\n {canResume && (\n <Button variant=\"outline\" onClick={handleResume} disabled={isProcessing}>\n {isProcessing ? t(\"billing.ui.subscription.resuming\") : t(\"billing.ui.subscription.resume\")}\n </Button>\n )}\n\n {canCancel && (\n <Button variant=\"destructive\" onClick={() => setShowCancel(true)}>\n {t(\"ui.buttons.cancel\")}\n </Button>\n )}\n\n <Button variant=\"outline\" onClick={handleManageViaPortal}>\n {t(\"billing.ui.subscription.manage_portal\")}\n </Button>\n </div>\n </div>\n </DialogContent>\n </Dialog>\n\n {/* Cancel Subscription Dialog */}\n {showCancel && (\n <CancelSubscriptionDialog\n subscription={subscription}\n open={showCancel}\n onOpenChange={setShowCancel}\n onSuccess={() => {\n onSubscriptionChange();\n setShowCancel(false);\n }}\n />\n )}\n </>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { useState } from \"react\";\nimport { SubmitHandler, useForm } from \"react-hook-form\";\nimport { z } from \"zod\";\nimport { FormTextarea } from \"../../../../../components\";\nimport {\n Button,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n Form,\n} from \"../../../../../shadcnui\";\nimport { formatDate, useBillingFormatOptions } from \"../../../components/utils\";\nimport { StripeSubscriptionInterface, StripeSubscriptionService } from \"../../data\";\n\ntype CancelSubscriptionDialogProps = {\n subscription: StripeSubscriptionInterface;\n open: boolean;\n onOpenChange: (open: boolean) => void;\n onSuccess: () => void;\n};\n\nconst formSchema = z.object({\n reason: z.string().optional(),\n});\n\nexport function CancelSubscriptionDialog({\n subscription,\n open,\n onOpenChange,\n onSuccess,\n}: CancelSubscriptionDialogProps) {\n const t = useTranslations();\n const fmt = useBillingFormatOptions();\n const [isSubmitting, setIsSubmitting] = useState<boolean>(false);\n\n const form = useForm<z.infer<typeof formSchema>>({\n resolver: zodResolver(formSchema),\n defaultValues: {\n reason: \"\",\n },\n });\n\n const onSubmit: SubmitHandler<z.infer<typeof formSchema>> = async (_values) => {\n setIsSubmitting(true);\n\n try {\n await StripeSubscriptionService.cancelSubscription({\n id: subscription.id,\n cancelImmediately: false,\n });\n\n onSuccess();\n onOpenChange(false);\n } catch (error) {\n console.error(\"[CancelSubscriptionDialog] Failed to cancel subscription:\", error);\n } finally {\n setIsSubmitting(false);\n }\n };\n\n const periodEndDate = formatDate(subscription.currentPeriodEnd, fmt);\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent className=\"max-w-md\">\n <DialogHeader>\n <DialogTitle>{t(\"billing.ui.subscription.cancel.title\")}</DialogTitle>\n <DialogDescription>{t(\"billing.ui.subscription.cancel.description\")}</DialogDescription>\n </DialogHeader>\n\n <Form {...form}>\n <form onSubmit={form.handleSubmit(onSubmit)} className=\"flex flex-col gap-y-4\">\n <div className=\"bg-primary/10 border-primary/30 text-foreground rounded-lg border p-3 text-sm\">\n {t(\"billing.ui.subscription.cancel.remains_active\", { date: periodEndDate })}\n </div>\n\n <FormTextarea\n form={form}\n id=\"reason\"\n name={t(\"billing.ui.subscription.cancel.reason\")}\n placeholder={t(\"billing.ui.subscription.cancel.reason_placeholder\")}\n className=\"min-h-24\"\n />\n\n <div className=\"flex gap-x-2 justify-end pt-2\">\n <Button type=\"button\" variant=\"outline\" onClick={() => onOpenChange(false)} disabled={isSubmitting}>\n {t(\"billing.ui.subscription.cancel.keep\")}\n </Button>\n <Button type=\"submit\" variant=\"destructive\" disabled={isSubmitting}>\n {isSubmitting\n ? t(\"billing.ui.subscription.cancel.canceling\")\n : t(\"billing.ui.subscription.cancel.confirm\")}\n </Button>\n </div>\n </form>\n </Form>\n </DialogContent>\n </Dialog>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { Badge, type BadgeProps } from \"../../../../../shadcnui\";\nimport { SubscriptionStatus } from \"../../data\";\n\ntype SubscriptionStatusBadgeProps = {\n status: SubscriptionStatus;\n cancelAtPeriodEnd?: boolean;\n};\n\ntype StatusConfig = {\n /** Message key, translated at render time. */\n label: string;\n variant: BadgeProps[\"variant\"];\n};\n\nconst statusConfig: Record<SubscriptionStatus, StatusConfig> = {\n [SubscriptionStatus.ACTIVE]: {\n label: \"billing.ui.subscription_status.active\",\n variant: \"softGreen\",\n },\n [SubscriptionStatus.TRIALING]: {\n label: \"billing.ui.subscription_status.trialing\",\n variant: \"softBlue\",\n },\n [SubscriptionStatus.PAST_DUE]: {\n label: \"billing.ui.subscription_status.past_due\",\n variant: \"softRed\",\n },\n [SubscriptionStatus.CANCELED]: {\n label: \"billing.ui.subscription_status.canceled\",\n variant: \"softGray\",\n },\n [SubscriptionStatus.PAUSED]: {\n label: \"billing.ui.subscription_status.paused\",\n variant: \"softYellow\",\n },\n [SubscriptionStatus.UNPAID]: {\n label: \"billing.ui.subscription_status.unpaid\",\n variant: \"softOrange\",\n },\n [SubscriptionStatus.INCOMPLETE]: {\n label: \"billing.ui.subscription_status.incomplete\",\n variant: \"softGray\",\n },\n [SubscriptionStatus.INCOMPLETE_EXPIRED]: {\n label: \"billing.ui.subscription_status.incomplete_expired\",\n variant: \"softGray\",\n },\n};\n\nconst cancelingConfig: StatusConfig = {\n label: \"billing.ui.cards.subscriptions.canceling\",\n variant: \"softAmber\",\n};\n\nexport function SubscriptionStatusBadge({ status, cancelAtPeriodEnd }: SubscriptionStatusBadgeProps) {\n const t = useTranslations();\n // Show \"Canceling\" when subscription is set to cancel at period end\n const config = cancelAtPeriodEnd\n ? cancelingConfig\n : statusConfig[status] || statusConfig[SubscriptionStatus.CANCELED];\n\n return <Badge variant={config.variant}>{t(config.label)}</Badge>;\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { Tabs, TabsList, TabsTrigger } from \"../../../../../shadcnui\";\n\nexport type BillingInterval = \"month\" | \"year\";\n\nexport type IntervalToggleProps = {\n value: BillingInterval;\n onChange: (interval: BillingInterval) => void;\n hasMonthly: boolean;\n hasYearly: boolean;\n};\n\nexport function IntervalToggle({ value, onChange, hasMonthly, hasYearly }: IntervalToggleProps) {\n const t = useTranslations();\n // Only render if BOTH intervals are available\n if (!hasMonthly || !hasYearly) {\n return null;\n }\n\n return (\n <Tabs value={value} onValueChange={(v) => onChange(v as BillingInterval)}>\n <TabsList>\n <TabsTrigger value=\"month\">{t(\"billing.admin.prices.interval.month\")}</TabsTrigger>\n <TabsTrigger value=\"year\">{t(\"billing.admin.prices.interval.year\")}</TabsTrigger>\n </TabsList>\n </Tabs>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { Check } from \"lucide-react\";\nimport { KeyboardEvent } from \"react\";\nimport { Badge, Button, Card, CardContent, CardFooter, CardHeader } from \"../../../../../shadcnui\";\nimport { formatCurrency, formatInterval, useBillingFormatOptions } from \"../../../components/utils\";\nimport { StripePriceInterface } from \"../../../stripe-price/data/stripe-price.interface\";\nimport { cn } from \"../../../../../utils/cn\";\n\nexport type PricingCardProps = {\n price: StripePriceInterface;\n isCurrentPlan?: boolean;\n isSelected?: boolean;\n isDisabled?: boolean;\n isLoading?: boolean;\n onSelect: (price: StripePriceInterface) => void;\n};\n\nexport function PricingCard({\n price,\n isCurrentPlan = false,\n isSelected = false,\n isDisabled = false,\n isLoading = false,\n onSelect,\n}: PricingCardProps) {\n const t = useTranslations();\n const fmt = useBillingFormatOptions();\n const description = price.description || price.nickname || t(\"billing.ui.pricing.standard\");\n const features = price.features || [];\n const formattedPrice = formatCurrency(price.unitAmount, price.currency, fmt);\n const interval = formatInterval(price, fmt);\n\n const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n if ((e.key === \"Enter\" || e.key === \" \") && !isDisabled && !isCurrentPlan) {\n e.preventDefault();\n onSelect(price);\n }\n };\n\n const handleClick = () => {\n if (!isDisabled && !isCurrentPlan && !isLoading) {\n onSelect(price);\n }\n };\n\n return (\n <Card\n role=\"radio\"\n aria-checked={isSelected}\n aria-label={t(\"billing.ui.pricing.card_aria\", { description, price: formattedPrice, interval })}\n tabIndex={isDisabled ? -1 : 0}\n onKeyDown={handleKeyDown}\n onClick={handleClick}\n className={cn(\n \"relative cursor-pointer transition-all duration-200 flex flex-col h-full\",\n \"focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2\",\n isCurrentPlan && \"bg-muted/30\",\n isSelected && !isCurrentPlan && \"ring-2 ring-primary\",\n !isDisabled && !isCurrentPlan && \"hover:shadow-md hover:border-primary/50\",\n isDisabled && \"opacity-50 pointer-events-none\",\n isLoading && \"pointer-events-none\",\n )}\n >\n {isCurrentPlan && (\n <Badge variant=\"secondary\" className=\"absolute top-2 end-2\">\n {t(\"billing.ui.pricing.current\")}\n </Badge>\n )}\n\n <CardHeader className=\"pb-2\">\n <h3 className=\"text-sm font-medium\">{description}</h3>\n </CardHeader>\n\n <CardContent className=\"pb-4 grow\">\n <div className=\"mb-4\">\n <span className=\"text-3xl font-bold\">{formattedPrice}</span>\n <span className=\"text-muted-foreground ms-1\">{interval}</span>\n </div>\n\n {features.length > 0 && (\n <ul className=\"space-y-2\">\n {features.map((feature, index) => (\n <li key={index} className=\"flex items-start gap-2\">\n <Check className=\"text-success mt-0.5 h-4 w-4 shrink-0\" />\n <span className=\"text-sm text-muted-foreground\">{feature}</span>\n </li>\n ))}\n </ul>\n )}\n </CardContent>\n\n <CardFooter>\n <Button\n variant={isCurrentPlan ? \"secondary\" : isSelected ? \"default\" : \"outline\"}\n className=\"w-full\"\n disabled={isDisabled || isCurrentPlan || isLoading}\n >\n {isLoading\n ? t(\"billing.ui.wizard.review.processing\")\n : isCurrentPlan\n ? t(\"billing.ui.pricing.current_plan\")\n : isSelected\n ? t(\"billing.ui.pricing.selected\")\n : t(\"billing.ui.wizard.steps.select_plan\")}\n </Button>\n </CardFooter>\n </Card>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { Skeleton } from \"../../../../../shadcnui\";\nimport { StripePriceInterface } from \"../../../stripe-price/data/stripe-price.interface\";\nimport { StripeProductInterface } from \"../../../stripe-product\";\nimport { BillingInterval } from \"./IntervalToggle\";\nimport { ProductPricingRow } from \"./ProductPricingRow\";\n\nexport type ProductPricingListProps = {\n products: StripeProductInterface[];\n selectedInterval: BillingInterval;\n currentPriceId?: string;\n selectedPriceId?: string;\n loadingPriceId?: string;\n loading?: boolean;\n onSelectPrice: (price: StripePriceInterface) => void;\n hideRecurringPrices?: boolean;\n hideOneTimePrices?: boolean;\n};\n\nfunction isRecurringProduct(prices: StripePriceInterface[]): boolean {\n return prices.some((p) => p.priceType === \"recurring\");\n}\n\nfunction getFilteredPrices(prices: StripePriceInterface[], selectedInterval: BillingInterval): StripePriceInterface[] {\n const isRecurring = isRecurringProduct(prices);\n\n if (!isRecurring) {\n return prices.filter((p) => p.priceType === \"one_time\");\n }\n\n const intervalPrices = prices.filter(\n (p) => p.priceType === \"recurring\" && p.recurring?.interval === selectedInterval,\n );\n\n if (intervalPrices.length === 0) {\n const fallbackInterval = selectedInterval === \"month\" ? \"year\" : \"month\";\n return prices.filter((p) => p.priceType === \"recurring\" && p.recurring?.interval === fallbackInterval);\n }\n\n return intervalPrices;\n}\n\nexport function ProductPricingList({\n products,\n selectedInterval,\n currentPriceId,\n selectedPriceId,\n loadingPriceId,\n loading = false,\n onSelectPrice,\n hideRecurringPrices = false,\n hideOneTimePrices = false,\n}: ProductPricingListProps) {\n const t = useTranslations();\n if (loading) {\n return <ProductPricingListSkeleton />;\n }\n\n if (products.length === 0) {\n return <div className=\"text-center py-8 text-muted-foreground\">{t(\"billing.ui.pricing.no_plans\")}</div>;\n }\n\n const sortedProducts = [...products].sort((a, b) => {\n const aRecurring = isRecurringProduct(a.stripePrices || []);\n const bRecurring = isRecurringProduct(b.stripePrices || []);\n if (aRecurring && !bRecurring) return -1;\n if (!aRecurring && bRecurring) return 1;\n return 0;\n });\n\n return (\n <div className=\"space-y-6\">\n {sortedProducts.map((product) => {\n const allPrices = product.stripePrices || [];\n\n // Filter prices based on hideRecurringPrices and hideOneTimePrices flags\n let pricesToFilter = allPrices;\n\n if (hideRecurringPrices) {\n pricesToFilter = pricesToFilter.filter((price) => price.priceType !== \"recurring\");\n }\n\n if (hideOneTimePrices) {\n pricesToFilter = pricesToFilter.filter((price) => price.priceType !== \"one_time\");\n }\n\n // Filter out trial prices - users shouldn't manually select trial plans\n pricesToFilter = pricesToFilter.filter((price) => !price.isTrial);\n\n const filteredPrices = getFilteredPrices(pricesToFilter, selectedInterval);\n\n if (filteredPrices.length === 0) {\n return null;\n }\n\n return (\n <ProductPricingRow\n key={product.id}\n product={product}\n prices={filteredPrices}\n currentPriceId={currentPriceId}\n selectedPriceId={selectedPriceId}\n loadingPriceId={loadingPriceId}\n onSelectPrice={onSelectPrice}\n />\n );\n })}\n </div>\n );\n}\n\nfunction ProductPricingListSkeleton() {\n return (\n <div className=\"space-y-6\">\n {[1, 2].map((rowIndex) => (\n <div key={rowIndex} className=\"space-y-3\">\n <Skeleton className=\"h-6 w-32\" />\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4\">\n {[1, 2, 3].map((cardIndex) => (\n <div key={cardIndex} className=\"p-4 rounded-lg border animate-pulse space-y-3\">\n <Skeleton className=\"h-6 w-24\" />\n <Skeleton className=\"h-8 w-32\" />\n <div className=\"space-y-2\">\n <Skeleton className=\"h-4 w-full\" />\n <Skeleton className=\"h-4 w-3/4\" />\n </div>\n <Skeleton className=\"h-10 w-full\" />\n </div>\n ))}\n </div>\n </div>\n ))}\n </div>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { StripePriceInterface } from \"../../../stripe-price/data/stripe-price.interface\";\nimport { StripeProductInterface } from \"../../../stripe-product\";\nimport { SectionHeader } from \"../../../../../components/typography\";\nimport { PricingCard } from \"./PricingCard\";\n\nexport type ProductPricingRowProps = {\n product: StripeProductInterface;\n prices: StripePriceInterface[]; // Multiple prices for this product\n currentPriceId?: string;\n selectedPriceId?: string;\n loadingPriceId?: string;\n onSelectPrice: (price: StripePriceInterface) => void;\n};\n\nexport function ProductPricingRow({\n product,\n prices,\n currentPriceId,\n selectedPriceId,\n loadingPriceId,\n onSelectPrice,\n}: ProductPricingRowProps) {\n const t = useTranslations();\n if (prices.length === 0) {\n return null;\n }\n\n return (\n <div className=\"space-y-3\">\n {/* Product name header */}\n <SectionHeader>{product.name}</SectionHeader>\n\n {/* Price cards in columns */}\n <div\n className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4\"\n role=\"radiogroup\"\n aria-label={t(\"billing.ui.pricing.options_aria\", { name: product.name })}\n >\n {prices\n .sort((a, b) => (a.unitAmount ?? 0) - (b.unitAmount ?? 0))\n .map((price) => (\n <PricingCard\n key={price.id}\n price={price}\n isCurrentPlan={price.id === currentPriceId}\n isSelected={price.id === selectedPriceId}\n isLoading={price.id === loadingPriceId}\n onSelect={onSelectPrice}\n />\n ))}\n </div>\n </div>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { ProrationPreviewInterface } from \"../../../stripe-invoice/data/stripe-invoice.interface\";\nimport { formatCurrency, formatDate, useBillingFormatOptions } from \"../../../components/utils\";\n\ntype ProrationPreviewProps = {\n preview: ProrationPreviewInterface;\n};\n\nexport function ProrationPreview({ preview }: ProrationPreviewProps) {\n const t = useTranslations();\n const fmt = useBillingFormatOptions();\n return (\n <div className=\"bg-primary/10 border-primary/30 rounded-lg border p-4\">\n <h4 className=\"text-primary mb-3 font-semibold\">{t(\"billing.ui.proration.title\")}</h4>\n\n <div className=\"space-y-2\">\n {preview.lineItems.map((item, index) => (\n <div key={index} className=\"flex justify-between text-sm\">\n <span className=\"text-muted-foreground\">{item.description}</span>\n <span className={`font-medium ${item.amount < 0 ? \"text-success\" : \"text-foreground\"}`}>\n {formatCurrency(item.amount, preview.currency, fmt)}\n </span>\n </div>\n ))}\n\n <div className=\"border-primary/30 mt-2 border-t pt-2\">\n <div className=\"flex justify-between font-semibold\">\n <span className=\"text-foreground\">{t(\"billing.ui.proration.net_due_today\")}</span>\n <span className=\"text-foreground\">{formatCurrency(preview.immediateCharge, preview.currency, fmt)}</span>\n </div>\n </div>\n\n {preview.lineItems.length > 0 && preview.lineItems[0].period && (\n <div className=\"text-muted-foreground mt-2 text-xs\">\n {t(\"billing.ui.proration.next_invoice\", {\n date: formatDate(preview.lineItems[0].period.end, fmt),\n amount: formatCurrency(preview.amountDue, preview.currency, fmt),\n })}\n </div>\n )}\n </div>\n </div>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { Check, Loader2 } from \"lucide-react\";\nimport { Button } from \"../../../../../shadcnui\";\nimport { formatCurrency, formatInterval, useBillingFormatOptions } from \"../../../components/utils\";\nimport { StripePriceInterface } from \"../../../stripe-price/data/stripe-price.interface\";\n\ntype SubscriptionConfirmationProps = {\n price: StripePriceInterface;\n isLoading: boolean;\n onConfirm: () => void;\n onCancel: () => void;\n};\n\nexport function SubscriptionConfirmation({ price, isLoading, onConfirm, onCancel }: SubscriptionConfirmationProps) {\n const t = useTranslations();\n const fmt = useBillingFormatOptions();\n const productName = price.product?.name || price.nickname || t(\"billing.ui.wizard.review.selected_plan\");\n const productDescription = price.product?.description || price.description;\n const features = price.features || [];\n\n return (\n <div className=\"bg-accent/10 border border-accent/30 rounded-lg p-4\">\n <h4 className=\"font-semibold mb-3\">{t(\"billing.ui.confirmation.title\")}</h4>\n\n <div className=\"space-y-3\">\n {/* Plan name and description */}\n <div>\n <div className=\"font-medium\">{productName}</div>\n {productDescription && <div className=\"text-sm text-muted-foreground\">{productDescription}</div>}\n </div>\n\n {/* Price */}\n <div className=\"text-lg font-semibold\">\n {formatCurrency(price.unitAmount, price.currency, fmt)}\n <span className=\"text-sm font-normal text-muted-foreground\">{formatInterval(price, fmt)}</span>\n </div>\n\n {/* Features */}\n {features.length > 0 && (\n <div className=\"space-y-1\">\n {features.map((feature, index) => (\n <div key={index} className=\"flex items-center gap-2 text-sm \">\n <Check className=\"h-4 w-4 text-primary\" />\n <span>{feature}</span>\n </div>\n ))}\n </div>\n )}\n\n {/* Action buttons */}\n <div className=\"flex justify-end gap-3 pt-2 border-t border-accent/30\">\n <Button variant=\"outline\" onClick={onCancel} disabled={isLoading}>\n {t(\"ui.buttons.cancel\")}\n </Button>\n <Button onClick={onConfirm} disabled={isLoading}>\n {isLoading ? (\n <>\n <Loader2 className=\"h-4 w-4 animate-spin me-2\" />\n {t(\"billing.ui.wizard.review.processing\")}\n </>\n ) : (\n t(\"billing.ui.confirmation.subscribe\")\n )}\n </Button>\n </div>\n </div>\n </div>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { useCallback, useEffect, useRef } from \"react\";\nimport { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from \"../../../../../shadcnui\";\nimport { StripeSubscriptionInterface } from \"../../data\";\nimport { useSubscriptionWizard } from \"../../hooks/useSubscriptionWizard\";\nimport { WizardProgressIndicator } from \"./WizardProgressIndicator\";\nimport { WizardStepPlanSelection } from \"./WizardStepPlanSelection\";\nimport { WizardStepReview } from \"./WizardStepReview\";\nimport { WizardStepPaymentMethod } from \"./WizardStepPaymentMethod\";\n\nexport type SubscriptionWizardProps = {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n onSuccess: () => void;\n hasActiveRecurringSubscription: boolean;\n subscription?: StripeSubscriptionInterface;\n};\n\nexport function SubscriptionWizard({\n open,\n onOpenChange,\n onSuccess,\n hasActiveRecurringSubscription,\n subscription,\n}: SubscriptionWizardProps) {\n const t = useTranslations();\n const handleClose = useCallback(() => onOpenChange(false), [onOpenChange]);\n\n const { state, actions } = useSubscriptionWizard({\n subscription,\n onSuccess,\n onClose: handleClose,\n });\n\n // Track if we've already checked payment method for this open state\n const hasCheckedRef = useRef(false);\n\n // Check payment method on mount\n useEffect(() => {\n if (open && !hasCheckedRef.current) {\n hasCheckedRef.current = true;\n actions.checkPaymentMethod();\n }\n if (!open) {\n hasCheckedRef.current = false;\n }\n }, [open, actions.checkPaymentMethod]);\n\n // Reset state when dialog closes\n useEffect(() => {\n if (!open) {\n actions.reset();\n }\n }, [open, actions.reset]);\n\n const isPurchasingAddons = hasActiveRecurringSubscription && !subscription;\n const isChangePlanMode = !!subscription;\n\n const dialogTitle = subscription\n ? t(\"billing.ui.wizard.change_title\")\n : isPurchasingAddons\n ? t(\"billing.ui.subscriptions.purchase_addons\")\n : t(\"billing.ui.subscriptions.subscribe\");\n\n const dialogDescription = subscription\n ? t(\"billing.ui.wizard.change_description\")\n : isPurchasingAddons\n ? t(\"billing.ui.wizard.addons_description\")\n : t(\"billing.ui.wizard.subscribe_description\");\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent className=\"max-w-2xl max-h-[90vh] overflow-y-auto\">\n <DialogHeader>\n <DialogTitle>{dialogTitle}</DialogTitle>\n <DialogDescription>{dialogDescription}</DialogDescription>\n </DialogHeader>\n\n <WizardProgressIndicator currentStep={state.step} />\n\n {state.step === \"plan-selection\" && (\n <WizardStepPlanSelection\n selectedPrice={state.selectedPrice}\n selectedInterval={state.selectedInterval}\n currentPriceId={subscription?.price?.id}\n hideRecurringPrices={isPurchasingAddons}\n hideOneTimePrices={isChangePlanMode}\n onSelectPrice={actions.selectPrice}\n onIntervalChange={actions.setInterval}\n onNext={actions.goToReview}\n isProcessing={state.isProcessing}\n />\n )}\n\n {state.step === \"review\" && (\n <WizardStepReview\n selectedPrice={state.selectedPrice}\n subscription={subscription}\n prorationPreview={state.prorationPreview}\n hasPaymentMethod={state.hasPaymentMethod}\n error={state.error}\n isProcessing={state.isProcessing}\n onBack={() => actions.goToStep(\"plan-selection\")}\n onAddPaymentMethod={() => actions.goToStep(\"payment-method\")}\n onConfirm={actions.confirmSubscription}\n promotionCode={state.promotionCode}\n isValidatingPromoCode={state.isValidatingPromoCode}\n promoCodeError={state.promoCodeError}\n onApplyPromoCode={actions.validatePromoCode}\n onRemovePromoCode={actions.clearPromoCode}\n isTrialUpgrade={state.isTrialSubscription}\n />\n )}\n\n {state.step === \"payment-method\" && (\n <WizardStepPaymentMethod\n onBack={() => actions.goToStep(\"review\")}\n onSuccess={actions.handlePaymentMethodSuccess}\n isProcessing={state.isProcessing}\n />\n )}\n </DialogContent>\n </Dialog>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { useCallback, useMemo, useReducer, useRef } from \"react\";\nimport { v4 } from \"uuid\";\nimport { useI18nLocale } from \"../../../../i18n\";\nimport { StripeCustomerService } from \"../../stripe-customer/data/stripe-customer.service\";\nimport { ProrationPreviewInterface } from \"../../stripe-invoice/data/stripe-invoice.interface\";\nimport { StripePriceInterface } from \"../../stripe-price/data/stripe-price.interface\";\nimport { StripeCouponCodeService } from \"../../stripe-coupon/data/stripe-coupon-code.service\";\nimport { toPromotionCodeResult } from \"../../stripe-coupon/utils/to-promotion-code-result\";\nimport { PromotionCodeValidationResult, StripePromotionCodeService } from \"../../stripe-promotion-code\";\nimport { BillingInterval } from \"../components/widgets/IntervalToggle\";\nimport {\n StripeSubscriptionCreateResponse,\n StripeSubscriptionInterface,\n StripeSubscriptionService,\n SubscriptionStatus,\n} from \"../data\";\nimport {\n isDiscountSoldOut,\n isDiscountUnavailable,\n promotionCodeToSend,\n shouldLookUpAutomatic,\n} from \"./wizard-promotion\";\nimport { useConfirmSubscriptionPayment } from \"./useConfirmSubscriptionPayment\";\nimport { clientSecretToConfirm, subscriptionIdToSync } from \"./wizard-payment\";\n\nexport type WizardStep = \"plan-selection\" | \"review\" | \"payment-method\";\n\nexport type WizardState = {\n step: WizardStep;\n selectedPrice: StripePriceInterface | null;\n selectedInterval: BillingInterval;\n hasPaymentMethod: boolean;\n isProcessing: boolean;\n error: string | null;\n prorationPreview: ProrationPreviewInterface | null;\n // Promotion code state\n promotionCode: PromotionCodeValidationResult | null;\n isValidatingPromoCode: boolean;\n promoCodeError: string | null;\n // Trial state\n isTrialSubscription: boolean;\n};\n\ntype WizardAction =\n | { type: \"SET_STEP\"; step: WizardStep }\n | { type: \"SELECT_PRICE\"; price: StripePriceInterface }\n | { type: \"SET_INTERVAL\"; interval: BillingInterval }\n | { type: \"SET_HAS_PAYMENT_METHOD\"; hasMethod: boolean }\n | { type: \"SET_PROCESSING\"; isProcessing: boolean }\n | { type: \"SET_ERROR\"; error: string | null }\n | { type: \"SET_PRORATION_PREVIEW\"; preview: ProrationPreviewInterface | null }\n | { type: \"SET_PROMOTION_CODE\"; code: PromotionCodeValidationResult | null }\n | { type: \"SET_VALIDATING_PROMO_CODE\"; isValidating: boolean }\n | { type: \"SET_PROMO_CODE_ERROR\"; error: string | null }\n | { type: \"SET_IS_TRIAL_SUBSCRIPTION\"; isTrial: boolean }\n | { type: \"RESET\" };\n\nconst initialState: WizardState = {\n step: \"plan-selection\",\n selectedPrice: null,\n selectedInterval: \"month\",\n hasPaymentMethod: false,\n isProcessing: false,\n error: null,\n prorationPreview: null,\n promotionCode: null,\n isValidatingPromoCode: false,\n promoCodeError: null,\n isTrialSubscription: false,\n};\n\nfunction wizardReducer(state: WizardState, action: WizardAction): WizardState {\n switch (action.type) {\n case \"SET_STEP\":\n return { ...state, step: action.step, error: null };\n case \"SELECT_PRICE\":\n return { ...state, selectedPrice: action.price };\n case \"SET_INTERVAL\":\n return { ...state, selectedInterval: action.interval };\n case \"SET_HAS_PAYMENT_METHOD\":\n return { ...state, hasPaymentMethod: action.hasMethod };\n case \"SET_PROCESSING\":\n return { ...state, isProcessing: action.isProcessing };\n case \"SET_ERROR\":\n return { ...state, error: action.error };\n case \"SET_PRORATION_PREVIEW\":\n return { ...state, prorationPreview: action.preview };\n case \"SET_PROMOTION_CODE\":\n return { ...state, promotionCode: action.code, promoCodeError: null };\n case \"SET_VALIDATING_PROMO_CODE\":\n return { ...state, isValidatingPromoCode: action.isValidating };\n case \"SET_PROMO_CODE_ERROR\":\n return { ...state, promoCodeError: action.error };\n case \"SET_IS_TRIAL_SUBSCRIPTION\":\n return { ...state, isTrialSubscription: action.isTrial };\n case \"RESET\":\n return initialState;\n default:\n return state;\n }\n}\n\nexport type UseSubscriptionWizardOptions = {\n subscription?: StripeSubscriptionInterface;\n onSuccess: () => void;\n onClose: () => void;\n};\n\nexport function useSubscriptionWizard({ subscription, onSuccess, onClose }: UseSubscriptionWizardOptions) {\n const t = useTranslations();\n const locale = useI18nLocale();\n const { confirmPayment } = useConfirmSubscriptionPayment();\n const [state, dispatch] = useReducer(wizardReducer, {\n ...initialState,\n selectedPrice: subscription?.price || null,\n });\n\n // Use refs for callbacks to avoid dependency changes\n const onSuccessRef = useRef(onSuccess);\n const onCloseRef = useRef(onClose);\n onSuccessRef.current = onSuccess;\n onCloseRef.current = onClose;\n\n const checkPaymentMethod = useCallback(async () => {\n try {\n const methods = await StripeCustomerService.listPaymentMethods();\n dispatch({ type: \"SET_HAS_PAYMENT_METHOD\", hasMethod: methods.length > 0 });\n } catch (error) {\n console.error(\"[useSubscriptionWizard] Failed to check payment methods:\", error);\n dispatch({ type: \"SET_HAS_PAYMENT_METHOD\", hasMethod: false });\n }\n }, []);\n\n const selectPrice = useCallback((price: StripePriceInterface) => {\n dispatch({ type: \"SELECT_PRICE\", price });\n }, []);\n\n const setInterval = useCallback((interval: BillingInterval) => {\n dispatch({ type: \"SET_INTERVAL\", interval });\n }, []);\n\n const goToStep = useCallback((step: WizardStep) => {\n dispatch({ type: \"SET_STEP\", step });\n }, []);\n\n /**\n * Puts the automatic code for this price (or none) in the wizard state, so the\n * review shows what the server would really charge. Returns false when the\n * lookup itself failed: the caller must then not show a price as the truth.\n */\n const lookUpAutomaticCode = useCallback(async (priceId: string): Promise<boolean> => {\n try {\n const automaticCode = await StripeCouponCodeService.findAutomatic({ priceId });\n dispatch({\n type: \"SET_PROMOTION_CODE\",\n code: automaticCode ? toPromotionCodeResult(automaticCode) : null,\n });\n return true;\n } catch (error) {\n console.error(\"[useSubscriptionWizard] Failed to look up the automatic promotion code:\", error);\n dispatch({ type: \"SET_PROMOTION_CODE\", code: null });\n return false;\n }\n }, []);\n\n /**\n * Re-reads the automatic code after the review's code changed. When the lookup\n * fails, the review price cannot be trusted: back to plan selection.\n */\n const refreshAutomaticCodeOrLeaveReview = useCallback(\n async (priceId: string): Promise<void> => {\n const found = await lookUpAutomaticCode(priceId);\n if (!found) {\n // SET_STEP clears the error, so the error goes second\n dispatch({ type: \"SET_STEP\", step: \"plan-selection\" });\n dispatch({ type: \"SET_ERROR\", error: t(\"billing.ui.wizard.errors.prepare_review\") });\n }\n },\n [lookUpAutomaticCode, t],\n );\n\n const goToReview = useCallback(async () => {\n if (!state.selectedPrice) return;\n\n dispatch({ type: \"SET_PROCESSING\", isProcessing: true });\n\n try {\n // Check payment method first\n await checkPaymentMethod();\n\n // Check if current subscription is trial\n const isTrialUpgrade = subscription?.status === SubscriptionStatus.TRIALING;\n dispatch({ type: \"SET_IS_TRIAL_SUBSCRIPTION\", isTrial: isTrialUpgrade });\n\n // For trial upgrades, require payment method before review\n if (isTrialUpgrade && !state.hasPaymentMethod) {\n const methods = await StripeCustomerService.listPaymentMethods();\n if (methods.length === 0) {\n dispatch({ type: \"SET_STEP\", step: \"payment-method\" });\n dispatch({\n type: \"SET_ERROR\",\n error: t(\"billing.ui.wizard.errors.payment_method_required\"),\n });\n return;\n }\n }\n\n // Fill the automatic code for this price, unless the GM typed one\n // A failed lookup never shows full price as if nothing applied: stay off the review\n if (shouldLookUpAutomatic(state.promotionCode)) {\n const found = await lookUpAutomaticCode(state.selectedPrice.id);\n if (!found) {\n dispatch({ type: \"SET_ERROR\", error: t(\"billing.ui.wizard.errors.prepare_review\") });\n return;\n }\n }\n\n // If editing subscription, get proration preview\n if (subscription && state.selectedPrice.id !== subscription.price?.id) {\n const preview = await StripeSubscriptionService.getProrationPreview({\n subscriptionId: subscription.id,\n newPriceId: state.selectedPrice.id,\n });\n dispatch({ type: \"SET_PRORATION_PREVIEW\", preview });\n }\n\n dispatch({ type: \"SET_STEP\", step: \"review\" });\n } catch (error: any) {\n console.error(\"[useSubscriptionWizard] Error preparing review:\", error);\n dispatch({ type: \"SET_ERROR\", error: error?.message || t(\"billing.ui.wizard.errors.prepare_review\") });\n } finally {\n dispatch({ type: \"SET_PROCESSING\", isProcessing: false });\n }\n }, [\n state.selectedPrice,\n state.hasPaymentMethod,\n state.promotionCode,\n subscription,\n checkPaymentMethod,\n lookUpAutomaticCode,\n t,\n ]);\n\n const confirmSubscription = useCallback(async () => {\n if (!state.selectedPrice) return;\n\n dispatch({ type: \"SET_PROCESSING\", isProcessing: true });\n dispatch({ type: \"SET_ERROR\", error: null });\n\n try {\n let result: StripeSubscriptionCreateResponse;\n if (subscription) {\n // Change existing subscription\n const changePlanParams = {\n id: subscription.id,\n newPriceId: state.selectedPrice.id,\n promotionCode: promotionCodeToSend(state.promotionCode),\n };\n result = await StripeSubscriptionService.changePlanWithMeta(changePlanParams);\n } else {\n // Create new subscription\n const createParams = {\n id: v4(),\n priceId: state.selectedPrice.id,\n promotionCode: promotionCodeToSend(state.promotionCode),\n };\n result = await StripeSubscriptionService.createSubscription(createParams);\n }\n\n // The first payment waits on the browser (confirmation, 3D Secure): until it\n // is confirmed nothing is paid, so the wizard stays open on a failure\n const clientSecret = clientSecretToConfirm(result.meta);\n if (clientSecret) {\n const confirmation = await confirmPayment(clientSecret);\n if (!confirmation.success) {\n console.error(\"[useSubscriptionWizard] Payment not confirmed:\", confirmation.error);\n dispatch({\n type: \"SET_ERROR\",\n error: t(\"billing.ui.wizard.errors.payment_not_confirmed\", { reason: confirmation.error ?? \"\" }),\n });\n return;\n }\n\n // Our copy is still incomplete until the webhooks land: store what Stripe has now,\n // so the billing page shows the plan at once. A failed sync never blocks success.\n const subscriptionId = subscriptionIdToSync(result.subscription, subscription);\n if (subscriptionId) {\n try {\n await StripeSubscriptionService.syncSubscription({ subscriptionId });\n } catch (syncError) {\n console.error(\"[useSubscriptionWizard] Subscription sync after payment failed:\", syncError);\n }\n }\n }\n\n onSuccessRef.current();\n onCloseRef.current();\n } catch (error: any) {\n // 410 discount_sold_out (the automatic discount's last place has just gone) or\n // 422 discount_unavailable (the automatic code shown is no longer valid): nothing\n // was charged. Never retry: drop the code and re-read the automatic one, so the\n // review shows the price that would really be charged now.\n const soldOut = isDiscountSoldOut(error);\n if (soldOut || isDiscountUnavailable(error)) {\n dispatch({\n type: \"SET_ERROR\",\n error: soldOut\n ? t(\"billing.ui.wizard.errors.discount_sold_out\")\n : t(\"billing.ui.wizard.errors.discount_unavailable\"),\n });\n dispatch({ type: \"SET_PROMOTION_CODE\", code: null });\n await refreshAutomaticCodeOrLeaveReview(state.selectedPrice.id);\n return;\n }\n\n // Handle 409 Conflict - duplicate recurring subscription\n if (error?.status === 409 || error?.response?.status === 409) {\n dispatch({\n type: \"SET_ERROR\",\n error: t(\"billing.ui.wizard.errors.already_subscribed\"),\n });\n return;\n }\n\n // Handle 402 - payment method required\n if (error?.status === 402 || error?.response?.status === 402) {\n dispatch({ type: \"SET_HAS_PAYMENT_METHOD\", hasMethod: false });\n dispatch({ type: \"SET_STEP\", step: \"payment-method\" });\n return;\n }\n\n dispatch({ type: \"SET_ERROR\", error: error?.message || t(\"billing.ui.wizard.errors.process_subscription\") });\n } finally {\n dispatch({ type: \"SET_PROCESSING\", isProcessing: false });\n }\n }, [state.selectedPrice, state.promotionCode, subscription, refreshAutomaticCodeOrLeaveReview, confirmPayment, t]);\n\n const handlePaymentMethodSuccess = useCallback(async () => {\n dispatch({ type: \"SET_HAS_PAYMENT_METHOD\", hasMethod: true });\n // Prepare the review exactly as \"Next: Review\" does: the trial path sent the GM\n // here before the automatic-code lookup and the proration preview had run\n await goToReview();\n }, [goToReview]);\n\n const reset = useCallback(() => {\n dispatch({ type: \"RESET\" });\n }, []);\n\n const validatePromoCode = useCallback(\n async (code: string) => {\n dispatch({ type: \"SET_VALIDATING_PROMO_CODE\", isValidating: true });\n dispatch({ type: \"SET_PROMO_CODE_ERROR\", error: null });\n\n try {\n const result = await StripePromotionCodeService.validatePromotionCode({\n code,\n stripePriceId: state.selectedPrice?.id,\n language: locale,\n });\n\n if (result.valid) {\n dispatch({ type: \"SET_PROMOTION_CODE\", code: result });\n } else {\n dispatch({\n type: \"SET_PROMO_CODE_ERROR\",\n error: result.errorMessage || t(\"billing.ui.wizard.errors.invalid_promo_code\"),\n });\n }\n } catch (error: any) {\n console.error(\"[useSubscriptionWizard] Promo code validation error:\", error);\n dispatch({\n type: \"SET_PROMO_CODE_ERROR\",\n error: error?.message || t(\"billing.ui.wizard.errors.validate_promo_code\"),\n });\n } finally {\n dispatch({ type: \"SET_VALIDATING_PROMO_CODE\", isValidating: false });\n }\n },\n [state.selectedPrice?.id, t, locale],\n );\n\n const clearPromoCode = useCallback(async () => {\n dispatch({ type: \"SET_PROMOTION_CODE\", code: null });\n dispatch({ type: \"SET_PROMO_CODE_ERROR\", error: null });\n dispatch({ type: \"SET_ERROR\", error: null });\n\n if (!state.selectedPrice) return;\n\n // Removing a typed code: the server attaches the automatic code again, so the\n // review must show it again\n dispatch({ type: \"SET_PROCESSING\", isProcessing: true });\n try {\n await refreshAutomaticCodeOrLeaveReview(state.selectedPrice.id);\n } finally {\n dispatch({ type: \"SET_PROCESSING\", isProcessing: false });\n }\n }, [state.selectedPrice, refreshAutomaticCodeOrLeaveReview]);\n\n const actions = useMemo(\n () => ({\n selectPrice,\n setInterval,\n goToStep,\n goToReview,\n confirmSubscription,\n handlePaymentMethodSuccess,\n checkPaymentMethod,\n reset,\n validatePromoCode,\n clearPromoCode,\n }),\n [\n selectPrice,\n setInterval,\n goToStep,\n goToReview,\n confirmSubscription,\n handlePaymentMethodSuccess,\n checkPaymentMethod,\n reset,\n validatePromoCode,\n clearPromoCode,\n ],\n );\n\n return {\n state,\n actions,\n };\n}\n","import { PromotionCodeValidationResult } from \"../../stripe-promotion-code/data/stripe-promotion-code.interface\";\n\n/**\n * The promotion code the wizard sends to the API: the one the review shows,\n * typed or automatic. Sending the automatic code too lets the server refuse\n * the payment when that code is no longer valid, instead of charging a price\n * the GM was not shown.\n */\nexport function promotionCodeToSend(code: PromotionCodeValidationResult | null): string | undefined {\n if (!code) return undefined;\n return code.promotionCodeId;\n}\n\n/**\n * True when the wizard should ask for the automatic code: no code yet, or the\n * current one is automatic. A code the GM typed always wins.\n */\nexport function shouldLookUpAutomatic(code: PromotionCodeValidationResult | null): boolean {\n return !code || code.automatic === true;\n}\n\n/** `value[key]` when `value` is an object, else undefined. */\nfunction read(value: unknown, key: string): unknown {\n return typeof value === \"object\" && value !== null ? Reflect.get(value, key) : undefined;\n}\n\nfunction errorStatus(error: unknown): number | undefined {\n const status = read(error, \"status\") ?? read(read(error, \"response\"), \"status\");\n return typeof status === \"number\" ? status : undefined;\n}\n\n/**\n * The `code` of an API error. The nestjs-neo4jsonapi `HttpExceptionFilter`\n * answers with a JSON:API error document, which `AbstractService.callApi`\n * attaches to the thrown error as `body`:\n * `{ message, errors: [{ status, code, title, detail, meta: { context: { code } } }] }`.\n */\nfunction errorCode(error: unknown): string | undefined {\n const body = read(error, \"body\");\n const errors = read(body, \"errors\");\n const first = Array.isArray(errors) ? errors[0] : undefined;\n const code = read(first, \"code\") ?? read(read(read(first, \"meta\"), \"context\"), \"code\") ?? read(body, \"code\");\n return typeof code === \"string\" ? code : undefined;\n}\n\n/**\n * True when the API refused the payment because the automatic discount has no\n * places left (HTTP 410, code `discount_sold_out`). Nothing was charged.\n */\nexport function isDiscountSoldOut(error: unknown): boolean {\n return errorStatus(error) === 410 && errorCode(error) === \"discount_sold_out\";\n}\n\n/**\n * True when the API refused the payment because the automatic code the review\n * showed is no longer valid (HTTP 422, code `discount_unavailable`). Nothing was\n * charged.\n */\nexport function isDiscountUnavailable(error: unknown): boolean {\n return errorStatus(error) === 422 && errorCode(error) === \"discount_unavailable\";\n}\n","\"use client\";\n\nimport { useStripe } from \"@stripe/react-stripe-js\";\nimport { useTranslations } from \"next-intl\";\nimport { useCallback, useState } from \"react\";\n\nexport interface ConfirmPaymentResult {\n success: boolean;\n error?: string;\n}\n\nexport interface UseConfirmSubscriptionPaymentReturn {\n confirmPayment: (clientSecret: string) => Promise<ConfirmPaymentResult>;\n isConfirming: boolean;\n}\n\n/**\n * Hook for confirming subscription payments using Stripe's SCA-compliant flow.\n *\n * This hook wraps stripe.confirmCardPayment() and handles:\n * - Loading state during confirmation\n * - Error handling for Stripe errors\n * - 3D Secure authentication popups (handled automatically by Stripe)\n *\n * @example\n * ```tsx\n * const { confirmPayment, isConfirming } = useConfirmSubscriptionPayment();\n *\n * const handleCreateSubscription = async () => {\n * const result = await StripeSubscriptionService.createSubscription({ priceId });\n *\n * if (result.meta.requiresAction && result.meta.clientSecret) {\n * const confirmation = await confirmPayment(result.meta.clientSecret);\n * if (!confirmation.success) {\n * setError(confirmation.error);\n * return;\n * }\n * }\n *\n * // Subscription is now active\n * };\n * ```\n */\nexport function useConfirmSubscriptionPayment(): UseConfirmSubscriptionPaymentReturn {\n const stripe = useStripe();\n const t = useTranslations();\n const [isConfirming, setIsConfirming] = useState(false);\n\n const confirmPayment = useCallback(\n async (clientSecret: string): Promise<ConfirmPaymentResult> => {\n if (!stripe) {\n console.error(\"[useConfirmSubscriptionPayment] Stripe not initialized\");\n return {\n success: false,\n error: t(\"billing.ui.payment_confirm.errors.not_initialized\"),\n };\n }\n\n if (!clientSecret) {\n console.error(\"[useConfirmSubscriptionPayment] No client secret provided\");\n return {\n success: false,\n error: t(\"billing.ui.payment_confirm.errors.missing_details\"),\n };\n }\n\n setIsConfirming(true);\n\n try {\n const { error: stripeError, paymentIntent } = await stripe.confirmCardPayment(clientSecret);\n\n if (stripeError) {\n console.error(\"[useConfirmSubscriptionPayment] Stripe error:\", stripeError);\n return {\n success: false,\n error: stripeError.message || t(\"billing.ui.payment_confirm.errors.failed\"),\n };\n }\n\n if (paymentIntent?.status === \"succeeded\") {\n return { success: true };\n }\n\n if (paymentIntent?.status === \"requires_action\") {\n // This shouldn't happen as Stripe handles 3DS inline, but handle it just in case\n return {\n success: false,\n error: t(\"billing.ui.payment_confirm.errors.requires_action\"),\n };\n }\n\n return {\n success: false,\n error: t(\"billing.ui.payment_confirm.errors.not_completed\"),\n };\n } catch (err: any) {\n console.error(\"[useConfirmSubscriptionPayment] Unexpected error:\", err);\n return {\n success: false,\n error: err.message || t(\"billing.ui.payment_confirm.errors.unexpected\"),\n };\n } finally {\n setIsConfirming(false);\n }\n },\n [stripe, t],\n );\n\n return {\n confirmPayment,\n isConfirming,\n };\n}\n","import { StripeSubscriptionCreateMeta } from \"../data/stripe-subscription.interface\";\n\n/**\n * The client secret the wizard must confirm in the browser after a subscribe\n * or a trial upgrade, or null when the payment needs nothing more.\n *\n * The API creates a subscription with `payment_behavior: \"default_incomplete\"`:\n * its first PaymentIntent waits on the browser (`requires_confirmation`, or 3D\n * Secure). Until it is confirmed nothing is paid and the subscription stays\n * `incomplete`, so closing the wizard on the API answer alone is wrong.\n */\nexport function clientSecretToConfirm(meta: Partial<StripeSubscriptionCreateMeta> | null | undefined): string | null {\n if (!meta?.requiresAction) return null;\n return meta.clientSecret || null;\n}\n\n/**\n * The subscription to re-read from Stripe after the browser confirmed its first\n * payment: the one the API just answered with (subscribe), else the one the wizard\n * was opened on (trial upgrade). Null when there is none to sync.\n *\n * Right after the confirmation our copy is still `incomplete` (the webhooks land a\n * second later), so the billing page would show no plan until a reload.\n */\nexport function subscriptionIdToSync(\n answered: { id?: string } | null | undefined,\n opened: { id?: string } | null | undefined,\n): string | null {\n return answered?.id || opened?.id || null;\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { Check } from \"lucide-react\";\nimport { WizardStep } from \"../../hooks/useSubscriptionWizard\";\n\ntype WizardProgressIndicatorProps = {\n currentStep: WizardStep;\n};\n\n// `label` is a message key, translated at render time.\nconst STEPS: { key: WizardStep; label: string }[] = [\n { key: \"plan-selection\", label: \"billing.ui.wizard.steps.select_plan\" },\n { key: \"review\", label: \"billing.ui.wizard.steps.review\" },\n { key: \"payment-method\", label: \"billing.ui.wizard.steps.payment\" },\n];\n\nfunction getStepIndex(step: WizardStep): number {\n return STEPS.findIndex((s) => s.key === step);\n}\n\nexport function WizardProgressIndicator({ currentStep }: WizardProgressIndicatorProps) {\n const t = useTranslations();\n const currentIndex = getStepIndex(currentStep);\n\n return (\n <div className=\"flex items-center justify-center gap-x-2 py-4\">\n {STEPS.map((step, index) => {\n const isCompleted = index < currentIndex;\n const isCurrent = index === currentIndex;\n\n return (\n <div key={step.key} className=\"flex items-center gap-x-2\">\n {/* Step Indicator */}\n <div\n className={`flex h-8 w-8 items-center justify-center rounded-full text-sm font-medium ${\n isCompleted\n ? \"bg-primary text-primary-foreground\"\n : isCurrent\n ? \"bg-primary text-primary-foreground\"\n : \"bg-muted text-muted-foreground\"\n }`}\n >\n {isCompleted ? <Check className=\"h-4 w-4\" /> : index + 1}\n </div>\n\n {/* Step Label */}\n <span className={`text-sm ${isCurrent ? \"font-medium text-foreground\" : \"text-muted-foreground\"}`}>\n {t(step.label)}\n </span>\n\n {/* Connector */}\n {index < STEPS.length - 1 && (\n <div className={`h-0.5 w-8 ${index < currentIndex ? \"bg-primary\" : \"bg-muted\"}`} />\n )}\n </div>\n );\n })}\n </div>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { useEffect, useMemo, useState } from \"react\";\nimport { Button } from \"../../../../../shadcnui\";\nimport { StripeProductInterface, StripeProductService } from \"../../../stripe-product\";\nimport { StripePriceInterface } from \"../../../stripe-price/data/stripe-price.interface\";\nimport { BillingInterval, IntervalToggle } from \"../widgets/IntervalToggle\";\nimport { ProductPricingList } from \"../widgets/ProductPricingList\";\n\ntype WizardStepPlanSelectionProps = {\n selectedPrice: StripePriceInterface | null;\n selectedInterval: BillingInterval;\n currentPriceId?: string;\n hideRecurringPrices: boolean;\n hideOneTimePrices: boolean;\n onSelectPrice: (price: StripePriceInterface) => void;\n onIntervalChange: (interval: BillingInterval) => void;\n onNext: () => void;\n isProcessing: boolean;\n};\n\nexport function WizardStepPlanSelection({\n selectedPrice,\n selectedInterval,\n currentPriceId,\n hideRecurringPrices,\n hideOneTimePrices,\n onSelectPrice,\n onIntervalChange,\n onNext,\n isProcessing,\n}: WizardStepPlanSelectionProps) {\n const t = useTranslations();\n const [products, setProducts] = useState<StripeProductInterface[]>([]);\n const [loading, setLoading] = useState(true);\n\n useEffect(() => {\n const loadProducts = async () => {\n try {\n const fetchedProducts = await StripeProductService.listProducts({ active: true });\n setProducts(fetchedProducts);\n } catch (error) {\n console.error(\"[WizardStepPlanSelection] Failed to load products:\", error);\n } finally {\n setLoading(false);\n }\n };\n\n loadProducts();\n }, []);\n\n // Compute available intervals from products\n const { hasMonthly, hasYearly } = useMemo(() => {\n let hasMonthly = false;\n let hasYearly = false;\n\n for (const product of products) {\n for (const price of product.stripePrices || []) {\n if (price.priceType === \"recurring\" && price.recurring?.interval === \"month\") {\n hasMonthly = true;\n }\n if (price.priceType === \"recurring\" && price.recurring?.interval === \"year\") {\n hasYearly = true;\n }\n }\n }\n\n return { hasMonthly, hasYearly };\n }, [products]);\n\n const handleSelectPrice = (price: StripePriceInterface) => {\n onSelectPrice(price);\n };\n\n return (\n <div className=\"space-y-6\">\n {/* Interval Toggle - only show for recurring products */}\n {!hideRecurringPrices && (\n <div className=\"flex justify-center\">\n <IntervalToggle\n value={selectedInterval}\n onChange={onIntervalChange}\n hasMonthly={hasMonthly}\n hasYearly={hasYearly}\n />\n </div>\n )}\n\n {/* Product Pricing List */}\n <ProductPricingList\n products={products}\n selectedInterval={selectedInterval}\n currentPriceId={currentPriceId}\n selectedPriceId={selectedPrice?.id}\n loading={loading}\n hideRecurringPrices={hideRecurringPrices}\n hideOneTimePrices={hideOneTimePrices}\n onSelectPrice={handleSelectPrice}\n />\n\n {/* Action Buttons */}\n <div className=\"flex justify-end pt-4 border-t\">\n <Button onClick={onNext} disabled={!selectedPrice || isProcessing}>\n {isProcessing ? t(\"common.loading\") : t(\"billing.ui.wizard.next_review\")}\n </Button>\n </div>\n </div>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { AlertCircle } from \"lucide-react\";\nimport { useEffect, useState } from \"react\";\nimport { Alert, AlertDescription, Badge, Button } from \"../../../../../shadcnui\";\nimport { SectionHeader } from \"../../../../../components/typography\";\nimport {\n applyDiscount,\n formatCurrency,\n promotionCodeDiscount,\n subscriptionDiscount,\n useBillingFormatOptions,\n} from \"../../../components/utils\";\nimport { StripePriceInterface } from \"../../../stripe-price/data/stripe-price.interface\";\nimport { ProrationPreviewInterface } from \"../../../stripe-invoice/data/stripe-invoice.interface\";\nimport { PromotionCodeValidationResult } from \"../../../stripe-promotion-code\";\nimport { PromoCodeInput } from \"../../../stripe-promotion-code/components/PromoCodeInput\";\nimport { StripeSubscriptionInterface } from \"../../data\";\nimport { StripeCustomerService } from \"../../../stripe-customer/data/stripe-customer.service\";\nimport { ReviewCard, reviewCard } from \"./review-card\";\n\ntype WizardStepReviewProps = {\n selectedPrice: StripePriceInterface | null;\n subscription?: StripeSubscriptionInterface;\n prorationPreview: ProrationPreviewInterface | null;\n hasPaymentMethod: boolean;\n error: string | null;\n isProcessing: boolean;\n onBack: () => void;\n onAddPaymentMethod: () => void;\n onConfirm: () => void;\n // Promotion code props\n promotionCode: PromotionCodeValidationResult | null;\n isValidatingPromoCode: boolean;\n promoCodeError: string | null;\n onApplyPromoCode: (code: string) => void;\n onRemovePromoCode: () => void;\n // Trial upgrade flag\n isTrialUpgrade: boolean;\n};\n\nexport function WizardStepReview({\n selectedPrice,\n subscription,\n prorationPreview,\n hasPaymentMethod,\n error,\n isProcessing,\n onBack,\n onAddPaymentMethod,\n onConfirm,\n promotionCode,\n isValidatingPromoCode,\n promoCodeError,\n onApplyPromoCode,\n onRemovePromoCode,\n isTrialUpgrade,\n}: WizardStepReviewProps) {\n const t = useTranslations();\n const fmt = useBillingFormatOptions();\n const [card, setCard] = useState<ReviewCard | null>(null);\n\n // Show which card is on file. The step remounts after the payment step, so a\n // card just added is read again here.\n useEffect(() => {\n if (!hasPaymentMethod) {\n setCard(null);\n return;\n }\n let cancelled = false;\n Promise.all([StripeCustomerService.listPaymentMethods(), StripeCustomerService.getCustomer()])\n .then(([methods, customer]) => {\n if (!cancelled) setCard(reviewCard(methods, customer?.defaultPaymentMethodId));\n })\n .catch((err) => {\n console.error(\"[WizardStepReview] Failed to load the card on file:\", err);\n if (!cancelled) setCard(null);\n });\n return () => {\n cancelled = true;\n };\n }, [hasPaymentMethod]);\n\n if (!selectedPrice) {\n return <div className=\"text-center py-8 text-muted-foreground\">{t(\"billing.ui.wizard.review.no_plan\")}</div>;\n }\n\n const isChangingPlan = subscription && subscription.price?.id !== selectedPrice.id;\n\n const formatInterval = (price: StripePriceInterface) => {\n if (price.priceType === \"one_time\") return t(\"billing.ui.wizard.review.interval_one_time\");\n const interval = price.recurring?.interval || \"month\";\n return interval === \"year\"\n ? t(\"billing.ui.wizard.review.interval_yearly\")\n : t(\"billing.ui.wizard.review.interval_monthly\");\n };\n\n // A code (typed or automatic) replaces the subscription's discount in Stripe.\n // Without one, a plan change keeps the discount the subscription already carries.\n const codeDiscount = promotionCodeDiscount(promotionCode);\n const carriedDiscount = isChangingPlan && !codeDiscount ? subscriptionDiscount(subscription) : null;\n const discount = codeDiscount ?? carriedDiscount;\n\n const discountedPrice = applyDiscount(selectedPrice.unitAmount, discount);\n\n // Calculate discounted immediate charge for proration preview\n const discountedImmediateCharge = applyDiscount(prorationPreview?.immediateCharge, discount);\n\n // Format the discount description\n const getDiscountDescription = (): string | null => {\n if (!discount) return null;\n\n if (discount.discountType === \"percent_off\") {\n return t(\"billing.ui.wizard.review.percent_off\", { value: discount.discountValue });\n }\n\n return t(\"billing.ui.wizard.review.amount_off\", {\n amount: formatCurrency(discount.discountValue, discount.currency || selectedPrice.currency, fmt),\n });\n };\n\n const discountDescription = getDiscountDescription();\n\n return (\n <div className=\"space-y-6\">\n {/* Selected Plan Summary */}\n <div className=\"bg-muted/50 rounded-lg p-4 space-y-3\">\n <SectionHeader>{t(\"billing.ui.wizard.review.selected_plan\")}</SectionHeader>\n <div className=\"flex justify-between items-center\">\n <div>\n <p className=\"font-medium\">{selectedPrice.product?.name}</p>\n {selectedPrice.nickname && <p className=\"text-sm text-muted-foreground\">{selectedPrice.nickname}</p>}\n </div>\n <div className=\"text-end\">\n {discountedPrice !== null ? (\n <>\n <p className=\"text-sm text-muted-foreground line-through\">\n {formatCurrency(selectedPrice.unitAmount || 0, selectedPrice.currency, fmt)}\n </p>\n <p className=\"text-success text-lg font-semibold\">\n {formatCurrency(discountedPrice, selectedPrice.currency, fmt)}\n </p>\n {discountDescription && <Badge variant=\"softGreen\">{discountDescription}</Badge>}\n {carriedDiscount && (\n <p className=\"text-sm text-muted-foreground\">{t(\"billing.ui.wizard.review.discount_carries_over\")}</p>\n )}\n </>\n ) : (\n <p className=\"font-semibold text-lg\">\n {formatCurrency(selectedPrice.unitAmount || 0, selectedPrice.currency, fmt)}\n </p>\n )}\n <p className=\"text-sm text-muted-foreground\">{formatInterval(selectedPrice)}</p>\n </div>\n </div>\n </div>\n\n {/* Proration Preview (for plan changes) */}\n {isChangingPlan && prorationPreview && (\n <div\n className={`${isTrialUpgrade ? \"bg-warning/15 border-warning/30\" : \"bg-primary/10 border-primary/30\"} space-y-2 rounded-lg border p-4`}\n >\n <h4 className={`font-medium ${isTrialUpgrade ? \"text-warning\" : \"text-primary\"}`}>\n {isTrialUpgrade\n ? t(\"billing.ui.wizard.review.trial_upgrade\")\n : t(\"billing.ui.wizard.review.proration_summary\")}\n </h4>\n <p className={`text-sm ${isTrialUpgrade ? \"text-warning\" : \"text-foreground\"}`}>\n {isTrialUpgrade\n ? discountedImmediateCharge !== null\n ? t(\"billing.ui.wizard.review.trial_upgrade_description_discounted\")\n : t(\"billing.ui.wizard.review.trial_upgrade_description\")\n : t(\"billing.ui.wizard.review.proration_description\")}\n </p>\n <div className=\"flex justify-between text-sm\">\n <span className={`${isTrialUpgrade ? \"text-warning\" : \"text-muted-foreground\"}`}>\n {isTrialUpgrade ? t(\"billing.ui.wizard.review.charge_now\") : t(\"billing.ui.wizard.review.due_now\")}\n </span>\n <span className={`font-medium ${isTrialUpgrade ? \"text-warning\" : \"text-foreground\"}`}>\n {discountedImmediateCharge !== null ? (\n <>\n <span className=\"line-through text-muted-foreground me-2\">\n {formatCurrency(prorationPreview.immediateCharge, prorationPreview.currency, fmt)}\n </span>\n <span className=\"text-success\">\n {formatCurrency(discountedImmediateCharge, prorationPreview.currency, fmt)}\n </span>\n </>\n ) : (\n formatCurrency(prorationPreview.immediateCharge, prorationPreview.currency, fmt)\n )}\n </span>\n </div>\n </div>\n )}\n\n {/* Promotion Code */}\n <div className=\"border rounded-lg p-4 space-y-3\">\n <h4 className=\"font-medium\">{t(\"billing.ui.wizard.review.promotion_code\")}</h4>\n <PromoCodeInput\n appliedCode={promotionCode}\n isValidating={isValidatingPromoCode}\n error={promoCodeError}\n onApply={onApplyPromoCode}\n onRemove={onRemovePromoCode}\n disabled={isProcessing}\n />\n </div>\n\n {/* Payment Method Status */}\n <div className=\"border rounded-lg p-4\">\n <div className=\"flex justify-between items-center\">\n <div>\n <h4 className=\"font-medium\">{t(\"billing.ui.payment_method.title\")}</h4>\n <p className=\"text-sm text-muted-foreground\">\n {hasPaymentMethod\n ? card\n ? t(\"billing.ui.wizard.review.card_on_file\", { brand: card.brand, last4: card.last4 })\n : t(\"billing.ui.wizard.review.payment_on_file\")\n : t(\"billing.ui.wizard.review.no_payment_on_file\")}\n </p>\n </div>\n {hasPaymentMethod ? (\n <Button variant=\"outline\" onClick={onAddPaymentMethod} disabled={isProcessing}>\n {t(\"billing.ui.wizard.review.use_another_card\")}\n </Button>\n ) : (\n <Button variant=\"outline\" onClick={onAddPaymentMethod}>\n {t(\"billing.ui.payment_methods.add\")}\n </Button>\n )}\n </div>\n </div>\n\n {/* Error Display */}\n {error && (\n <Alert variant=\"destructive\">\n <AlertCircle className=\"h-4 w-4\" />\n <AlertDescription>{error}</AlertDescription>\n </Alert>\n )}\n\n {/* Action Buttons */}\n <div className=\"flex justify-between pt-4 border-t\">\n <Button variant=\"outline\" onClick={onBack} disabled={isProcessing}>\n {t(\"billing.ui.wizard.back\")}\n </Button>\n <Button onClick={hasPaymentMethod ? onConfirm : onAddPaymentMethod} disabled={isProcessing}>\n {isProcessing\n ? t(\"billing.ui.wizard.review.processing\")\n : hasPaymentMethod\n ? isChangingPlan\n ? t(\"billing.ui.wizard.review.confirm_change\")\n : t(\"billing.ui.wizard.review.subscribe_now\")\n : t(\"billing.ui.payment_methods.add\")}\n </Button>\n </div>\n </div>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { useState } from \"react\";\nimport { Check, X, Loader2 } from \"lucide-react\";\nimport { Button, Input } from \"../../../../shadcnui\";\nimport { useI18nLocale } from \"../../../../i18n\";\nimport { formatFixed } from \"../../components/utils\";\nimport { PromotionCodeValidationResult } from \"../data/stripe-promotion-code.interface\";\n\ntype PromoCodeInputProps = {\n appliedCode: PromotionCodeValidationResult | null;\n isValidating: boolean;\n error: string | null;\n onApply: (code: string) => void;\n onRemove: () => void;\n disabled?: boolean;\n};\n\nexport function PromoCodeInput({\n appliedCode,\n isValidating,\n error,\n onApply,\n onRemove,\n disabled = false,\n}: PromoCodeInputProps) {\n const t = useTranslations();\n const locale = useI18nLocale();\n const [code, setCode] = useState(\"\");\n\n // Clear the typed text whenever a code becomes applied (typed or automatic):\n // the box then reads empty after the applied code is removed, instead of\n // still holding the removed code. Adjusting state during render on a prop\n // change (React's \"storing information from previous renders\" pattern) —\n // a failed validation keeps the text so the user can fix a typo.\n const appliedKey = appliedCode?.valid\n ? `${appliedCode.automatic ? \"auto\" : \"typed\"}:${appliedCode.code ?? \"\"}`\n : null;\n const [previousAppliedKey, setPreviousAppliedKey] = useState(appliedKey);\n if (appliedKey !== previousAppliedKey) {\n setPreviousAppliedKey(appliedKey);\n if (appliedKey !== null) setCode(\"\");\n }\n\n const handleRemove = () => {\n setCode(\"\");\n onRemove();\n };\n\n const handleApply = () => {\n if (code.trim()) {\n onApply(code.trim().toUpperCase());\n }\n };\n\n const handleKeyDown = (e: React.KeyboardEvent) => {\n if (e.key === \"Enter\") {\n e.preventDefault();\n handleApply();\n }\n };\n\n // Format discount for display\n const formatDiscount = (result: PromotionCodeValidationResult): string => {\n if (result.discountType === \"percent_off\") {\n return t(\"billing.ui.wizard.review.percent_off\", { value: String(result.discountValue) });\n }\n // amount_off is in cents, convert to dollars\n const amount = (result.discountValue || 0) / 100;\n const currency = result.currency?.toUpperCase() || \"USD\";\n return t(\"billing.ui.wizard.review.amount_off\", { amount: `${currency} ${formatFixed(amount, locale, 2)}` });\n };\n\n // Format duration for display\n const formatDuration = (result: PromotionCodeValidationResult): string => {\n switch (result.duration) {\n case \"forever\":\n return t(\"billing.ui.promo_code.duration_forever\");\n case \"once\":\n return t(\"billing.ui.promo_code.duration_once\");\n case \"repeating\":\n return t(\"billing.ui.promo_code.duration_repeating\", { months: String(result.durationInMonths) });\n default:\n return \"\";\n }\n };\n\n const codeInput = (\n <div className=\"space-y-2\">\n <div className=\"flex gap-2\">\n <Input\n placeholder={t(\"billing.ui.promo_code.placeholder\")}\n value={code}\n onChange={(e) => setCode(e.target.value.toUpperCase())}\n onKeyDown={handleKeyDown}\n disabled={disabled || isValidating}\n className=\"flex-1\"\n />\n <Button variant=\"outline\" onClick={handleApply} disabled={disabled || isValidating || !code.trim()}>\n {isValidating ? <Loader2 className=\"h-4 w-4 animate-spin\" /> : t(\"billing.ui.promo_code.apply\")}\n </Button>\n </div>\n {error && <p className=\"text-destructive text-xs/relaxed\">{error}</p>}\n </div>\n );\n\n // Show the automatic code as applied, with the option to type a different one\n if (appliedCode?.valid && appliedCode.automatic) {\n return (\n <div className=\"space-y-3\">\n <div className=\"space-y-2\">\n <div className=\"bg-success/15 border-success/30 flex items-center gap-2 rounded-lg border p-3\">\n <Check className=\"text-success h-4 w-4\" />\n <span className=\"text-success font-medium\">{t(\"billing.ui.promo_code.automatic_label\")}</span>\n <span className=\"text-success text-sm\">{formatDiscount(appliedCode)}</span>\n </div>\n <p className=\"text-success text-sm\">{formatDuration(appliedCode)}</p>\n </div>\n <div className=\"space-y-2\">\n <p className=\"text-muted-foreground text-xs/relaxed\">{t(\"billing.ui.promo_code.use_different\")}</p>\n {codeInput}\n </div>\n </div>\n );\n }\n\n // Show applied code state\n if (appliedCode?.valid) {\n return (\n <div className=\"space-y-2\">\n <div className=\"bg-success/15 border-success/30 flex items-center justify-between rounded-lg border p-3\">\n <div className=\"flex items-center gap-2\">\n <Check className=\"text-success h-4 w-4\" />\n <span className=\"text-success font-medium\">{appliedCode.code}</span>\n <span className=\"text-success text-sm\">{formatDiscount(appliedCode)}</span>\n </div>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={handleRemove}\n disabled={disabled}\n className=\"text-success hover:bg-success/25 hover:text-success\"\n >\n <X className=\"h-4 w-4\" />\n </Button>\n </div>\n <p className=\"text-success text-sm\">{formatDuration(appliedCode)}</p>\n </div>\n );\n }\n\n return codeInput;\n}\n","import { PaymentMethodInterface } from \"../../../stripe-customer/data/payment-method.interface\";\n\nexport type ReviewCard = { brand: string; last4: string };\n\n/**\n * The card the review shows as the one on file: the customer's default, or the\n * first card listed when no default is set (or the default is no longer listed).\n * Null when no card with a brand and last 4 digits is known.\n */\nexport function reviewCard(\n methods: PaymentMethodInterface[] | null | undefined,\n defaultPaymentMethodId: string | null | undefined,\n): ReviewCard | null {\n if (!methods || methods.length === 0) return null;\n\n const method = (defaultPaymentMethodId && methods.find((m) => m.id === defaultPaymentMethodId)) || methods[0];\n if (!method.card?.last4) return null;\n\n const brand = method.card.brand\n ? method.card.brand.charAt(0).toUpperCase() + method.card.brand.slice(1)\n : method.type.charAt(0).toUpperCase() + method.type.slice(1);\n\n return { brand, last4: method.card.last4 };\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { PaymentMethodForm } from \"../../../stripe-customer/components/forms/PaymentMethodForm\";\nimport { SectionHeader } from \"../../../../../components/typography\";\n\ntype WizardStepPaymentMethodProps = {\n onBack: () => void;\n onSuccess: () => void;\n isProcessing: boolean;\n};\n\nexport function WizardStepPaymentMethod({ onBack, onSuccess, isProcessing }: WizardStepPaymentMethodProps) {\n const t = useTranslations();\n return (\n <div className=\"space-y-6\">\n <div className=\"text-center\">\n <SectionHeader>{t(\"billing.ui.payment_methods.add\")}</SectionHeader>\n <p className=\"text-sm text-muted-foreground\">{t(\"billing.ui.wizard.payment_description\")}</p>\n </div>\n\n <PaymentMethodForm onSuccess={onSuccess} onCancel={onBack} isLoading={isProcessing} alwaysSetDefault />\n </div>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { Activity } from \"lucide-react\";\nimport { useEffect, useState } from \"react\";\nimport { StripeSubscriptionInterface, StripeSubscriptionService } from \"../../../stripe-subscription\";\nimport { MeterInterface, MeterSummaryInterface } from \"../../data/stripe-usage.interface\";\nimport { StripeUsageService } from \"../../data/stripe-usage.service\";\nimport { UsageSummaryCards } from \"../widgets/UsageSummaryCards\";\n\nexport function UsageContainer() {\n const t = useTranslations();\n const [meters, setMeters] = useState<MeterInterface[]>([]);\n const [summaries, setSummaries] = useState<Record<string, MeterSummaryInterface | null>>({});\n const [loading, setLoading] = useState<boolean>(true);\n const [subscriptions, setSubscriptions] = useState<StripeSubscriptionInterface[]>([]);\n\n useEffect(() => {\n loadUsageData();\n }, []);\n\n const loadUsageData = async () => {\n setLoading(true);\n\n try {\n // First, check if there are any metered subscriptions\n const fetchedSubscriptions = await StripeSubscriptionService.listSubscriptions();\n setSubscriptions(fetchedSubscriptions);\n\n const hasMeteredSubscriptions = fetchedSubscriptions.some((sub) => sub.price?.recurring?.usageType === \"metered\");\n\n if (!hasMeteredSubscriptions) {\n setLoading(false);\n return;\n }\n\n // Load meters\n const fetchedMeters = await StripeUsageService.listMeters();\n setMeters(fetchedMeters);\n\n // Load summaries for each meter (current month)\n const summariesMap: Record<string, MeterSummaryInterface | null> = {};\n const now = new Date();\n const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);\n const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59, 999);\n\n for (const meter of fetchedMeters) {\n try {\n const meterSummaries = await StripeUsageService.getMeterSummaries({\n meterId: meter.id,\n startTime: startOfMonth,\n endTime: endOfMonth,\n });\n // Use the first (most recent) summary\n summariesMap[meter.id] = meterSummaries.length > 0 ? meterSummaries[0] : null;\n } catch (error) {\n console.error(`[UsageContainer] Failed to load summaries for meter ${meter.id}:`, error);\n summariesMap[meter.id] = null;\n }\n }\n\n setSummaries(summariesMap);\n } catch (error) {\n console.error(\"[UsageContainer] Failed to load usage data:\", error);\n } finally {\n setLoading(false);\n }\n };\n\n // Check if there are any metered subscriptions\n const hasMeteredSubscriptions = subscriptions.some((sub) => sub.price?.recurring?.usageType === \"metered\");\n\n // Don't render if no metered subscriptions\n if (!loading && !hasMeteredSubscriptions) {\n return null;\n }\n\n if (loading) {\n return (\n <div className=\"flex h-64 items-center justify-center\">\n <p className=\"text-muted-foreground\">{t(\"billing.ui.usage.loading\")}</p>\n </div>\n );\n }\n\n return (\n <div className=\"flex w-full flex-col gap-y-6\">\n {/* Header */}\n <div className=\"flex items-center gap-x-3\">\n <Activity className=\"h-8 w-8\" />\n <h1 className=\"text-primary text-3xl font-semibold\">{t(\"billing.ui.usage.title\")}</h1>\n </div>\n\n {/* Empty State */}\n {meters.length === 0 && (\n <div className=\"bg-muted/50 flex flex-col items-center justify-center gap-y-4 rounded-lg border-2 border-dashed border-gray-300 p-12\">\n <Activity className=\"text-muted-foreground h-16 w-16\" />\n <div className=\"text-center\">\n <h3 className=\"mb-2 text-sm font-medium\">{t(\"billing.ui.usage.empty_title\")}</h3>\n <p className=\"text-muted-foreground\">{t(\"billing.ui.usage.empty_description\")}</p>\n </div>\n </div>\n )}\n\n {/* Usage Summary Cards */}\n {meters.length > 0 && <UsageSummaryCards meters={meters} summaries={summaries} />}\n </div>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { Activity } from \"lucide-react\";\nimport { Card, CardContent, CardHeader } from \"../../../../../shadcnui\";\nimport { dateFormatLocale } from \"../../../../../utils\";\nimport { type BillingFormatOptions, formatFixed, useBillingFormatOptions } from \"../../../components/utils\";\nimport { MeterInterface, MeterSummaryInterface } from \"../../data/stripe-usage.interface\";\n\ntype UsageSummaryCardProps = {\n meter: MeterInterface;\n summary: MeterSummaryInterface | null;\n};\n\n/**\n * Get progress bar color based on usage percentage\n */\nfunction getProgressColor(percentage: number | null): string {\n if (percentage === null) return \"bg-blue-500\";\n if (percentage >= 90) return \"bg-red-500\";\n if (percentage >= 75) return \"bg-orange-500\";\n return \"bg-green-500\";\n}\n\n/**\n * Format a Unix timestamp to readable date\n */\nfunction formatDate(date: Date | undefined, { locale, t }: Required<BillingFormatOptions>): string {\n if (!date) return t(\"billing.ui.not_available\");\n\n try {\n return new Intl.DateTimeFormat(dateFormatLocale(locale), {\n month: \"short\",\n day: \"numeric\",\n }).format(new Date(date));\n } catch (_error) {\n return t(\"billing.ui.invalid_date\");\n }\n}\n\nexport function UsageSummaryCard({ meter, summary }: UsageSummaryCardProps) {\n const t = useTranslations();\n const fmt = useBillingFormatOptions();\n const currentUsage = summary?.aggregatedValue ?? 0;\n const limit = (meter as any).limit; // Meters may have optional limit field\n const percentage = limit && limit > 0 ? (currentUsage / limit) * 100 : null;\n const progressColor = getProgressColor(percentage);\n const progressWidth = percentage !== null ? Math.min(percentage, 100) : 0;\n\n const displayName = meter.displayName || meter.eventName;\n const hasLimit = limit !== null && limit !== undefined;\n\n return (\n <Card>\n <CardHeader className=\"flex flex-row items-center gap-x-3 pb-3\">\n <div className=\"flex h-10 w-10 items-center justify-center rounded-lg bg-blue-100 text-blue-600\">\n <Activity className=\"h-5 w-5\" />\n </div>\n <div className=\"flex flex-col\">\n <h3 className=\"text-sm font-medium\">{displayName}</h3>\n <p className=\"text-muted-foreground text-xs\">{meter.id}</p>\n </div>\n </CardHeader>\n\n <CardContent className=\"flex flex-col gap-y-4\">\n {/* Current Usage */}\n <div>\n <p className=\"text-3xl font-bold\">{currentUsage.toLocaleString(fmt.locale)}</p>\n {hasLimit && (\n <p className=\"text-muted-foreground text-sm\">\n {t(\"billing.ui.usage.of_limit_used\", { limit: limit.toLocaleString(fmt.locale) })}\n </p>\n )}\n </div>\n\n {/* Progress Bar */}\n {hasLimit ? (\n <div className=\"flex flex-col gap-y-2\">\n <div className=\"h-2 w-full overflow-hidden rounded-full bg-gray-200\">\n <div className={`h-full transition-all ${progressColor}`} style={{ width: `${progressWidth}%` }} />\n </div>\n <p className=\"text-muted-foreground text-sm\">\n {t(\"billing.ui.usage.percent_used\", {\n value: percentage !== null ? formatFixed(percentage, fmt.locale, 1) : \"\",\n })}\n </p>\n </div>\n ) : (\n <p className=\"text-muted-foreground text-sm\">{t(\"billing.ui.usage.no_limit\")}</p>\n )}\n\n {/* Period Information */}\n {summary && summary.start && summary.end && (\n <div className=\"border-t pt-3\">\n <p className=\"text-muted-foreground text-xs\">\n {t(\"billing.ui.usage.period\", {\n start: formatDate(summary.start, fmt),\n end: formatDate(summary.end, fmt),\n })}\n </p>\n </div>\n )}\n </CardContent>\n </Card>\n );\n}\n","\"use client\";\n\nimport { MeterInterface, MeterSummaryInterface } from \"../../data/stripe-usage.interface\";\nimport { UsageSummaryCard } from \"../details/UsageSummaryCard\";\n\ntype UsageSummaryCardsProps = {\n meters: MeterInterface[];\n summaries: Record<string, MeterSummaryInterface | null>;\n};\n\nexport function UsageSummaryCards({ meters, summaries }: UsageSummaryCardsProps) {\n return (\n <div className=\"grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3\">\n {meters.map((meter) => (\n <UsageSummaryCard key={meter.id} meter={meter} summary={summaries[meter.id] || null} />\n ))}\n </div>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from \"../../../../../shadcnui\";\nimport { SectionHeader } from \"../../../../../components/typography\";\nimport { dateFormatLocale } from \"../../../../../utils\";\nimport { type BillingFormatOptions, useBillingFormatOptions } from \"../../../components/utils\";\nimport { StripeUsageInterface } from \"../../data/stripe-usage.interface\";\n\ntype UsageHistoryTableProps = {\n usageRecords: StripeUsageInterface[];\n};\n\n/**\n * Format a date with time for usage history\n */\nfunction formatDateTime(date: Date | string | undefined, { locale, t }: Required<BillingFormatOptions>): string {\n if (!date) return t(\"billing.ui.not_available\");\n\n const dateObj = typeof date === \"string\" ? new Date(date) : date;\n\n try {\n return new Intl.DateTimeFormat(dateFormatLocale(locale), {\n month: \"short\",\n day: \"numeric\",\n year: \"numeric\",\n hour: \"numeric\",\n minute: \"2-digit\",\n }).format(dateObj);\n } catch (_error) {\n return t(\"billing.ui.invalid_date\");\n }\n}\n\nexport function UsageHistoryTable({ usageRecords }: UsageHistoryTableProps) {\n const t = useTranslations();\n const fmt = useBillingFormatOptions();\n if (usageRecords.length === 0) {\n return (\n <div className=\"rounded-lg border p-8 text-center\">\n <p className=\"text-muted-foreground\">{t(\"billing.ui.usage.history_empty\")}</p>\n </div>\n );\n }\n\n return (\n <div className=\"flex w-full flex-col gap-y-4\">\n <SectionHeader level={2}>{t(\"billing.ui.usage.history_title\")}</SectionHeader>\n <div className=\"overflow-clip rounded-lg border\">\n <Table>\n <TableHeader className=\"bg-muted\">\n <TableRow>\n <TableHead>{t(\"billing.ui.columns.date_time\")}</TableHead>\n <TableHead>{t(\"billing.ui.columns.meter_event\")}</TableHead>\n <TableHead className=\"text-end\">{t(\"billing.ui.columns.quantity\")}</TableHead>\n <TableHead>{t(\"billing.ui.columns.event_id\")}</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {usageRecords.map((record) => {\n const dateTime = formatDateTime(record.timestamp, fmt);\n const quantity = record.quantity.toLocaleString(fmt.locale);\n\n return (\n <TableRow key={record.id}>\n <TableCell className=\"font-medium\">{dateTime}</TableCell>\n <TableCell className=\"text-muted-foreground\">{record.meterEventName}</TableCell>\n <TableCell className=\"text-end font-medium\">{quantity}</TableCell>\n <TableCell className=\"text-muted-foreground text-sm tabular-nums\">{record.stripeEventId}</TableCell>\n </TableRow>\n );\n })}\n </TableBody>\n </Table>\n </div>\n </div>\n );\n}\n","\"use client\";\n\nimport { ReactNode } from \"react\";\nimport { Dialog, DialogContent, DialogHeader, DialogTitle } from \"../../../../shadcnui\";\n\ntype BillingDetailModalProps = {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n title: string;\n children: ReactNode;\n className?: string;\n};\n\nexport function BillingDetailModal({ open, onOpenChange, title, children, className }: BillingDetailModalProps) {\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent className={className ?? \"max-w-4xl max-h-[90vh] overflow-y-auto\"}>\n <DialogHeader>\n <DialogTitle>{title}</DialogTitle>\n </DialogHeader>\n {children}\n </DialogContent>\n </Dialog>\n );\n}\n","/**\n * The URL to write back after consuming a one-shot query parameter (a deep-link\n * `?action=`). Drops only `param`; every other parameter and the hash stay, so a\n * host page that keys its state on the URL (the settings hub's `?section=`) keeps\n * showing the same place.\n */\nexport function urlWithoutParam(params: { pathname: string; search: string; hash?: string; param: string }): string {\n const query = new URLSearchParams(params.search);\n query.delete(params.param);\n const remaining = query.toString();\n return `${params.pathname}${remaining ? `?${remaining}` : \"\"}${params.hash ?? \"\"}`;\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { AlertCircle } from \"lucide-react\";\nimport { Button } from \"../../../../shadcnui\";\nimport { StripeSubscriptionInterface, SubscriptionStatus } from \"../../stripe-subscription\";\n\ntype BillingAlertBannerProps = {\n subscription: StripeSubscriptionInterface;\n onUpdatePayment?: () => void;\n onAddPayment?: () => void;\n};\n\nexport function BillingAlertBanner({ subscription, onUpdatePayment, onAddPayment }: BillingAlertBannerProps) {\n const t = useTranslations();\n // Payment failed alert (past_due)\n if (subscription.status === SubscriptionStatus.PAST_DUE) {\n return (\n <div className=\"bg-red-50 border border-red-200 rounded-lg p-4 flex items-start gap-x-3\">\n <AlertCircle className=\"text-destructive mt-0.5 h-5 w-5\" />\n <div className=\"flex-1\">\n <h3 className=\"text-destructive text-sm font-medium\">{t(\"billing.ui.alert.payment_failed_title\")}</h3>\n <p className=\"text-destructive mt-1 text-sm\">{t(\"billing.ui.alert.payment_failed_description\")}</p>\n </div>\n {onUpdatePayment && (\n <Button variant=\"outline\" size=\"sm\" onClick={onUpdatePayment} className=\"text-destructive border-red-300\">\n {t(\"billing.ui.alert.update_payment_method\")}\n </Button>\n )}\n </div>\n );\n }\n\n // Trial ending soon alert (trialing with ≤7 days remaining)\n if (subscription.status === SubscriptionStatus.TRIALING && subscription.trialEnd) {\n const trialEnd = new Date(subscription.trialEnd);\n const now = new Date();\n const daysRemaining = Math.ceil((trialEnd.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));\n\n if (daysRemaining <= 7) {\n return (\n <div className=\"bg-yellow-50 border border-yellow-200 rounded-lg p-4 flex items-start gap-x-3\">\n <AlertCircle className=\"text-warning mt-0.5 h-5 w-5\" />\n <div className=\"flex-1\">\n <h3 className=\"text-warning text-sm font-medium\">{t(\"billing.ui.alert.trial_ending_title\")}</h3>\n <p className=\"text-warning mt-1 text-sm\">\n {t(\"billing.ui.alert.trial_ending_description\", { count: daysRemaining })}\n </p>\n </div>\n {onAddPayment && (\n <Button variant=\"outline\" size=\"sm\" onClick={onAddPayment} className=\"text-warning border-yellow-300\">\n {t(\"billing.ui.payment_methods.add\")}\n </Button>\n )}\n </div>\n );\n }\n }\n\n // No critical state\n return null;\n}\n","\"use client\";\n\nimport { Elements } from \"@stripe/react-stripe-js\";\nimport { loadStripe, Stripe } from \"@stripe/stripe-js\";\nimport { ReactNode, useMemo } from \"react\";\nimport { getStripePublishableKey } from \"../../../../client/config\";\n\n// Cache the stripe promise to avoid recreating on each render\nlet stripePromiseCache: { key: string; promise: Promise<Stripe | null> } | null = null;\n\nfunction getStripePromise(publishableKey: string | undefined): Promise<Stripe | null> {\n if (!publishableKey) {\n return Promise.resolve(null);\n }\n\n // Return cached promise if key matches\n if (stripePromiseCache?.key === publishableKey) {\n return stripePromiseCache.promise;\n }\n\n // Create and cache new promise\n const promise = loadStripe(publishableKey);\n stripePromiseCache = { key: publishableKey, promise };\n return promise;\n}\n\nexport function StripeProvider({ children }: { children: ReactNode }) {\n // Evaluate key at render time, not module load time\n const publishableKey = getStripePublishableKey();\n const stripePromise = useMemo(() => getStripePromise(publishableKey), [publishableKey]);\n const options = useMemo(() => ({}), []);\n\n // If no Stripe key is configured, just render children without Stripe context\n if (!publishableKey) {\n return <>{children}</>;\n }\n\n return (\n <Elements stripe={stripePromise} options={options}>\n {children}\n </Elements>\n );\n}\n\n// Helper function to check if Stripe is configured\nexport function isStripeConfigured(): boolean {\n return !!getStripePublishableKey();\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { createContext, ReactNode, useCallback, useContext, useMemo, useState } from \"react\";\nimport { SharedProvider } from \"../../../contexts/SharedContext\";\nimport { JsonApiHydratedDataInterface, Modules, rehydrate } from \"../../../core\";\nimport { usePageUrlGenerator } from \"../../../hooks\";\nimport { BreadcrumbItemData } from \"../../../interfaces\";\nimport { getRoleId } from \"../../../roles\";\nimport { useCurrentUserContext } from \"../../user/contexts\";\nimport {\n type BillingFormatOptions,\n formatCurrency,\n formatInterval,\n useBillingFormatOptions,\n} from \"../components/utils\";\nimport { PriceArchiver } from \"../stripe-price/components/forms/PriceArchiver\";\nimport PriceEditor from \"../stripe-price/components/forms/PriceEditor\";\nimport { StripePriceInput, StripePriceInterface } from \"../stripe-price/data/stripe-price.interface\";\nimport { StripePriceService } from \"../stripe-price/data/stripe-price.service\";\n\nexport interface PriceContextType {\n price: StripePriceInterface | undefined;\n setPrice: (value: StripePriceInterface | undefined) => void;\n reloadPrice: () => Promise<void>;\n createPrice: (input: StripePriceInput) => Promise<StripePriceInterface>;\n updatePrice: (input: StripePriceInput) => Promise<StripePriceInterface>;\n archivePrice: (id?: string) => Promise<void>;\n restorePrice: (id?: string) => Promise<void>;\n /** Bumped by every mutation. Mounted lists re-fetch on it. */\n priceVersion: number;\n refreshPrices: () => void;\n /** Product a newly created price attaches to. Set in list mode (prices tab). */\n productId: string | undefined;\n}\n\nconst PriceContext = createContext<PriceContextType | undefined>(undefined);\n\nexport type PriceProviderProps = {\n children: ReactNode;\n dehydratedPrice?: JsonApiHydratedDataInterface;\n /**\n * Publish breadcrumbs + title into SharedProvider. Pass `false` when this\n * provider is mounted INSIDE another entity's page (the prices tab mounts\n * PriceProvider that way): SharedProvider replaces its value for the subtree\n * rather than merging (SharedContext.tsx:35-37), so a nested provider that\n * publishes chrome would blank the host page's title.\n */\n publishChrome?: boolean;\n /** Required in list mode (the prices tab) so new prices attach to the right product. */\n productId?: string;\n};\n\nconst defaultContextValue: PriceContextType = {\n price: undefined,\n setPrice: () => {},\n reloadPrice: async () => {},\n createPrice: async () => {\n throw new Error(\"createPrice requires a PriceProvider\");\n },\n updatePrice: async () => {\n throw new Error(\"updatePrice requires a PriceProvider\");\n },\n archivePrice: async () => {},\n restorePrice: async () => {},\n priceVersion: 0,\n refreshPrices: () => {},\n productId: undefined,\n};\n\n/**\n * Human label for a price: its nickname, else amount + interval. Pass the\n * caller's `useBillingFormatOptions()` so the amount and interval follow the\n * active locale; without it the label is formatted in English.\n */\nexport function priceLabel(price: StripePriceInterface, fmt: BillingFormatOptions = {}): string {\n return (\n price.nickname ?? `${formatCurrency(price.unitAmount, price.currency, fmt)} ${formatInterval(price, fmt)}`.trim()\n );\n}\n\nexport const PriceProvider = ({\n children,\n dehydratedPrice,\n publishChrome = true,\n productId: productIdProp,\n}: PriceProviderProps) => {\n const t = useTranslations();\n const fmt = useBillingFormatOptions();\n const generateUrl = usePageUrlGenerator();\n const { hasRole } = useCurrentUserContext();\n\n const [price, setPrice] = useState<StripePriceInterface | undefined>(\n dehydratedPrice ? rehydrate<StripePriceInterface>(Modules.StripePrice, dehydratedPrice) : undefined,\n );\n const [priceVersion, setPriceVersion] = useState<number>(0);\n\n const refreshPrices = useCallback(() => setPriceVersion((value) => value + 1), []);\n\n // NOT `price.productId`: that getter throws (\"productId is not defined\") when\n // the attribute is absent, and it always is — the API's mapStripePrice sets\n // `stripeProduct: undefined`, and the serialiser derives the productId\n // attribute from exactly that field, so a price never carries one over the\n // wire. Reading it here crashed the whole price page on mount. Fall back to\n // the hydrated relationship, which is a safe optional getter.\n const productId = productIdProp ?? price?.product?.id;\n\n const reloadPrice = useCallback(async () => {\n if (!price?.id) return;\n setPrice(await StripePriceService.getPrice({ id: price.id }));\n }, [price?.id]);\n\n const createPrice = useCallback(\n async (input: StripePriceInput) => {\n const saved = await StripePriceService.createPrice(input);\n refreshPrices();\n return saved;\n },\n [refreshPrices],\n );\n\n const updatePrice = useCallback(\n async (input: StripePriceInput) => {\n const saved = await StripePriceService.updatePrice(input);\n setPrice(saved);\n refreshPrices();\n return saved;\n },\n [refreshPrices],\n );\n\n // The archive/reactivate routes answer 204 NO_CONTENT with an empty body\n // (stripe-price.controller.ts:129-155) even though the service signature\n // claims it returns void — so the response is discarded and the entity is\n // re-read instead.\n const archivePrice = useCallback(\n async (id?: string) => {\n const target = id ?? price?.id;\n if (!target) return;\n await StripePriceService.archivePrice({ id: target });\n await reloadPrice();\n refreshPrices();\n },\n [price?.id, reloadPrice, refreshPrices],\n );\n\n const restorePrice = useCallback(\n async (id?: string) => {\n const target = id ?? price?.id;\n if (!target) return;\n await StripePriceService.reactivatePrice({ id: target });\n await reloadPrice();\n refreshPrices();\n },\n [price?.id, reloadPrice, refreshPrices],\n );\n\n const value = useMemo<PriceContextType>(\n () => ({\n price,\n setPrice,\n reloadPrice,\n createPrice,\n updatePrice,\n archivePrice,\n restorePrice,\n priceVersion,\n refreshPrices,\n productId,\n }),\n [price, reloadPrice, createPrice, updatePrice, archivePrice, restorePrice, priceVersion, refreshPrices, productId],\n );\n\n if (!publishChrome) return <PriceContext.Provider value={value}>{children}</PriceContext.Provider>;\n\n const breadcrumb = (): BreadcrumbItemData[] => {\n const items: BreadcrumbItemData[] = [\n { name: t(\"billing.admin.products.title\"), href: generateUrl({ page: Modules.StripeProduct }) },\n ];\n if (price?.product)\n items.push({\n name: price.product.name,\n href: generateUrl({ page: Modules.StripeProduct, id: price.product.id }),\n });\n if (price) items.push({ name: priceLabel(price, fmt) });\n return items;\n };\n\n const title = () => {\n const response: any = { type: t(\"billing.admin.prices.title\") };\n if (!price) return response;\n\n response.element = priceLabel(price, fmt);\n\n // narr8 note: gate on hasRole ONLY. hasPermissionToModule always returns\n // false in this app, so gating an editor on it hides the editor outright.\n if (hasRole(getRoleId().Administrator)) {\n response.functions = [\n <PriceArchiver key=\"priceArchiver\" price={price} />,\n <PriceEditor key=\"priceEditor\" price={price} propagateChanges={setPrice} />,\n ];\n }\n\n return response;\n };\n\n return (\n <SharedProvider value={{ breadcrumbs: breadcrumb(), title: title() }}>\n <PriceContext.Provider value={value}>{children}</PriceContext.Provider>\n </SharedProvider>\n );\n};\n\nexport const usePriceContext = (): PriceContextType => useContext(PriceContext) ?? defaultContextValue;\n","\"use client\";\n\nimport { ArchiveIcon, RotateCcwIcon } from \"lucide-react\";\nimport { useTranslations } from \"next-intl\";\nimport { useState } from \"react\";\nimport { errorToast } from \"../../../../../components/errors\";\nimport { Button, ConfirmDialog } from \"../../../../../shadcnui\";\nimport { useBillingFormatOptions } from \"../../../components/utils\";\nimport { priceLabel, usePriceContext } from \"../../../contexts/PriceContext\";\nimport { StripePriceInterface } from \"../../data/stripe-price.interface\";\n\ntype PriceArchiverProps = {\n price: StripePriceInterface;\n};\n\n/**\n * Archive / restore control for a price. Built on ConfirmDialog, which is\n * controlled and trigger-less by design — the caller owns the trigger button,\n * so none of Base UI's trigger-composition pitfalls apply (no asChild, no\n * nested <button>).\n */\nexport function PriceArchiver({ price }: PriceArchiverProps) {\n const t = useTranslations();\n const fmt = useBillingFormatOptions();\n const { archivePrice, restorePrice } = usePriceContext();\n const [open, setOpen] = useState<boolean>(false);\n\n const isArchived = !price.active;\n const scope = isArchived ? \"restore\" : \"archive\";\n\n return (\n <>\n <Button variant=\"outline\" size=\"sm\" onClick={() => setOpen(true)}>\n {isArchived ? <RotateCcwIcon /> : <ArchiveIcon />}\n {t(`billing.admin.prices.${scope}.confirm`)}\n </Button>\n <ConfirmDialog\n open={open}\n onOpenChange={setOpen}\n title={t(`billing.admin.prices.${scope}.title`)}\n description={t(`billing.admin.prices.${scope}.description`, { label: priceLabel(price, fmt) })}\n confirmLabel={t(`billing.admin.prices.${scope}.confirm`)}\n cancelLabel={t(\"ui.buttons.cancel\")}\n destructive={!isArchived}\n onConfirm={async () => {\n try {\n if (isArchived) await restorePrice(price.id);\n else await archivePrice(price.id);\n } catch (error) {\n errorToast({ title: t(`billing.admin.prices.${scope}.title`), error });\n throw error; // keeps the dialog open — ConfirmDialog swallows rejections\n }\n }}\n />\n </>\n );\n}\n","\"use client\";\n\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { PlusIcon, XIcon } from \"lucide-react\";\nimport { useTranslations } from \"next-intl\";\nimport { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { v4 } from \"uuid\";\nimport { z } from \"zod\";\nimport { EditorSheet, FieldLabel, FormCheckbox, FormInput, FormSelect, FormTextarea } from \"../../../../../components\";\nimport { Modules } from \"../../../../../core\";\nimport { useI18nRouter } from \"../../../../../i18n\";\nimport { Alert, AlertDescription, AlertTitle, Button, Checkbox, Input } from \"../../../../../shadcnui\";\nimport { showToast } from \"../../../../../utils/toast\";\nimport { FeatureInterface, FeatureService } from \"../../../../feature\";\nimport { useBillingFormatOptions } from \"../../../components/utils\";\nimport { priceLabel, usePriceContext } from \"../../../contexts/PriceContext\";\nimport { StripePriceInput, StripePriceInterface } from \"../../data/stripe-price.interface\";\nimport { StripePriceSeed } from \"../../data/stripe-price.transfer\";\n\nexport type PriceEditorProps = {\n productId?: string;\n price?: StripePriceInterface;\n /**\n * Pre-fills a NEW price — the Clone (a row in the prices list) and Import (a\n * JSON document) paths both hand over the same seed.\n *\n * The seed deliberately carries NO identity: no Stripe price id, no internal\n * id and no product id. A clone/import is a brand-new price, minted fresh by\n * Stripe and attached to whatever product this editor is mounted for\n * (`productId` / the context), so copying any identity across would either\n * collide or silently re-point the new price at the source environment's\n * objects. Seeding therefore never makes this an edit — see `isEdit` below.\n */\n seed?: StripePriceSeed;\n propagateChanges?: (price: StripePriceInterface) => void;\n onSuccess?: () => void | Promise<void>;\n trigger?: ReactNode;\n forceShow?: boolean;\n onClose?: () => void;\n dialogOpen?: boolean;\n onDialogOpenChange?: (open: boolean) => void;\n};\n\nfunction PriceEditorInternal({\n productId,\n price,\n seed,\n propagateChanges,\n onSuccess,\n trigger,\n forceShow,\n onClose,\n dialogOpen,\n onDialogOpenChange,\n}: PriceEditorProps) {\n const t = useTranslations();\n const fmt = useBillingFormatOptions();\n const router = useI18nRouter();\n const { createPrice, updatePrice, productId: contextProductId } = usePriceContext();\n const [allFeatures, setAllFeatures] = useState<FeatureInterface[]>([]);\n // Stays `!!price`, never `!!price || !!seed`: a seeded editor is a CREATE, so\n // the create branch of onSubmit (fresh v4(), targetProductId, no id reuse)\n // must run for it exactly as it does for an empty form.\n const isEdit = !!price;\n // NOT `price.productId` — that getter throws when the attribute is absent, and\n // it always is (see the note in PriceContext). Only create mode needs this,\n // and there it arrives as a prop or from the context.\n const targetProductId = productId ?? price?.product?.id ?? contextProductId;\n\n useEffect(() => {\n const fetchFeatures = async () => {\n setAllFeatures(await FeatureService.findMany({}));\n };\n void fetchFeatures();\n }, []);\n\n const formSchema = useMemo(\n () =>\n z.object({\n unitAmount: z.preprocess(\n (value) => (typeof value === \"string\" ? parseFloat(value) : value),\n z.number().min(0, { message: t(\"billing.admin.prices.errors.amount\") }),\n ),\n currency: z.string().min(1, { message: t(\"billing.admin.prices.errors.currency\") }),\n interval: z.enum([\"one_time\", \"day\", \"week\", \"month\", \"year\"]),\n intervalCount: z.preprocess(\n (value) =>\n value === \"\" || value === undefined ? undefined : typeof value === \"string\" ? parseInt(value, 10) : value,\n z.number().min(1).optional(),\n ),\n usageType: z.enum([\"licensed\", \"metered\"]).optional(),\n nickname: z.string().optional(),\n isTrial: z.boolean(),\n description: z.string().optional(),\n features: z.array(z.string()),\n // Normalised, not a bare `z.string()`: the field renders as\n // `<FormInput type=\"number\">`, which hands back a number, so a plain\n // string schema rejects every value with \"expected string, received\n // number\".\n //\n // Normalised by hand rather than with `z.coerce.string()`, because\n // zod 4's coercion is `String(input)` — a cleared number input yields\n // NaN, which would become the TRUTHY string \"NaN\" and reach\n // `parseInt` as a NaN token.\n //\n // Deliberately NOT converted to the `z.preprocess(... z.number())`\n // idiom used by unitAmount/intervalCount above. The whole token\n // pipeline is string-based — seeded with `.toString()` and read back\n // with `values.token ? parseInt(...)` on both submit branches — and a\n // number pipeline would make `0` FALSY, silently dropping the token\n // from the payload. A price with no token is \"tokens not configured\"\n // (AI stays enabled), so that would make a zero-token / no-AI plan\n // impossible to create from this form.\n token: z.preprocess(\n (value) =>\n value === undefined || value === null || value === \"\" || (typeof value === \"number\" && Number.isNaN(value))\n ? \"\"\n : String(value),\n z.string(),\n ),\n featureIds: z.array(z.string()),\n }),\n [t],\n );\n\n type PriceFormValues = z.infer<typeof formSchema>;\n\n // Seeds carry platform features as NAMES, never ids: Feature ids differ per\n // environment, names are the stable handle. A name this environment does not\n // know is DROPPED rather than failing the whole seed — the effect below\n // reports the dropped ones once.\n const resolvePlatformFeatures = useCallback(\n (names: string[] | undefined): { ids: string[]; unknown: string[] } => {\n const ids: string[] = [];\n const unknown: string[] = [];\n for (const name of names ?? []) {\n const match = allFeatures.find((feature) => feature.name === name);\n if (match) ids.push(match.id);\n else unknown.push(name);\n }\n return { ids, unknown };\n },\n [allFeatures],\n );\n\n // Fed to BOTH useForm and EditorSheet.onReset. The previous implementation\n // reseeded with its own `useEffect(… form.reset)` on open; EditorSheet already\n // owns that (EditorSheet.tsx:163-190) and running both fights over the form.\n //\n // `active` is absent on purpose: archiving is the single deactivation path\n // (PriceArchiver), and a second toggle here would let the two disagree.\n const getDefaultValues = useCallback((): PriceFormValues => {\n const coreFeatureIds = allFeatures.filter((feature) => feature.isCore).map((feature) => feature.id);\n\n // Seed branch: create mode only (`price` always wins — an edit form must\n // show the price it edits, never a seed). Mirrors the `price` branch below\n // field for field, including the /100: the seed keeps Stripe's MINOR units\n // while the form field is in MAJOR units.\n if (!price && seed) {\n return {\n unitAmount: seed.unitAmount / 100,\n currency: seed.currency,\n interval: seed.interval,\n intervalCount: seed.intervalCount ?? 1,\n usageType: seed.usageType ?? \"licensed\",\n nickname: seed.nickname ?? \"\",\n isTrial: seed.isTrial ?? false,\n description: seed.description ?? \"\",\n features: seed.features ?? [],\n // `.toString()`, for the same reason the schema keeps the token\n // pipeline string-based: a seeded `0` must survive as \"0\" and not be\n // dropped as falsy.\n token: seed.token?.toString() ?? \"\",\n featureIds: [...new Set([...resolvePlatformFeatures(seed.platformFeatures).ids, ...coreFeatureIds])],\n };\n }\n\n return {\n unitAmount: price?.unitAmount ? price.unitAmount / 100 : 0,\n currency: price?.currency ?? \"usd\",\n interval: price?.priceType === \"one_time\" ? \"one_time\" : (price?.recurring?.interval ?? \"month\"),\n intervalCount: price?.recurring?.intervalCount ?? 1,\n usageType: price?.recurring?.usageType ?? \"licensed\",\n nickname: price?.nickname ?? \"\",\n isTrial: price?.isTrial ?? false,\n description: price?.description ?? \"\",\n features: price?.features ?? [],\n token: price?.token?.toString() ?? \"\",\n featureIds: [...new Set([...(price?.priceFeatures?.map((f) => f.id) ?? []), ...coreFeatureIds])],\n };\n }, [price, seed, allFeatures, resolvePlatformFeatures]);\n\n const form = useForm<PriceFormValues>({\n resolver: zodResolver(formSchema) as any,\n defaultValues: getDefaultValues(),\n });\n\n // ── The seeding race ──────────────────────────────────────────────────────\n // `getDefaultValues` reads `allFeatures`, which the fetch effect above\n // resolves asynchronously — so `useForm` has ALREADY seeded from an empty\n // list by the time the features land, and every id-bearing default\n // (`featureIds`: the core features, plus whatever the seed named) is missing.\n //\n // Nothing else fixes it: EditorSheet re-seeds on open only when `isEdit`\n // (EditorSheet.tsx:209 — create defaults usually contain a fresh uuid, so an\n // unconditional reset there would fire on every open and clobber parents that\n // pre-fill via form.setValue). That is exactly why the core-feature\n // checkboxes come up empty on a first create today.\n //\n // So catch up here, once, and only when it is safe:\n // - `!isEdit` — the edit path is EditorSheet's, do not fight it.\n // - `length > 0` — this is the empty → loaded transition, not the initial\n // render (and not a genuinely empty feature list).\n // - the ref — one shot per mount, so a later `allFeatures` identity\n // change cannot re-reset a form the user is using.\n // - `!isDirty` — never clobber anything already typed; a user who edited\n // before the fetch returned keeps their input, and the\n // one-shot ref is burned either way.\n const hasCaughtUpWithFeatures = useRef(false);\n useEffect(() => {\n if (isEdit || hasCaughtUpWithFeatures.current || allFeatures.length === 0) return;\n hasCaughtUpWithFeatures.current = true;\n if (form.formState.isDirty) return;\n form.reset(getDefaultValues());\n }, [allFeatures, isEdit, form, getDefaultValues]);\n\n // Unknown platform feature names are reported ONCE per seed, not per render:\n // `seed` typically arrives as an object literal from the list row or the\n // import dialog, so its identity changes on every parent render — the guard\n // keys on the names themselves instead. Waits for `allFeatures`, since before\n // the fetch resolves every name looks unknown.\n const warnedUnknownFor = useRef<string | undefined>(undefined);\n useEffect(() => {\n if (isEdit || !seed || allFeatures.length === 0) return;\n const fingerprint = JSON.stringify(seed.platformFeatures ?? []);\n if (warnedUnknownFor.current === fingerprint) return;\n warnedUnknownFor.current = fingerprint;\n\n const { unknown } = resolvePlatformFeatures(seed.platformFeatures);\n if (unknown.length > 0)\n showToast(t(\"billing.admin.prices.import.warnings.unknownFeatures\", { features: unknown.join(\", \") }));\n }, [seed, isEdit, allFeatures, resolvePlatformFeatures, t]);\n\n const watchInterval = form.watch(\"interval\");\n const isRecurring = watchInterval !== \"one_time\";\n\n const currencyOptions = [\n { id: \"usd\", text: \"USD ($)\" },\n { id: \"eur\", text: \"EUR (€)\" },\n { id: \"gbp\", text: \"GBP (£)\" },\n ];\n\n const intervalOptions = [\n { id: \"one_time\", text: t(\"billing.admin.prices.interval.one_time\") },\n { id: \"day\", text: t(\"billing.admin.prices.interval.day\") },\n { id: \"week\", text: t(\"billing.admin.prices.interval.week\") },\n { id: \"month\", text: t(\"billing.admin.prices.interval.month\") },\n { id: \"year\", text: t(\"billing.admin.prices.interval.year\") },\n ];\n\n const usageTypeOptions = [\n { id: \"licensed\", text: t(\"billing.admin.prices.usage.licensed\") },\n { id: \"metered\", text: t(\"billing.admin.prices.usage.metered\") },\n ];\n\n return (\n <EditorSheet\n form={form}\n entityType={t(\"billing.admin.prices.entity\")}\n entityName={price ? priceLabel(price, fmt) : undefined}\n isEdit={isEdit}\n module={Modules.StripePrice}\n size=\"lg\"\n propagateChanges={propagateChanges}\n onSuccess={onSuccess}\n onNavigate={(url) => router.push(url)}\n onSubmit={async (values) => {\n const trimmedFeatures = values.features.filter((feature) => feature.trim());\n\n if (isEdit) {\n const patch: StripePriceInput = {\n id: price.id,\n nickname: values.nickname || undefined,\n description: values.description || undefined,\n features: trimmedFeatures.length > 0 ? trimmedFeatures : undefined,\n token: values.token ? parseInt(values.token, 10) : undefined,\n // Stripe one-time prices carry neither a trial flag nor platform\n // features, so those two only travel for recurring prices.\n ...(price.priceType === \"recurring\" ? { isTrial: values.isTrial, featureIds: values.featureIds } : {}),\n };\n return await updatePrice(patch);\n }\n\n if (!targetProductId) throw new Error(\"PriceEditor requires a productId to create a price\");\n\n const payload: StripePriceInput = {\n id: v4(),\n productId: targetProductId,\n currency: values.currency,\n unitAmount: Math.round(values.unitAmount * 100),\n };\n if (isRecurring) {\n payload.recurring = {\n interval: values.interval as \"day\" | \"week\" | \"month\" | \"year\",\n intervalCount: values.intervalCount ?? 1,\n usageType: values.usageType ?? \"licensed\",\n };\n payload.isTrial = values.isTrial;\n if (values.featureIds.length > 0) payload.featureIds = values.featureIds;\n }\n if (values.nickname) payload.nickname = values.nickname;\n if (values.description) payload.description = values.description;\n if (trimmedFeatures.length > 0) payload.features = trimmedFeatures;\n if (values.token) payload.token = parseInt(values.token, 10);\n\n return await createPrice(payload);\n }}\n onReset={getDefaultValues}\n trigger={trigger}\n forceShow={forceShow}\n onClose={onClose}\n dialogOpen={dialogOpen}\n onDialogOpenChange={onDialogOpenChange}\n >\n <div className=\"flex w-full flex-col gap-y-4\">\n {isEdit && (\n <Alert>\n <AlertTitle>{t(\"billing.admin.prices.immutability.title\")}</AlertTitle>\n <AlertDescription>{t(\"billing.admin.prices.immutability.description\")}</AlertDescription>\n </Alert>\n )}\n\n <div className=\"grid grid-cols-2 gap-x-4\">\n {/* type=\"decimal\", NOT type=\"currency\": the currency variant hardcodes\n a Euro symbol (FormInput.tsx:105-106), which contradicts the\n currency selector sitting next to it. */}\n <FormInput\n form={form}\n id=\"unitAmount\"\n type=\"decimal\"\n name={t(\"billing.admin.prices.fields.amount\")}\n placeholder={t(\"billing.admin.prices.placeholders.amount\")}\n disabled={isEdit}\n isRequired\n />\n <FormSelect\n form={form}\n id=\"currency\"\n name={t(\"billing.admin.prices.fields.currency\")}\n values={currencyOptions}\n disabled={isEdit}\n />\n </div>\n\n <FormSelect\n form={form}\n id=\"interval\"\n name={t(\"billing.admin.prices.fields.interval\")}\n values={intervalOptions}\n disabled={isEdit}\n />\n\n {isRecurring && (\n <div className=\"grid grid-cols-2 gap-x-4\">\n <FormInput\n form={form}\n id=\"intervalCount\"\n type=\"number\"\n name={t(\"billing.admin.prices.fields.intervalCount\")}\n placeholder={t(\"billing.admin.prices.placeholders.intervalCount\")}\n disabled={isEdit}\n />\n <FormSelect\n form={form}\n id=\"usageType\"\n name={t(\"billing.admin.prices.fields.usageType\")}\n values={usageTypeOptions}\n disabled={isEdit}\n />\n </div>\n )}\n\n <FormInput\n form={form}\n id=\"nickname\"\n name={t(\"billing.admin.prices.fields.nickname\")}\n placeholder={t(\"billing.admin.prices.placeholders.nickname\")}\n />\n\n <FormTextarea\n form={form}\n id=\"description\"\n name={t(\"billing.admin.prices.fields.description\")}\n placeholder={t(\"billing.admin.prices.placeholders.description\")}\n className=\"min-h-24\"\n />\n\n <FormInput\n form={form}\n id=\"token\"\n type=\"number\"\n name={t(\"billing.admin.prices.fields.token\")}\n placeholder={t(\"billing.admin.prices.placeholders.token\")}\n />\n\n <div className=\"flex flex-col gap-y-2\">\n <FieldLabel>{t(\"billing.admin.prices.fields.features\")}</FieldLabel>\n {form.watch(\"features\").map((_, index) => (\n <div key={index} className=\"flex gap-2\">\n <Input\n {...form.register(`features.${index}`)}\n placeholder={t(\"billing.admin.prices.placeholders.feature\", { index: index + 1 })}\n className=\"flex-1\"\n />\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"icon\"\n aria-label={t(\"billing.admin.prices.actions.removeFeature\")}\n onClick={() =>\n form.setValue(\n \"features\",\n form.getValues(\"features\").filter((_, i) => i !== index),\n )\n }\n >\n <XIcon />\n </Button>\n </div>\n ))}\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"self-start\"\n onClick={() => form.setValue(\"features\", [...form.getValues(\"features\"), \"\"])}\n >\n <PlusIcon />\n {t(\"billing.admin.prices.actions.addFeature\")}\n </Button>\n </div>\n\n {isRecurring && allFeatures.length > 0 && (\n <div className=\"flex flex-col gap-y-2\">\n <FieldLabel>{t(\"billing.admin.prices.fields.platformFeatures\")}</FieldLabel>\n <div className=\"flex max-h-48 flex-col gap-y-2 overflow-y-auto rounded-md border p-4\">\n {allFeatures.map((feature) => {\n const isChecked = form.watch(\"featureIds\").includes(feature.id);\n return (\n <div key={feature.id} className=\"flex items-center gap-x-2\">\n <Checkbox\n id={`feature-${feature.id}`}\n checked={isChecked}\n disabled={feature.isCore}\n onCheckedChange={(checked) => {\n const current = form.getValues(\"featureIds\");\n if (checked) form.setValue(\"featureIds\", [...new Set([...current, feature.id])]);\n else if (!feature.isCore)\n form.setValue(\n \"featureIds\",\n current.filter((id) => id !== feature.id),\n );\n }}\n />\n <FieldLabel htmlFor={`feature-${feature.id}`}>{feature.name}</FieldLabel>\n {/* Typography role 12 (caption) — token colour, never text-gray-*. */}\n {feature.isCore && (\n <span className=\"text-muted-foreground text-xs\">{t(\"billing.admin.prices.help.core\")}</span>\n )}\n </div>\n );\n })}\n </div>\n </div>\n )}\n\n {isRecurring && (\n <FormCheckbox\n form={form}\n id=\"isTrial\"\n name={t(\"billing.admin.prices.fields.trial\")}\n description={t(\"billing.admin.prices.help.trial\")}\n />\n )}\n </div>\n </EditorSheet>\n );\n}\n\nexport default function PriceEditor(props: PriceEditorProps) {\n return <PriceEditorInternal {...props} />;\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { createContext, ReactNode, useCallback, useContext, useMemo, useState } from \"react\";\nimport { SharedProvider } from \"../../../contexts/SharedContext\";\nimport { JsonApiHydratedDataInterface, Modules, rehydrate } from \"../../../core\";\nimport { usePageUrlGenerator } from \"../../../hooks\";\nimport { BreadcrumbItemData } from \"../../../interfaces\";\nimport { getRoleId } from \"../../../roles\";\nimport { useCurrentUserContext } from \"../../user/contexts\";\nimport { ProductArchiver } from \"../stripe-product/components/forms/ProductArchiver\";\nimport ProductEditor from \"../stripe-product/components/forms/ProductEditor\";\nimport { StripeProductInput, StripeProductInterface } from \"../stripe-product/data/stripe-product.interface\";\nimport { StripeProductService } from \"../stripe-product/data/stripe-product.service\";\n\nexport interface ProductContextType {\n product: StripeProductInterface | undefined;\n setProduct: (value: StripeProductInterface | undefined) => void;\n reloadProduct: () => Promise<void>;\n createProduct: (input: StripeProductInput) => Promise<StripeProductInterface>;\n updateProduct: (input: StripeProductInput) => Promise<StripeProductInterface>;\n archiveProduct: (id?: string) => Promise<void>;\n restoreProduct: (id?: string) => Promise<void>;\n /** Bumped by every mutation. Mounted lists re-fetch on it. */\n catalogueVersion: number;\n refreshCatalogue: () => void;\n}\n\nconst ProductContext = createContext<ProductContextType | undefined>(undefined);\n\nexport type ProductProviderProps = {\n children: ReactNode;\n dehydratedProduct?: JsonApiHydratedDataInterface;\n /**\n * Publish breadcrumbs + title into SharedProvider. Pass `false` when this\n * provider is mounted INSIDE another entity's page (the prices tab mounts\n * PriceProvider that way): SharedProvider replaces its value for the subtree\n * rather than merging (SharedContext.tsx:35-37), so a nested provider that\n * publishes chrome would blank the host page's title.\n */\n publishChrome?: boolean;\n};\n\nconst defaultContextValue: ProductContextType = {\n product: undefined,\n setProduct: () => {},\n reloadProduct: async () => {},\n createProduct: async () => {\n throw new Error(\"createProduct requires a ProductProvider\");\n },\n updateProduct: async () => {\n throw new Error(\"updateProduct requires a ProductProvider\");\n },\n archiveProduct: async () => {},\n restoreProduct: async () => {},\n catalogueVersion: 0,\n refreshCatalogue: () => {},\n};\n\nexport const ProductProvider = ({ children, dehydratedProduct, publishChrome = true }: ProductProviderProps) => {\n const t = useTranslations();\n const generateUrl = usePageUrlGenerator();\n const { hasRole } = useCurrentUserContext();\n\n const [product, setProduct] = useState<StripeProductInterface | undefined>(\n dehydratedProduct ? rehydrate<StripeProductInterface>(Modules.StripeProduct, dehydratedProduct) : undefined,\n );\n const [catalogueVersion, setCatalogueVersion] = useState<number>(0);\n\n const refreshCatalogue = useCallback(() => setCatalogueVersion((value) => value + 1), []);\n\n const reloadProduct = useCallback(async () => {\n if (!product?.id) return;\n setProduct(await StripeProductService.getProduct({ id: product.id }));\n }, [product?.id]);\n\n const createProduct = useCallback(\n async (input: StripeProductInput) => {\n const saved = await StripeProductService.createProduct(input);\n refreshCatalogue();\n return saved;\n },\n [refreshCatalogue],\n );\n\n const updateProduct = useCallback(\n async (input: StripeProductInput) => {\n const saved = await StripeProductService.updateProduct(input);\n setProduct(saved);\n refreshCatalogue();\n return saved;\n },\n [refreshCatalogue],\n );\n\n // The archive/reactivate routes answer 204 NO_CONTENT with an empty body\n // (stripe-product.controller.ts:99-115) even though the service signature\n // claims it returns a product — so the response is discarded and the entity\n // is re-read instead.\n const archiveProduct = useCallback(\n async (id?: string) => {\n const target = id ?? product?.id;\n if (!target) return;\n await StripeProductService.archiveProduct({ id: target });\n await reloadProduct();\n refreshCatalogue();\n },\n [product?.id, reloadProduct, refreshCatalogue],\n );\n\n const restoreProduct = useCallback(\n async (id?: string) => {\n const target = id ?? product?.id;\n if (!target) return;\n await StripeProductService.reactivateProduct({ id: target });\n await reloadProduct();\n refreshCatalogue();\n },\n [product?.id, reloadProduct, refreshCatalogue],\n );\n\n const value = useMemo<ProductContextType>(\n () => ({\n product,\n setProduct,\n reloadProduct,\n createProduct,\n updateProduct,\n archiveProduct,\n restoreProduct,\n catalogueVersion,\n refreshCatalogue,\n }),\n [\n product,\n reloadProduct,\n createProduct,\n updateProduct,\n archiveProduct,\n restoreProduct,\n catalogueVersion,\n refreshCatalogue,\n ],\n );\n\n if (!publishChrome) return <ProductContext.Provider value={value}>{children}</ProductContext.Provider>;\n\n const breadcrumb = (): BreadcrumbItemData[] => {\n const items: BreadcrumbItemData[] = [\n {\n name: t(\"billing.admin.products.title\"),\n // Linked only when it is NOT the current page.\n ...(product ? { href: generateUrl({ page: Modules.StripeProduct }) } : {}),\n },\n ];\n if (product) items.push({ name: product.name });\n return items;\n };\n\n const title = () => {\n const response: any = { type: t(\"billing.admin.products.title\") };\n if (!product) return response;\n\n response.element = product.name;\n\n // narr8 note: gate on hasRole ONLY. hasPermissionToModule always returns\n // false in this app, so gating an editor on it hides the editor outright.\n if (hasRole(getRoleId().Administrator)) {\n response.functions = [\n <ProductArchiver key=\"productArchiver\" product={product} />,\n <ProductEditor key=\"productEditor\" product={product} propagateChanges={setProduct} />,\n ];\n }\n\n return response;\n };\n\n return (\n <SharedProvider value={{ breadcrumbs: breadcrumb(), title: title() }}>\n <ProductContext.Provider value={value}>{children}</ProductContext.Provider>\n </SharedProvider>\n );\n};\n\nexport const useProductContext = (): ProductContextType => useContext(ProductContext) ?? defaultContextValue;\n","\"use client\";\n\nimport { ArchiveIcon, RotateCcwIcon } from \"lucide-react\";\nimport { useTranslations } from \"next-intl\";\nimport { useState } from \"react\";\nimport { errorToast } from \"../../../../../components/errors\";\nimport { Button, ConfirmDialog } from \"../../../../../shadcnui\";\nimport { useProductContext } from \"../../../contexts/ProductContext\";\nimport { StripeProductInterface } from \"../../data/stripe-product.interface\";\n\ntype ProductArchiverProps = {\n product: StripeProductInterface;\n};\n\n/**\n * Archive / restore control for a product. Built on ConfirmDialog, which is\n * controlled and trigger-less by design — the caller owns the trigger button,\n * so none of Base UI's trigger-composition pitfalls apply (no asChild, no\n * nested <button>).\n */\nexport function ProductArchiver({ product }: ProductArchiverProps) {\n const t = useTranslations();\n const { archiveProduct, restoreProduct } = useProductContext();\n const [open, setOpen] = useState<boolean>(false);\n\n const isArchived = !product.active;\n const scope = isArchived ? \"restore\" : \"archive\";\n\n return (\n <>\n <Button variant=\"outline\" size=\"sm\" onClick={() => setOpen(true)}>\n {isArchived ? <RotateCcwIcon /> : <ArchiveIcon />}\n {t(`billing.admin.products.${scope}.confirm`)}\n </Button>\n <ConfirmDialog\n open={open}\n onOpenChange={setOpen}\n title={t(`billing.admin.products.${scope}.title`)}\n description={t(`billing.admin.products.${scope}.description`, { name: product.name })}\n confirmLabel={t(`billing.admin.products.${scope}.confirm`)}\n cancelLabel={t(\"ui.buttons.cancel\")}\n destructive={!isArchived}\n onConfirm={async () => {\n try {\n if (isArchived) await restoreProduct(product.id);\n else await archiveProduct(product.id);\n } catch (error) {\n errorToast({ title: t(`billing.admin.products.${scope}.title`), error });\n throw error; // keeps the dialog open — ConfirmDialog swallows rejections\n }\n }}\n />\n </>\n );\n}\n","\"use client\";\n\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { useTranslations } from \"next-intl\";\nimport { ReactNode, useCallback, useMemo } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { v4 } from \"uuid\";\nimport { z } from \"zod\";\nimport { EditorSheet, FormInput, FormTextarea } from \"../../../../../components\";\nimport { Modules } from \"../../../../../core\";\nimport { useI18nRouter } from \"../../../../../i18n\";\nimport { useProductContext } from \"../../../contexts/ProductContext\";\nimport { StripeProductInterface } from \"../../data/stripe-product.interface\";\n\nexport type ProductEditorProps = {\n product?: StripeProductInterface;\n propagateChanges?: (product: StripeProductInterface) => void;\n onSuccess?: () => void | Promise<void>;\n trigger?: ReactNode;\n forceShow?: boolean;\n onClose?: () => void;\n dialogOpen?: boolean;\n onDialogOpenChange?: (open: boolean) => void;\n};\n\nfunction ProductEditorInternal({\n product,\n propagateChanges,\n onSuccess,\n trigger,\n forceShow,\n onClose,\n dialogOpen,\n onDialogOpenChange,\n}: ProductEditorProps) {\n const t = useTranslations();\n const router = useI18nRouter();\n const { createProduct, updateProduct } = useProductContext();\n const isEdit = !!product;\n\n const formSchema = useMemo(\n () =>\n z.object({\n id: z.uuidv4(),\n name: z.string().min(1, { message: t(\"billing.admin.products.errors.name\") }),\n description: z.string().optional(),\n }),\n [t],\n );\n\n // `active` is deliberately absent: archiving is the single deactivation path\n // (ProductArchiver), and a second toggle here would let the two disagree.\n const getDefaultValues = useCallback(\n () => ({\n id: product?.id ?? v4(),\n name: product?.name ?? \"\",\n description: product?.description ?? \"\",\n }),\n [product],\n );\n\n const form = useForm<z.infer<typeof formSchema>>({\n resolver: zodResolver(formSchema),\n defaultValues: getDefaultValues(),\n });\n\n return (\n <EditorSheet\n form={form}\n entityType={t(\"billing.admin.products.entity\")}\n entityName={product?.name}\n isEdit={isEdit}\n module={Modules.StripeProduct}\n size=\"md\"\n propagateChanges={propagateChanges}\n onSuccess={onSuccess}\n onNavigate={(url) => router.push(url)}\n onSubmit={async (values) => {\n const payload = { id: values.id, name: values.name, description: values.description };\n return isEdit ? await updateProduct(payload) : await createProduct(payload);\n }}\n onReset={getDefaultValues}\n trigger={trigger}\n forceShow={forceShow}\n onClose={onClose}\n dialogOpen={dialogOpen}\n onDialogOpenChange={onDialogOpenChange}\n >\n <div className=\"flex w-full flex-col gap-y-4\">\n <FormInput\n form={form}\n id=\"name\"\n name={t(\"billing.admin.products.fields.name\")}\n placeholder={t(\"billing.admin.products.placeholders.name\")}\n isRequired\n />\n <FormTextarea\n form={form}\n id=\"description\"\n name={t(\"billing.admin.products.fields.description\")}\n placeholder={t(\"billing.admin.products.placeholders.description\")}\n className=\"min-h-32\"\n />\n </div>\n </EditorSheet>\n );\n}\n\nexport default function ProductEditor(props: ProductEditorProps) {\n return <ProductEditorInternal {...props} />;\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { DetailField, SectionHeader } from \"../../../../../components\";\nimport { Modules } from \"../../../../../core\";\nimport { usePageUrlGenerator } from \"../../../../../hooks\";\nimport { Badge, Link } from \"../../../../../shadcnui\";\nimport { formatCurrency, formatInterval, useBillingFormatOptions } from \"../../../components/utils\";\nimport { usePriceContext } from \"../../../contexts/PriceContext\";\n\nexport function PriceDetails() {\n const t = useTranslations();\n const fmt = useBillingFormatOptions();\n const generateUrl = usePageUrlGenerator();\n const { price } = usePriceContext();\n\n if (!price) return null;\n\n const isRecurring = price.priceType === \"recurring\";\n\n return (\n <div className=\"flex w-full flex-col gap-y-8\">\n <div className=\"flex w-full flex-col gap-y-4\">\n <SectionHeader>{t(\"billing.admin.prices.title\")}</SectionHeader>\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2\">\n <DetailField\n label={t(\"billing.admin.prices.fields.product\")}\n value={\n price.product ? (\n <Link href={generateUrl({ page: Modules.StripeProduct, id: price.product.id })}>\n {price.product.name}\n </Link>\n ) : (\n \"—\"\n )\n }\n />\n <DetailField\n label={t(\"billing.admin.prices.fields.status\")}\n value={\n <span className=\"flex flex-wrap items-center gap-1\">\n {price.active ? (\n <Badge variant=\"softGreen\">{t(\"billing.admin.prices.status.active\")}</Badge>\n ) : (\n <Badge variant=\"softGray\">{t(\"billing.admin.prices.status.archived\")}</Badge>\n )}\n {price.isTrial && <Badge variant=\"softBlue\">{t(\"billing.admin.prices.badge.trial\")}</Badge>}\n </span>\n }\n />\n <DetailField\n label={t(\"billing.admin.prices.fields.amount\")}\n value={<span className=\"tabular-nums\">{formatCurrency(price.unitAmount, price.currency, fmt)}</span>}\n />\n <DetailField label={t(\"billing.admin.prices.fields.currency\")} value={price.currency.toUpperCase()} />\n <DetailField\n label={t(\"billing.admin.prices.fields.type\")}\n value={isRecurring ? t(\"billing.admin.prices.type.recurring\") : t(\"billing.admin.prices.type.oneTime\")}\n />\n <DetailField label={t(\"billing.admin.prices.fields.billing\")} value={formatInterval(price, fmt)} />\n {isRecurring && (\n <DetailField\n label={t(\"billing.admin.prices.fields.intervalCount\")}\n value={<span className=\"tabular-nums\">{price.recurring?.intervalCount ?? 1}</span>}\n />\n )}\n {isRecurring && (\n <DetailField\n label={t(\"billing.admin.prices.fields.usageType\")}\n value={\n price.recurring?.usageType === \"metered\"\n ? t(\"billing.admin.prices.usage.metered\")\n : t(\"billing.admin.prices.usage.licensed\")\n }\n />\n )}\n </div>\n </div>\n\n <div className=\"flex w-full flex-col gap-y-4\">\n <SectionHeader>{t(\"billing.admin.prices.fields.platformFeatures\")}</SectionHeader>\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2\">\n <DetailField\n label={t(\"billing.admin.prices.fields.token\")}\n value={<span className=\"tabular-nums\">{price.token ?? \"—\"}</span>}\n />\n <DetailField\n label={t(\"billing.admin.prices.fields.stripeId\")}\n value={<span className=\"tabular-nums\">{price.stripePriceId}</span>}\n />\n <DetailField\n label={t(\"billing.admin.prices.fields.description\")}\n value={price.description ?? \"—\"}\n className=\"md:col-span-2\"\n />\n <DetailField\n label={t(\"billing.admin.prices.fields.features\")}\n className=\"md:col-span-2\"\n value={\n price.features && price.features.length > 0 ? (\n <ul className=\"flex flex-col gap-1 text-sm\">\n {price.features.map((feature) => (\n <li key={feature}>{feature}</li>\n ))}\n </ul>\n ) : (\n \"—\"\n )\n }\n />\n <DetailField\n label={t(\"billing.admin.prices.fields.platformFeatures\")}\n className=\"md:col-span-2\"\n value={\n price.priceFeatures.length > 0 ? (\n <span className=\"flex flex-wrap items-center gap-1\">\n {price.priceFeatures.map((feature) => (\n <Badge key={feature.id} variant=\"softGray\">\n {feature.name}\n </Badge>\n ))}\n </span>\n ) : (\n \"—\"\n )\n }\n />\n </div>\n </div>\n </div>\n );\n}\n","\"use client\";\n\nimport { RoundPageContainer } from \"../../../../../components\";\nimport { Modules } from \"../../../../../core\";\nimport { usePriceContext } from \"../../../contexts/PriceContext\";\nimport { PriceDetails } from \"../details/PriceDetails\";\n\nexport function PriceContainer() {\n const { price } = usePriceContext();\n\n if (!price) return null;\n\n // No tabs: a price is a single flat record. RoundPageContainer without\n // `tabs` and without `fullWidth` gives the centred max-w-6xl column and the\n // title bar that PriceContext's chrome fills.\n return (\n <RoundPageContainer module={Modules.StripePrice} id={price.id}>\n <PriceDetails />\n </RoundPageContainer>\n );\n}\n","\"use client\";\n\nimport { DollarSignIcon, UploadIcon } from \"lucide-react\";\nimport { useTranslations } from \"next-intl\";\nimport { ChangeEvent, Fragment, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { ContentListTable, errorToast } from \"../../../../../components\";\nimport { Modules } from \"../../../../../core\";\nimport { DataListRetriever, useDataListRetriever } from \"../../../../../hooks\";\nimport { Button, EmptyState } from \"../../../../../shadcnui\";\nimport { usePriceContext } from \"../../../contexts/PriceContext\";\nimport { StripePriceFields } from \"../../data/stripe-price.fields\";\nimport { StripePriceInterface } from \"../../data/stripe-price.interface\";\nimport { StripePriceService } from \"../../data/stripe-price.service\";\nimport {\n buildStripePriceTransferDocument,\n parseStripePriceTransferDocument,\n priceToSeed,\n StripePriceSeed,\n stripePriceTransferFilename,\n} from \"../../data/stripe-price.transfer\";\nimport PriceEditor from \"../forms/PriceEditor\";\n\ntype PricesListProps = {\n productId: string;\n /**\n * Only when the list IS the page's own content inside a `RoundPageContainer\n * fullWidth`. Inside a tab panel leave it unset, or ContentListTable drops its\n * own `rounded-md border` and the table floats borderless in the panel.\n * Mirrors ThreatList, which is rendered both ways.\n */\n fullWidth?: boolean;\n};\n\nexport function PricesList({ productId, fullWidth }: PricesListProps) {\n const t = useTranslations();\n const { priceVersion } = usePriceContext();\n const importInputRef = useRef<HTMLInputElement>(null);\n\n // ONE seeded editor serves both clone and import. It is mounted on demand\n // (`seed &&`) rather than kept mounted and toggled, because PriceEditor builds\n // its react-hook-form defaults at construction time — a permanently mounted\n // editor would be constructed with no seed and ignore every later one.\n const [seed, setSeed] = useState<StripePriceSeed | undefined>(undefined);\n // Remount key. Cloning a second row while the first clone's editor has just\n // closed would otherwise reuse the same component instance and keep the first\n // seed's defaults.\n const [seedNonce, setSeedNonce] = useState(0);\n\n const data: DataListRetriever<StripePriceInterface> = useDataListRetriever({\n retriever: (params) => StripePriceService.listPrices({ ...params, productId }),\n retrieverParams: {},\n module: Modules.StripePrice,\n });\n\n useEffect(() => {\n if (priceVersion > 0) void data.refresh();\n }, [priceVersion]);\n\n const openSeededEditor = useCallback((next: StripePriceSeed) => {\n setSeed(next);\n setSeedNonce((nonce) => nonce + 1);\n }, []);\n\n const onClone = useCallback(\n (price: StripePriceInterface) => openSeededEditor(priceToSeed(price)),\n [openSeededEditor],\n );\n\n /**\n * Purely client-side: the document is built from the price already in memory,\n * so there is no API round trip. Download mechanics copied verbatim from\n * BackupCodesDialog.handleDownload (Blob -> createObjectURL -> anchor ->\n * click -> revokeObjectURL).\n */\n const onExport = useCallback((price: StripePriceInterface) => {\n const text = JSON.stringify(buildStripePriceTransferDocument(price), null, 2);\n const blob = new Blob([text], { type: \"application/json\" });\n const url = URL.createObjectURL(blob);\n const a = document.createElement(\"a\");\n a.href = url;\n a.download = stripePriceTransferFilename(price);\n a.click();\n URL.revokeObjectURL(url);\n }, []);\n\n // Memoised: ContentListTable is `memo`-wrapped and feeds this straight into\n // the table generator, so a fresh object every render would rebuild every\n // column on every render.\n const tableContext = useMemo(() => ({ onClone, onExport }), [onClone, onExport]);\n\n const onImportFileChange = useCallback(\n async (event: ChangeEvent<HTMLInputElement>) => {\n const input = event.target;\n const file = input.files?.[0];\n // Reset immediately (EditableAvatar.tsx:119-126 does the same) so picking\n // the SAME file twice still fires `change`. Held in `input` rather than\n // read off the event after the await, since the handler is async.\n input.value = \"\";\n if (!file) return;\n\n try {\n // NOT named `document`: that shadows the global the export path uses for\n // `document.createElement`, and a later edit that moved code between the\n // two handlers would break silently.\n const transfer = parseStripePriceTransferDocument(await file.text());\n openSeededEditor(transfer.price);\n } catch (error) {\n // The parser's own messages are hardcoded English (it is a pure,\n // i18n-free module), so they are logged for debugging and the user sees\n // the translated line instead.\n console.error(\"[PricesList] price import failed\", error);\n errorToast({\n title: t(\"billing.admin.prices.import.errors.title\"),\n error: t(\"billing.admin.prices.import.errors.invalid\"),\n });\n }\n },\n [openSeededEditor, t],\n );\n\n const functions: ReactNode[] = [\n <PriceEditor\n key=\"create-price\"\n productId={productId}\n // Present ONLY to suppress navigation: without an `onSuccess`,\n // EditorSheet.wrappedOnSubmit falls through to `onNavigate` and pushes the\n // brand-new price's page, yanking the admin off the product they were\n // editing. Deliberately does NOT call `data.refresh()` — PriceContext's\n // `createPrice` already bumps `priceVersion`, and the effect above\n // refreshes on that, so refreshing here too would fire two identical list\n // requests for one create.\n onSuccess={() => {}}\n />,\n <Fragment key=\"import-price\">\n <Button variant=\"outline\" size=\"sm\" type=\"button\" onClick={() => importInputRef.current?.click()}>\n <UploadIcon />\n {t(\"billing.admin.prices.actions.import\")}\n </Button>\n <input\n ref={importInputRef}\n type=\"file\"\n accept=\"application/json\"\n className=\"hidden\"\n onChange={onImportFileChange}\n />\n </Fragment>,\n ];\n\n return (\n <>\n <ContentListTable\n data={data}\n fields={[\n StripePriceFields.nickname,\n StripePriceFields.amount,\n StripePriceFields.interval,\n StripePriceFields.token,\n StripePriceFields.status,\n StripePriceFields.actions,\n ]}\n tableGeneratorType={Modules.StripePrice}\n functions={functions}\n context={tableContext}\n fullWidth={fullWidth}\n title={t(\"billing.admin.prices.title\")}\n emptyState={\n <EmptyState\n icon={DollarSignIcon}\n title={t(\"billing.admin.prices.empty.title\")}\n description={t(\"billing.admin.prices.empty.description\")}\n />\n }\n />\n {/* Passing `dialogOpen` makes EditorSheet render no trigger at all\n (EditorSheet.tsx:265-267), so this is a headless, caller-driven sheet. */}\n {seed && (\n <PriceEditor\n key={seedNonce}\n productId={productId}\n seed={seed}\n dialogOpen\n onDialogOpenChange={(open) => {\n if (!open) setSeed(undefined);\n }}\n onSuccess={async () => {\n setSeed(undefined);\n await data.refresh();\n }}\n />\n )}\n </>\n );\n}\n","import { z } from \"zod\";\nimport { StripePriceInterface } from \"./stripe-price.interface\";\n\n// ============================================================================\n// Stripe Price Transfer Document\n// ----------------------------------------------------------------------------\n// Pure, DOM-free: no React, no window, no Blob, no fetch. Everything here is a\n// plain data transformation so it can be unit-tested without a renderer, and so\n// both the export (download) and the clone (prefill an editor) paths can share\n// one definition of \"the editable shape of a price\".\n// ============================================================================\n\nexport const STRIPE_PRICE_TRANSFER_KIND = \"stripe-price\";\nexport const STRIPE_PRICE_TRANSFER_VERSION = 1;\n\n/** Versions this build can still read. Older documents stay parseable here. */\nconst SUPPORTED_VERSIONS: number[] = [STRIPE_PRICE_TRANSFER_VERSION];\n\n/** The editable shape of a price, portable between environments. */\nexport type StripePriceSeed = {\n unitAmount: number; // MINOR units (cents), exactly as Stripe stores it\n currency: string;\n interval: \"one_time\" | \"day\" | \"week\" | \"month\" | \"year\";\n intervalCount?: number;\n usageType?: \"licensed\" | \"metered\";\n nickname?: string;\n description?: string;\n features?: string[]; // free-text marketing bullets\n token?: number;\n isTrial?: boolean;\n platformFeatures?: string[]; // Feature entity NAMES (never ids — ids differ per environment)\n};\n\nexport type StripePriceTransferDocument = {\n kind: typeof STRIPE_PRICE_TRANSFER_KIND;\n version: typeof STRIPE_PRICE_TRANSFER_VERSION;\n price: StripePriceSeed;\n};\n\nconst seedSchema = z.object({\n unitAmount: z.number().min(0),\n currency: z.string().min(1),\n interval: z.enum([\"one_time\", \"day\", \"week\", \"month\", \"year\"]),\n intervalCount: z.number().min(1).optional(),\n usageType: z.enum([\"licensed\", \"metered\"]).optional(),\n nickname: z.string().optional(),\n description: z.string().optional(),\n features: z.array(z.string()).optional(),\n token: z.number().optional(),\n isTrial: z.boolean().optional(),\n platformFeatures: z.array(z.string()).optional(),\n});\n\n/**\n * `currency` and `priceType` are getters that THROW when the attribute is\n * absent (stripe-price.ts:42-58), and `productId` demonstrably is absent on a\n * hydrated price (see the note in PriceEditor). Read anything model-backed\n * through here so a partially-hydrated price degrades to a default instead of\n * exploding mid-export.\n */\nfunction readOptional<T>(read: () => T): T | undefined {\n try {\n return read();\n } catch {\n return undefined;\n }\n}\n\nfunction nonEmptyStrings(values: string[] | undefined): string[] {\n if (!Array.isArray(values)) return [];\n return values.map((value) => value.trim()).filter((value) => value.length > 0);\n}\n\n/**\n * Clone path: a live price -> the editable seed.\n *\n * Carries NO identity: no id, no stripePriceId, no productId, no active, no\n * lookupKey, no metadata. A clone is a brand-new Stripe price and Stripe mints\n * its own id on create; copying any of those across would either collide or\n * silently re-point the new price at the source environment's objects.\n */\nexport function priceToSeed(price: StripePriceInterface): StripePriceSeed {\n const priceType = readOptional(() => price.priceType);\n const currency = readOptional(() => price.currency);\n const recurring = price.recurring;\n\n // Same derivation as PriceEditor's getDefaultValues, so a cloned seed and the\n // form it prefills never disagree about what interval a price has.\n const interval: StripePriceSeed[\"interval\"] =\n priceType === \"one_time\" ? \"one_time\" : (recurring?.interval ?? \"month\");\n\n // MINOR units on purpose. PriceEditor divides by 100 for display; the\n // document stays in the unit Stripe itself stores.\n const seed: StripePriceSeed = {\n unitAmount: price.unitAmount ?? 0,\n currency: currency ?? \"usd\",\n interval,\n };\n\n if (interval !== \"one_time\") {\n if (typeof recurring?.intervalCount === \"number\") seed.intervalCount = recurring.intervalCount;\n if (recurring?.usageType) seed.usageType = recurring.usageType;\n }\n\n const nickname = price.nickname?.trim();\n if (nickname) seed.nickname = nickname;\n\n const description = price.description?.trim();\n if (description) seed.description = description;\n\n const features = nonEmptyStrings(price.features);\n if (features.length > 0) seed.features = features;\n\n // `0` is a meaningful token value (\"no AI\"), so test for the type, never for\n // truthiness.\n if (typeof price.token === \"number\" && !Number.isNaN(price.token)) seed.token = price.token;\n\n if (typeof price.isTrial === \"boolean\") seed.isTrial = price.isTrial;\n\n // NAMES, never ids: Feature ids differ per environment, names are the stable\n // handle an importing environment can resolve against its own Feature rows.\n const platformFeatures = nonEmptyStrings(price.priceFeatures?.map((feature) => feature.name));\n if (platformFeatures.length > 0) seed.platformFeatures = platformFeatures;\n\n return seed;\n}\n\n/** Export path. */\nexport function buildStripePriceTransferDocument(price: StripePriceInterface): StripePriceTransferDocument {\n return {\n kind: STRIPE_PRICE_TRANSFER_KIND,\n version: STRIPE_PRICE_TRANSFER_VERSION,\n price: priceToSeed(price),\n };\n}\n\nfunction slugify(value: string): string {\n return value\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n}\n\n/** Export path: a safe download filename, e.g. \"price-standard.json\". */\nexport function stripePriceTransferFilename(price: StripePriceInterface): string {\n const seed = priceToSeed(price);\n const fromNickname = seed.nickname ? slugify(seed.nickname) : \"\";\n // Fallback keeps two exports of different prices from colliding when neither\n // is nicknamed. Both halves are slugified, so no separator can survive.\n const slug = fromNickname || slugify(`${seed.unitAmount}-${seed.currency}`);\n\n return `price-${slug || \"export\"}.json`;\n}\n\n/** Import path. Throws an Error when the text is not a valid document. */\nexport function parseStripePriceTransferDocument(raw: string): StripePriceTransferDocument {\n let candidate: unknown;\n try {\n candidate = JSON.parse(raw);\n } catch {\n throw new Error(\"The file is not valid JSON.\");\n }\n\n if (typeof candidate !== \"object\" || candidate === null || Array.isArray(candidate))\n throw new Error(\"The file is not a price transfer document.\");\n\n const document = candidate as Record<string, unknown>;\n\n if (document.kind !== STRIPE_PRICE_TRANSFER_KIND)\n throw new Error(\n `The file is not a price transfer document: expected kind \"${STRIPE_PRICE_TRANSFER_KIND}\", found ${JSON.stringify(document.kind)}.`,\n );\n\n if (typeof document.version !== \"number\" || !SUPPORTED_VERSIONS.includes(document.version))\n throw new Error(\n `Unsupported price transfer version ${JSON.stringify(document.version)}. Supported: ${SUPPORTED_VERSIONS.join(\", \")}.`,\n );\n\n // Unknown keys are stripped rather than rejected, so a document written by a\n // newer build of the same version still imports.\n const parsed = seedSchema.safeParse(document.price);\n if (!parsed.success) {\n const details = parsed.error.issues\n .map((issue) => `${issue.path.join(\".\") || \"price\"}: ${issue.message}`)\n .join(\"; \");\n throw new Error(`The price in the file is not valid. ${details}`);\n }\n\n return {\n kind: STRIPE_PRICE_TRANSFER_KIND,\n version: STRIPE_PRICE_TRANSFER_VERSION,\n price: parsed.data,\n };\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { RoundPageContainer } from \"../../../../../components\";\nimport { Modules } from \"../../../../../core\";\nimport { PriceProvider } from \"../../../contexts/PriceContext\";\nimport { useProductContext } from \"../../../contexts/ProductContext\";\nimport { PricesList } from \"../../../stripe-price/components/lists/PricesList\";\nimport { ProductDetails } from \"../details/ProductDetails\";\n\nexport function ProductContainer() {\n const t = useTranslations();\n const { product } = useProductContext();\n\n if (!product) return null;\n\n return (\n <RoundPageContainer\n module={Modules.StripeProduct}\n id={product.id}\n tabs={[\n {\n sectionKey: \"details\",\n label: t(\"billing.admin.products.tabs.details\"),\n content: <ProductDetails />,\n },\n {\n key: Modules.StripePrice,\n label: t(\"billing.admin.prices.title\"),\n // publishChrome={false}: SharedProvider REPLACES its value for the\n // subtree (SharedContext.tsx:35-37), so a nested provider publishing\n // chrome here would blank this page's own title.\n // No `fillHeight` and no `fullWidth` on the list: a ContentListTable in\n // a tab keeps its own bordered card and flows with the panel, exactly\n // as ThreatList does inside GoalContainer.\n content: (\n <PriceProvider productId={product.id} publishChrome={false}>\n <PricesList productId={product.id} />\n </PriceProvider>\n ),\n },\n ]}\n />\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { DetailField, SectionHeader } from \"../../../../../components\";\nimport { Badge } from \"../../../../../shadcnui\";\nimport { useProductContext } from \"../../../contexts/ProductContext\";\n\nexport function ProductDetails() {\n const t = useTranslations();\n const { product } = useProductContext();\n\n if (!product) return null;\n\n return (\n <div className=\"flex w-full flex-col gap-y-4\">\n <SectionHeader>{t(\"billing.admin.products.tabs.details\")}</SectionHeader>\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2\">\n <DetailField label={t(\"billing.admin.products.fields.name\")} value={product.name} />\n <DetailField\n label={t(\"billing.admin.products.fields.status\")}\n value={\n product.active ? (\n <Badge variant=\"softGreen\">{t(\"billing.admin.products.status.active\")}</Badge>\n ) : (\n <Badge variant=\"softGray\">{t(\"billing.admin.products.status.archived\")}</Badge>\n )\n }\n />\n <DetailField\n label={t(\"billing.admin.products.fields.description\")}\n value={product.description ?? \"—\"}\n className=\"md:col-span-2\"\n />\n <DetailField\n label={t(\"billing.admin.products.fields.prices\")}\n value={<span className=\"tabular-nums\">{product.stripePrices.length}</span>}\n />\n <DetailField\n label={t(\"billing.admin.products.fields.stripeId\")}\n value={<span className=\"tabular-nums\">{product.stripeProductId}</span>}\n />\n </div>\n </div>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { RoundPageContainer } from \"../../../../../components\";\nimport { Modules } from \"../../../../../core\";\nimport { CouponProvider } from \"../../../contexts/CouponContext\";\nimport { ProductProvider } from \"../../../contexts/ProductContext\";\nimport { CouponsList } from \"../../../stripe-coupon/components/lists/CouponsList\";\nimport { ProductsList } from \"../lists/ProductsList\";\n\n/**\n * Page container for the administrative products page: two tabs, Products and\n * Codes, with `sectionKey`s so the URL keeps the tab (`?section=codes`).\n *\n * Client component by necessity: `module={Modules.StripeProduct}` is a registry\n * entry carrying an icon component and methods, which a Server Component cannot\n * pass across the boundary. Routes mount this container instead — the same\n * shape as every other list page.\n *\n * No `fullWidth`, on the page or the lists: the page keeps its title bar and\n * each ContentListTable keeps its own bordered card in the tab (see ProductContainer). CouponProvider\n * is nested with `publishChrome={false}`: SharedProvider REPLACES its value for\n * the subtree (SharedContext.tsx:35-37), so a nested provider publishing chrome\n * would blank the products page's own title.\n */\nexport function ProductsListContainer() {\n const t = useTranslations();\n\n return (\n <ProductProvider>\n <CouponProvider publishChrome={false}>\n <RoundPageContainer\n module={Modules.StripeProduct}\n tabs={[\n { sectionKey: \"products\", label: t(\"billing.admin.products.tabs.products\"), content: <ProductsList /> },\n { sectionKey: \"codes\", label: t(\"billing.admin.products.tabs.codes\"), content: <CouponsList /> },\n ]}\n />\n </CouponProvider>\n </ProductProvider>\n );\n}\n","\"use client\";\n\nimport { TicketPercentIcon } from \"lucide-react\";\nimport { useTranslations } from \"next-intl\";\nimport { ReactNode, useEffect } from \"react\";\nimport { ContentListTable } from \"../../../../../components\";\nimport { Modules } from \"../../../../../core\";\nimport { DataListRetriever, useDataListRetriever } from \"../../../../../hooks\";\nimport { EmptyState } from \"../../../../../shadcnui\";\nimport { useCouponContext } from \"../../../contexts/CouponContext\";\nimport { StripeCouponFields } from \"../../data/stripe-coupon.fields\";\nimport { StripeCouponInterface } from \"../../data/stripe-coupon.interface\";\nimport { StripeCouponService } from \"../../data/stripe-coupon.service\";\nimport CouponEditor from \"../forms/CouponEditor\";\n\ntype CouponsListProps = {\n /**\n * Pass this whenever the list is the page's own content inside a\n * `RoundPageContainer fullWidth`. Without it ContentListTable wraps itself in\n * `rounded-md border`, drawing a second bordered card inside the page's\n * rounded shell.\n */\n fullWidth?: boolean;\n};\n\nexport function CouponsList({ fullWidth }: CouponsListProps = {}) {\n const t = useTranslations();\n const { catalogueVersion } = useCouponContext();\n\n const data: DataListRetriever<StripeCouponInterface> = useDataListRetriever({\n retriever: (params) => StripeCouponService.listCoupons(params),\n retrieverParams: {},\n module: Modules.StripeCoupon,\n });\n\n // Renames and code changes happen on the detail page, so the list has to\n // learn about them through the provider's counter rather than a direct callback.\n useEffect(() => {\n if (catalogueVersion > 0) void data.refresh();\n }, [catalogueVersion]);\n\n const functions: ReactNode[] = [<CouponEditor key=\"create-coupon\" />];\n\n return (\n <ContentListTable\n data={data}\n fields={[\n StripeCouponFields.name,\n StripeCouponFields.discount,\n StripeCouponFields.places,\n StripeCouponFields.codes,\n StripeCouponFields.status,\n ]}\n tableGeneratorType={Modules.StripeCoupon}\n functions={functions}\n fullWidth={fullWidth}\n title={t(\"billing.admin.coupons.title\")}\n emptyState={\n <EmptyState\n icon={TicketPercentIcon}\n title={t(\"billing.admin.coupons.empty.title\")}\n description={t(\"billing.admin.coupons.empty.description\")}\n />\n }\n />\n );\n}\n","\"use client\";\n\nimport { PackageIcon } from \"lucide-react\";\nimport { useTranslations } from \"next-intl\";\nimport { ReactNode, useEffect } from \"react\";\nimport { ContentListTable } from \"../../../../../components\";\nimport { Modules } from \"../../../../../core\";\nimport { DataListRetriever, useDataListRetriever } from \"../../../../../hooks\";\nimport { EmptyState } from \"../../../../../shadcnui\";\nimport { useProductContext } from \"../../../contexts/ProductContext\";\nimport { StripeProductFields } from \"../../data/stripe-product.fields\";\nimport { StripeProductInterface } from \"../../data/stripe-product.interface\";\nimport { StripeProductService } from \"../../data/stripe-product.service\";\nimport ProductEditor from \"../forms/ProductEditor\";\n\ntype ProductsListProps = {\n /**\n * Pass this whenever the list is the page's own content inside a\n * `RoundPageContainer fullWidth`. Without it ContentListTable wraps itself in\n * `rounded-md border`, drawing a second bordered card inside the page's\n * rounded shell.\n */\n fullWidth?: boolean;\n};\n\nexport function ProductsList({ fullWidth }: ProductsListProps = {}) {\n const t = useTranslations();\n const { catalogueVersion } = useProductContext();\n\n const data: DataListRetriever<StripeProductInterface> = useDataListRetriever({\n retriever: (params) => StripeProductService.listProducts(params),\n retrieverParams: {},\n module: Modules.StripeProduct,\n });\n\n // Archiving and restoring happen on the detail page, so the list has to learn\n // about them through the provider's counter rather than a direct callback.\n useEffect(() => {\n if (catalogueVersion > 0) void data.refresh();\n }, [catalogueVersion]);\n\n const functions: ReactNode[] = [<ProductEditor key=\"create-product\" />];\n\n return (\n <ContentListTable\n data={data}\n fields={[StripeProductFields.name, StripeProductFields.status, StripeProductFields.prices]}\n tableGeneratorType={Modules.StripeProduct}\n functions={functions}\n fullWidth={fullWidth}\n title={t(\"billing.admin.products.title\")}\n emptyState={\n <EmptyState\n icon={PackageIcon}\n title={t(\"billing.admin.products.empty.title\")}\n description={t(\"billing.admin.products.empty.description\")}\n />\n }\n />\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { RoundPageContainer } from \"../../../../../components\";\nimport { useCouponContext } from \"../../../contexts/CouponContext\";\nimport { CouponDetails } from \"../details/CouponDetails\";\nimport { CouponCodesList } from \"../lists/CouponCodesList\";\nimport { couponPageModule } from \"../../utils/coupon-page-module\";\n\nexport function CouponContainer() {\n const t = useTranslations();\n const { coupon } = useCouponContext();\n\n if (!coupon) return null;\n\n return (\n <RoundPageContainer\n module={couponPageModule()}\n id={coupon.id}\n tabs={[\n {\n sectionKey: \"details\",\n label: t(\"billing.admin.products.tabs.details\"),\n content: <CouponDetails />,\n },\n {\n sectionKey: \"codes\",\n label: t(\"billing.admin.coupons.codes.title\"),\n // No `fillHeight` and no `fullWidth` on the list: a ContentListTable in\n // a tab keeps its own bordered card and flows with the panel, exactly\n // as PricesList does inside ProductContainer. The codes list reads the\n // page's own CouponProvider, so no nested provider is needed.\n content: <CouponCodesList couponId={coupon.id} />,\n },\n ]}\n />\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { DetailField, SectionHeader } from \"../../../../../components\";\nimport { Badge } from \"../../../../../shadcnui\";\nimport { formatDate, useBillingFormatOptions } from \"../../../components/utils\";\nimport { useCouponContext } from \"../../../contexts/CouponContext\";\nimport { formatCouponDiscount } from \"../../utils/format-coupon-discount\";\n\nexport function CouponDetails() {\n const t = useTranslations();\n const fmt = useBillingFormatOptions();\n const { coupon } = useCouponContext();\n\n if (!coupon) return null;\n\n return (\n <div className=\"flex w-full flex-col gap-y-4\">\n <SectionHeader>{t(\"billing.admin.products.tabs.details\")}</SectionHeader>\n {/* Typography role 10 (helper text). */}\n <p className=\"text-muted-foreground text-xs/relaxed\">{t(\"billing.admin.coupons.fixed_notice\")}</p>\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2\">\n <DetailField label={t(\"billing.admin.coupons.fields.name\")} value={coupon.name} />\n <DetailField\n label={t(\"billing.admin.coupons.fields.status\")}\n value={\n coupon.valid ? (\n <Badge variant=\"softGreen\">{t(\"billing.admin.coupons.status.active\")}</Badge>\n ) : (\n <Badge variant=\"softGray\">{t(\"billing.admin.coupons.status.ended\")}</Badge>\n )\n }\n />\n <DetailField\n label={t(\"billing.admin.coupons.fields.discount\")}\n value={<span className=\"tabular-nums\">{formatCouponDiscount(coupon, t, fmt)}</span>}\n />\n <DetailField\n label={t(\"billing.admin.coupons.fields.places\")}\n value={\n <span className=\"tabular-nums\">\n {coupon.maxRedemptions !== undefined\n ? t(\"billing.admin.coupons.places.limited\", {\n used: String(coupon.timesRedeemed),\n limit: String(coupon.maxRedemptions),\n })\n : t(\"billing.admin.coupons.places.unlimited\", { used: String(coupon.timesRedeemed) })}\n </span>\n }\n />\n <DetailField\n label={t(\"billing.admin.coupons.fields.products\")}\n value={\n coupon.stripeProductIds.length > 0 ? (\n <span className=\"tabular-nums\">{coupon.stripeProductIds.join(\", \")}</span>\n ) : (\n t(\"billing.admin.coupons.all_products\")\n )\n }\n />\n <DetailField\n label={t(\"billing.admin.coupons.fields.redeemBy\")}\n value={<span className=\"tabular-nums\">{coupon.redeemBy ? formatDate(coupon.redeemBy, fmt) : \"—\"}</span>}\n />\n <DetailField\n label={t(\"billing.admin.coupons.fields.stripeId\")}\n value={<span className=\"tabular-nums\">{coupon.stripeCouponId}</span>}\n />\n </div>\n </div>\n );\n}\n","\"use client\";\n\nimport { TicketIcon } from \"lucide-react\";\nimport { useTranslations } from \"next-intl\";\nimport { ReactNode, useEffect } from \"react\";\nimport { ContentListTable } from \"../../../../../components\";\nimport { Modules } from \"../../../../../core\";\nimport { DataListRetriever, useDataListRetriever } from \"../../../../../hooks\";\nimport { EmptyState } from \"../../../../../shadcnui\";\nimport { useCouponContext } from \"../../../contexts/CouponContext\";\nimport { StripeCouponCodeFields } from \"../../data/stripe-coupon-code.fields\";\nimport { StripeCouponCodeInterface } from \"../../data/stripe-coupon-code.interface\";\nimport { StripeCouponCodeService } from \"../../data/stripe-coupon-code.service\";\nimport CouponCodeEditor from \"../forms/CouponCodeEditor\";\n\ntype CouponCodesListProps = {\n couponId: string;\n};\n\nexport function CouponCodesList({ couponId }: CouponCodesListProps) {\n const t = useTranslations();\n const { catalogueVersion } = useCouponContext();\n\n const data: DataListRetriever<StripeCouponCodeInterface> = useDataListRetriever({\n retriever: (params) => StripeCouponCodeService.listByCoupon({ ...params, couponId }),\n retrieverParams: {},\n module: Modules.StripeCouponCode,\n });\n\n // Switching \"Apply automatically\" on one code clears it on the others in the\n // same server write, so every flag change re-reads the whole list.\n useEffect(() => {\n if (catalogueVersion > 0) void data.refresh();\n }, [catalogueVersion]);\n\n const functions: ReactNode[] = [<CouponCodeEditor key=\"create-code\" couponId={couponId} />];\n\n return (\n <ContentListTable\n data={data}\n fields={[\n StripeCouponCodeFields.code,\n StripeCouponCodeFields.places,\n StripeCouponCodeFields.expiresAt,\n StripeCouponCodeFields.automatic,\n StripeCouponCodeFields.status,\n StripeCouponCodeFields.actions,\n ]}\n tableGeneratorType={Modules.StripeCouponCode}\n functions={functions}\n title={t(\"billing.admin.coupons.codes.title\")}\n emptyState={\n <EmptyState\n icon={TicketIcon}\n title={t(\"billing.admin.coupons.codes.empty.title\")}\n description={t(\"billing.admin.coupons.codes.empty.description\")}\n />\n }\n />\n );\n}\n","\"use client\";\n\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { useTranslations } from \"next-intl\";\nimport { ReactNode, useCallback, useMemo } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { v4 } from \"uuid\";\nimport { z } from \"zod\";\nimport { EditorSheet } from \"../../../../../components\";\nimport { Modules } from \"../../../../../core\";\nimport { useI18nRouter } from \"../../../../../i18n\";\nimport { useCouponContext } from \"../../../contexts/CouponContext\";\nimport { CouponCodeFields, couponCodeDefaults, couponCodeSchema, toCouponCodeInput } from \"./CouponCodeFields\";\n\nexport type CouponCodeEditorProps = {\n couponId: string;\n onSuccess?: () => void | Promise<void>;\n trigger?: ReactNode;\n forceShow?: boolean;\n onClose?: () => void;\n dialogOpen?: boolean;\n onDialogOpenChange?: (open: boolean) => void;\n};\n\n/**\n * Create-only: Stripe allows changing a promotion code's `active` flag and\n * little else, so a code is never edited here. \"Apply automatically\" and\n * deactivate / reactivate live on the row (CouponCodeActions).\n */\nfunction CouponCodeEditorInternal({\n couponId,\n onSuccess,\n trigger,\n forceShow,\n onClose,\n dialogOpen,\n onDialogOpenChange,\n}: CouponCodeEditorProps) {\n const t = useTranslations();\n const router = useI18nRouter();\n const { createCode } = useCouponContext();\n\n const formSchema = useMemo(() => z.object({ id: z.uuidv4() }).and(couponCodeSchema(t)), [t]);\n type CouponCodeEditorValues = z.infer<typeof formSchema>;\n\n const getDefaultValues = useCallback((): CouponCodeEditorValues => ({ id: v4(), ...couponCodeDefaults() }), []);\n\n const form = useForm<CouponCodeEditorValues>({\n resolver: zodResolver(formSchema),\n defaultValues: getDefaultValues(),\n });\n\n return (\n <EditorSheet\n form={form}\n entityType={t(\"billing.admin.coupons.codes.entity\")}\n isEdit={false}\n module={Modules.StripeCouponCode}\n size=\"md\"\n // A code has no page of its own: without onSuccess EditorSheet would\n // navigate to `<module pageUrl>/<codeId>`, which does not exist.\n // The context's createCode already reloads the coupon and the codes list.\n onSuccess={onSuccess ?? (() => {})}\n onNavigate={(url) => router.push(url)}\n onSubmit={async (values) => await createCode({ id: values.id, couponId, ...toCouponCodeInput(values) })}\n onReset={getDefaultValues}\n trigger={trigger}\n forceShow={forceShow}\n onClose={onClose}\n dialogOpen={dialogOpen}\n onDialogOpenChange={onDialogOpenChange}\n >\n <CouponCodeFields form={form} />\n </EditorSheet>\n );\n}\n\nexport default function CouponCodeEditor(props: CouponCodeEditorProps) {\n return <CouponCodeEditorInternal {...props} />;\n}\n"]}
1
+ {"version":3,"sources":["/home/runner/work/nextjs-jsonapi/nextjs-jsonapi/dist/billing/index.js","../../src/features/billing/components/cards/SubscriptionSummaryCard.tsx","../../src/features/billing/components/cards/PaymentMethodSummaryCard.tsx","../../src/features/billing/components/cards/CustomerInfoCard.tsx","../../src/features/billing/components/cards/InvoicesSummaryCard.tsx","../../src/features/billing/components/cards/BillingUsageSummaryCard.tsx","../../src/features/billing/components/containers/BillingDashboardContainer.tsx","../../src/features/billing/stripe-customer/components/containers/PaymentMethodsContainer.tsx","../../src/features/billing/stripe-customer/components/forms/PaymentMethodEditor.tsx","../../src/features/billing/stripe-customer/components/forms/PaymentMethodForm.tsx","../../src/features/billing/stripe-customer/components/details/PaymentMethodCard.tsx","../../src/features/billing/stripe-customer/components/lists/PaymentMethodsList.tsx","../../src/features/billing/stripe-invoice/components/containers/InvoicesContainer.tsx","../../src/features/billing/stripe-invoice/components/lists/InvoicesList.tsx","../../src/features/billing/stripe-invoice/components/details/InvoiceDetails.tsx","../../src/features/billing/stripe-invoice/components/widgets/InvoiceStatusBadge.tsx","../../src/features/billing/stripe-subscription/components/containers/SubscriptionsContainer.tsx","../../src/features/billing/stripe-subscription/components/lists/SubscriptionsList.tsx","../../src/features/billing/stripe-subscription/components/details/SubscriptionDetails.tsx","../../src/features/billing/stripe-subscription/components/forms/CancelSubscriptionDialog.tsx","../../src/features/billing/stripe-subscription/components/widgets/SubscriptionStatusBadge.tsx","../../src/features/billing/stripe-subscription/components/widgets/IntervalToggle.tsx","../../src/features/billing/stripe-subscription/components/widgets/PricingCard.tsx","../../src/features/billing/stripe-subscription/components/widgets/ProductPricingList.tsx","../../src/features/billing/stripe-subscription/components/widgets/ProductPricingRow.tsx","../../src/features/billing/stripe-subscription/components/widgets/ProrationPreview.tsx","../../src/features/billing/stripe-subscription/components/widgets/SubscriptionConfirmation.tsx","../../src/features/billing/stripe-subscription/components/wizards/SubscriptionWizard.tsx","../../src/features/billing/stripe-subscription/hooks/useSubscriptionWizard.ts","../../src/features/billing/stripe-subscription/hooks/wizard-promotion.ts","../../src/features/billing/stripe-subscription/hooks/useConfirmSubscriptionPayment.ts","../../src/features/billing/stripe-subscription/hooks/wizard-payment.ts","../../src/features/billing/stripe-subscription/components/wizards/WizardProgressIndicator.tsx","../../src/features/billing/stripe-subscription/components/wizards/WizardStepPlanSelection.tsx","../../src/features/billing/stripe-subscription/components/wizards/WizardStepReview.tsx","../../src/features/billing/stripe-promotion-code/components/PromoCodeInput.tsx","../../src/features/billing/stripe-subscription/components/wizards/review-card.ts","../../src/features/billing/stripe-subscription/components/wizards/WizardStepPaymentMethod.tsx","../../src/features/billing/stripe-usage/components/containers/UsageContainer.tsx","../../src/features/billing/stripe-usage/components/details/UsageSummaryCard.tsx","../../src/features/billing/stripe-usage/components/widgets/UsageSummaryCards.tsx","../../src/features/billing/stripe-usage/components/lists/UsageHistoryTable.tsx","../../src/features/billing/components/modals/BillingDetailModal.tsx","../../src/features/billing/components/utils/url.ts","../../src/features/billing/components/widgets/BillingAlertBanner.tsx","../../src/features/billing/components/providers/StripeProvider.tsx","../../src/features/billing/contexts/PriceContext.tsx","../../src/features/billing/stripe-price/components/forms/PriceArchiver.tsx","../../src/features/billing/stripe-price/components/forms/PriceEditor.tsx","../../src/features/billing/contexts/ProductContext.tsx","../../src/features/billing/stripe-product/components/forms/ProductArchiver.tsx","../../src/features/billing/stripe-product/components/forms/ProductEditor.tsx","../../src/features/billing/stripe-price/components/details/PriceDetails.tsx","../../src/features/billing/stripe-price/components/containers/PriceContainer.tsx","../../src/features/billing/stripe-price/components/lists/PricesList.tsx","../../src/features/billing/stripe-price/data/stripe-price.transfer.ts","../../src/features/billing/stripe-product/components/containers/ProductContainer.tsx","../../src/features/billing/stripe-product/components/details/ProductDetails.tsx","../../src/features/billing/stripe-product/components/containers/ProductsListContainer.tsx","../../src/features/billing/stripe-coupon/components/lists/CouponsList.tsx","../../src/features/billing/stripe-product/components/lists/ProductsList.tsx","../../src/features/billing/stripe-coupon/components/containers/CouponContainer.tsx","../../src/features/billing/stripe-coupon/components/details/CouponDetails.tsx","../../src/features/billing/stripe-coupon/components/lists/CouponCodesList.tsx","../../src/features/billing/stripe-coupon/components/forms/CouponCodeEditor.tsx"],"names":["useTranslations","getStatusBadgeVariant","useEffect","statusConfig","formatPlanName","hasMonthly","hasYearly","formatInterval","formatDate","hasMeteredSubscriptions","formSchema","z","defaultContextValue","read","document"],"mappings":"AAAA;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,uDAA6B;AAC7B;AACE;AACF,uDAA6B;AAC7B;AACE;AACF,uDAA6B;AAC7B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,uDAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B;AACE;AACF,uDAA6B;AAC7B;AACA;ACzHA,qCAAgC;AAChC,2CAAyC;AAmDjC,+CAAA;AAtCR,IAAM,sBAAA,EAAkC,MAAA,CAAO,MAAA,CAAO,mCAAkB,CAAA;AAExE,SAAS,qBAAA,CAAsB,MAAA,EAAiF;AAC9G,EAAA,OAAA,CAAQ,MAAA,EAAQ;AAAA,IACd,KAAA,qBAAA;AACE,MAAA,OAAO,SAAA;AAAA,IACT,KAAA,yBAAA;AACE,MAAA,OAAO,WAAA;AAAA,IACT,KAAA,yBAAA;AAAA,IACA,KAAA,qBAAA;AAAA,IACA,KAAA,yBAAA;AACE,MAAA,OAAO,aAAA;AAAA,IACT,OAAA;AACE,MAAA,OAAO,SAAA;AAAA,EACX;AACF;AAbS,qCAAA,qBAAA,EAAA,uBAAA,CAAA;AAeT,SAAS,cAAA,CAAe,YAAA,EAA2C,QAAA,EAA0B;AAC3F,EAAA,MAAM,YAAA,kBAAc,YAAA,qBAAa,KAAA,6BAAO,OAAA,6BAAS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AA;AAAgC,MAAA;AAChC,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AAEA,MAAA;AAA8B,IAAA;AAChC,EAAA;AAEJ;AA9CS;AAgDF;AACE,EAAA;AACT;AAFwB;AhEktJZ;AACA;A+D7vJsB;AAhBlB;AACJV,EAAAA;AACF,EAAA;AAEF,EAAA;AACJ,IAAA;AACA,IAAA;AACQ,IAAA;AACT,EAAA;AAIDE,EAAAA;AACM,IAAA;AACF,EAAA;AAEE,EAAA;AAGJ,EAAA;AAAC,IAAA;AAAA,IAAA;AACC,MAAA;AACA,MAAA;AAAQ,QAAA;AAAA,QAAA;AAAA,QAAA;AAAA,QAAA;AAAA,QAAA;AAAA,QAAA;AAOR,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACG,QAAA;AAAA,QAAA;AACC,UAAA;AACA,UAAA;AACA,UAAA;AAA8D,QAAA;AAChE,MAAA;AAAA,IAAA;AAEJ,EAAA;AAEJ;AAzCgB;A/DszJJ;AACA;A6DnzJO;AAdH;AACJF,EAAAA;AACF,EAAA;AAEH,EAAA;AAGH,EAAA;AAAC,IAAA;AAAA,IAAA;AACC,MAAA;AACI,MAAA;AACE,MAAA;AACJ,QAAA;AACE,UAAA;AACA,UAAA;AACA,UAAA;AACF,QAAA;AACA,QAAA;AACE,UAAA;AACA,UAAA;AAA4C;AAAA;AAAA;AAAA;AAK5C,UAAA;AACF,QAAA;AACF,MAAA;AAAA,IAAA;AACF,EAAA;AAEJ;AA5BgB;A7Dg2JJ;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/nextjs-jsonapi/nextjs-jsonapi/dist/billing/index.js","sourcesContent":[null,"\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { ChevronRight, CreditCard } from \"lucide-react\";\nimport { Badge, Button, Card, CardContent, CardHeader, CardTitle, Skeleton } from \"../../../../shadcnui\";\nimport { StripeSubscriptionInterface, SubscriptionStatus } from \"../../stripe-subscription\";\nimport { formatDate, useBillingFormatOptions } from \"../utils\";\n\ntype SubscriptionSummaryCardProps = {\n subscriptions: StripeSubscriptionInterface[];\n loading?: boolean;\n error?: string;\n onManageClick: () => void;\n};\n\n/** Statuses with a `billing.ui.subscription_status.*` label; anything else Stripe adds is shown raw. */\nconst SUBSCRIPTION_STATUSES: string[] = Object.values(SubscriptionStatus);\n\nfunction getStatusBadgeVariant(status: SubscriptionStatus): \"default\" | \"secondary\" | \"destructive\" | \"outline\" {\n switch (status) {\n case SubscriptionStatus.ACTIVE:\n return \"default\";\n case SubscriptionStatus.TRIALING:\n return \"secondary\";\n case SubscriptionStatus.PAST_DUE:\n case SubscriptionStatus.UNPAID:\n case SubscriptionStatus.CANCELED:\n return \"destructive\";\n default:\n return \"outline\";\n }\n}\n\nfunction formatPlanName(subscription: StripeSubscriptionInterface, fallback: string): string {\n const productName = subscription.price?.product?.name || \"\";\n const nickname = subscription.price?.nickname || \"\";\n\n if (productName && nickname) {\n return `${productName} - ${nickname}`;\n }\n return productName || nickname || fallback;\n}\n\nexport function SubscriptionSummaryCard({\n subscriptions,\n loading,\n error,\n onManageClick,\n}: SubscriptionSummaryCardProps) {\n const t = useTranslations();\n const fmt = useBillingFormatOptions();\n if (loading) {\n return (\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle>{t(\"billing.ui.subscriptions.title\")}</CardTitle>\n <CreditCard className=\"h-4 w-4 text-muted-foreground\" />\n </CardHeader>\n <CardContent>\n <Skeleton className=\"h-6 w-32 mb-2\" />\n <Skeleton className=\"h-4 w-24 mb-1\" />\n <Skeleton className=\"h-4 w-40\" />\n </CardContent>\n </Card>\n );\n }\n\n if (error) {\n return (\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle>{t(\"billing.ui.subscriptions.title\")}</CardTitle>\n <CreditCard className=\"h-4 w-4 text-muted-foreground\" />\n </CardHeader>\n <CardContent>\n <p className=\"text-destructive text-xs/relaxed\">{error}</p>\n </CardContent>\n </Card>\n );\n }\n\n // A failing card (past_due) still counts as the GM's plan: they keep it during the grace period.\n const activeSubscriptions = subscriptions.filter(\n (sub) =>\n sub.status === SubscriptionStatus.ACTIVE ||\n sub.status === SubscriptionStatus.TRIALING ||\n sub.status === SubscriptionStatus.PAST_DUE,\n );\n const primarySubscription = activeSubscriptions[0];\n\n return (\n <Card className=\"cursor-pointer hover:bg-accent/50 transition-colors\" onClick={onManageClick}>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle>{t(\"billing.ui.subscriptions.title\")}</CardTitle>\n <CreditCard className=\"h-4 w-4 text-muted-foreground\" />\n </CardHeader>\n <CardContent>\n {/* No active or trialing plan (none at all, or only unpaid / ended ones): offer the plans */}\n {!primarySubscription ? (\n <div className=\"space-y-2\">\n <p className=\"text-xl font-bold text-muted-foreground\">{t(\"billing.ui.cards.subscriptions.empty_title\")}</p>\n <p className=\"text-xs text-muted-foreground\">{t(\"billing.ui.cards.subscriptions.empty_description\")}</p>\n <Button\n variant=\"outline\"\n size=\"sm\"\n className=\"mt-2\"\n onClick={(e) => {\n e.stopPropagation();\n onManageClick();\n }}\n >\n {t(\"billing.ui.cards.subscriptions.view_plans\")}\n <ChevronRight className=\"h-4 w-4 ms-1 rtl:rotate-180\" />\n </Button>\n </div>\n ) : (\n <div className=\"space-y-2\">\n <div className=\"flex items-center gap-2\">\n <p className=\"text-xl font-bold\">\n {formatPlanName(primarySubscription, t(\"billing.ui.cards.subscriptions.fallback_name\"))}\n </p>\n {primarySubscription.status === SubscriptionStatus.PAST_DUE ? (\n <Badge variant=\"softRed\">{t(\"billing.ui.cards.subscriptions.payment_failing\")}</Badge>\n ) : (\n <Badge\n variant={\n primarySubscription.cancelAtPeriodEnd\n ? \"secondary\"\n : getStatusBadgeVariant(primarySubscription.status)\n }\n >\n {primarySubscription.cancelAtPeriodEnd\n ? t(\"billing.ui.cards.subscriptions.canceling\")\n : SUBSCRIPTION_STATUSES.includes(primarySubscription.status)\n ? t(`billing.ui.subscription_status.${primarySubscription.status}`)\n : primarySubscription.status}\n </Badge>\n )}\n </div>\n <p className=\"text-xs text-muted-foreground\">\n {!primarySubscription.price?.isTrial\n ? t(\"billing.ui.cards.subscriptions.ends_on\", {\n date: formatDate(primarySubscription.currentPeriodEnd, fmt),\n })\n : primarySubscription.cancelAtPeriodEnd\n ? t(\"billing.ui.cards.subscriptions.cancels_on\", {\n date: formatDate(primarySubscription.currentPeriodEnd, fmt),\n })\n : t(\"billing.ui.cards.subscriptions.renews_on\", {\n date: formatDate(primarySubscription.currentPeriodEnd, fmt),\n })}\n </p>\n {activeSubscriptions.length > 1 && (\n <p className=\"text-xs text-muted-foreground\">\n {t(\"billing.ui.cards.subscriptions.more\", { count: activeSubscriptions.length - 1 })}\n </p>\n )}\n </div>\n )}\n </CardContent>\n </Card>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { Wallet, ChevronRight } from \"lucide-react\";\nimport { Card, CardContent, CardHeader, CardTitle, Button, Skeleton } from \"../../../../shadcnui\";\nimport { PaymentMethodInterface } from \"../../stripe-customer\";\n\ntype PaymentMethodSummaryCardProps = {\n paymentMethods: PaymentMethodInterface[];\n defaultPaymentMethodId?: string;\n loading?: boolean;\n error?: string;\n onManageClick: () => void;\n};\n\nfunction getCardBrandIcon(brand: string): string {\n const brandMap: Record<string, string> = {\n visa: \"Visa\",\n mastercard: \"Mastercard\",\n amex: \"Amex\",\n discover: \"Discover\",\n diners: \"Diners\",\n jcb: \"JCB\",\n unionpay: \"UnionPay\",\n };\n return brandMap[brand.toLowerCase()] || brand;\n}\n\nexport function PaymentMethodSummaryCard({\n paymentMethods,\n defaultPaymentMethodId,\n loading,\n error,\n onManageClick,\n}: PaymentMethodSummaryCardProps) {\n const t = useTranslations();\n if (loading) {\n return (\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle>{t(\"billing.ui.payment_method.title\")}</CardTitle>\n <Wallet className=\"h-4 w-4 text-muted-foreground\" />\n </CardHeader>\n <CardContent>\n <Skeleton className=\"h-6 w-32 mb-2\" />\n <Skeleton className=\"h-4 w-24\" />\n </CardContent>\n </Card>\n );\n }\n\n if (error) {\n return (\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle>{t(\"billing.ui.payment_method.title\")}</CardTitle>\n <Wallet className=\"h-4 w-4 text-muted-foreground\" />\n </CardHeader>\n <CardContent>\n <p className=\"text-destructive text-xs/relaxed\">{error}</p>\n </CardContent>\n </Card>\n );\n }\n\n // Find default payment method or use first one\n const defaultMethod = paymentMethods.find((pm) => pm.id === defaultPaymentMethodId) || paymentMethods[0];\n\n return (\n <Card className=\"cursor-pointer hover:bg-accent/50 transition-colors\" onClick={onManageClick}>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle>{t(\"billing.ui.payment_method.title\")}</CardTitle>\n <Wallet className=\"h-4 w-4 text-muted-foreground\" />\n </CardHeader>\n <CardContent>\n {paymentMethods.length === 0 ? (\n <div className=\"space-y-2\">\n <p className=\"text-xl font-bold text-muted-foreground\">\n {t(\"billing.ui.cards.payment_method.empty_title\")}\n </p>\n <p className=\"text-xs text-muted-foreground\">{t(\"billing.ui.cards.payment_method.empty_description\")}</p>\n <Button\n variant=\"outline\"\n size=\"sm\"\n className=\"mt-2\"\n onClick={(e) => {\n e.stopPropagation();\n onManageClick();\n }}\n >\n {t(\"billing.ui.cards.payment_method.add_card\")}\n <ChevronRight className=\"h-4 w-4 ms-1 rtl:rotate-180\" />\n </Button>\n </div>\n ) : defaultMethod?.card ? (\n <div className=\"space-y-2\">\n <p className=\"text-xl font-bold\">\n {getCardBrandIcon(defaultMethod.card.brand)} ****{defaultMethod.card.last4}\n </p>\n <p className=\"text-sm text-muted-foreground\">\n {t(\"billing.ui.payment_method.expires\", {\n date: `${String(defaultMethod.card.expMonth).padStart(2, \"0\")}/${defaultMethod.card.expYear}`,\n })}\n </p>\n {paymentMethods.length > 1 && (\n <p className=\"text-xs text-muted-foreground\">\n {t(\"billing.ui.cards.payment_method.more_cards\", { count: paymentMethods.length - 1 })}\n </p>\n )}\n </div>\n ) : (\n <div className=\"space-y-2\">\n <p className=\"text-xl font-bold\">{defaultMethod?.type || t(\"billing.ui.payment_method.title\")}</p>\n {paymentMethods.length > 1 && (\n <p className=\"text-xs text-muted-foreground\">\n {t(\"billing.ui.cards.payment_method.more_methods\", { count: paymentMethods.length - 1 })}\n </p>\n )}\n </div>\n )}\n </CardContent>\n </Card>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { ExternalLink, User } from \"lucide-react\";\nimport { useState } from \"react\";\nimport { Button, Card, CardContent, CardHeader, CardTitle, Skeleton } from \"../../../../shadcnui\";\nimport { useI18nLocale } from \"../../../../i18n\";\nimport { StripeCustomerInterface, StripeCustomerService } from \"../../stripe-customer\";\n\ntype CustomerInfoCardProps = {\n customer: StripeCustomerInterface | null;\n loading?: boolean;\n error?: string;\n};\n\nfunction formatBalance(balance: number | undefined, currency: string | undefined, locale: string): string {\n const currencyCode = currency?.toUpperCase() || \"USD\";\n // Balance in Stripe is negative when customer has credit\n const displayBalance = balance ? -balance : 0;\n return new Intl.NumberFormat(locale, {\n style: \"currency\",\n currency: currencyCode,\n }).format(displayBalance / 100);\n}\n\nexport function CustomerInfoCard({ customer, loading, error }: CustomerInfoCardProps) {\n const t = useTranslations();\n const locale = useI18nLocale();\n const [portalLoading, setPortalLoading] = useState(false);\n\n const handlePortalClick = async (e: React.MouseEvent) => {\n e.stopPropagation();\n setPortalLoading(true);\n try {\n const { url } = await StripeCustomerService.createPortalSession();\n window.open(url, \"_blank\");\n } catch (err) {\n console.error(\"[CustomerInfoCard] Failed to create portal session:\", err);\n } finally {\n setPortalLoading(false);\n }\n };\n\n if (loading) {\n return (\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle>{t(\"billing.ui.cards.customer.title\")}</CardTitle>\n <User className=\"h-4 w-4 text-muted-foreground\" />\n </CardHeader>\n <CardContent>\n <Skeleton className=\"h-6 w-32 mb-2\" />\n <Skeleton className=\"h-4 w-48 mb-1\" />\n <Skeleton className=\"h-4 w-24\" />\n </CardContent>\n </Card>\n );\n }\n\n if (error) {\n return (\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle>{t(\"billing.ui.cards.customer.title\")}</CardTitle>\n <User className=\"h-4 w-4 text-muted-foreground\" />\n </CardHeader>\n <CardContent>\n <p className=\"text-destructive text-xs/relaxed\">{error}</p>\n </CardContent>\n </Card>\n );\n }\n\n if (!customer) {\n return (\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle>{t(\"billing.ui.cards.customer.title\")}</CardTitle>\n <User className=\"h-4 w-4 text-muted-foreground\" />\n </CardHeader>\n <CardContent>\n <p className=\"text-xl font-bold text-muted-foreground\">{t(\"billing.ui.cards.customer.not_set_up\")}</p>\n <p className=\"text-xs text-muted-foreground\">{t(\"billing.ui.cards.customer.not_set_up_description\")}</p>\n </CardContent>\n </Card>\n );\n }\n\n return (\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle>{t(\"billing.ui.cards.customer.title\")}</CardTitle>\n <User className=\"h-4 w-4 text-muted-foreground\" />\n </CardHeader>\n <CardContent>\n <div className=\"space-y-2\">\n {customer.name && <p className=\"text-xl font-bold\">{customer.name}</p>}\n {customer.email && <p className=\"text-sm text-muted-foreground\">{customer.email}</p>}\n {customer.balance !== undefined && customer.balance !== 0 && (\n <p className=\"text-sm\">\n <span className=\"text-muted-foreground\">{t(\"billing.ui.cards.customer.credit_balance\")} </span>\n <span className={customer.balance < 0 ? \"text-success\" : \"text-destructive\"}>\n {formatBalance(customer.balance, customer.currency, locale)}\n </span>\n </p>\n )}\n <Button variant=\"outline\" size=\"sm\" className=\"mt-2\" onClick={handlePortalClick} disabled={portalLoading}>\n {portalLoading ? t(\"common.loading\") : t(\"billing.ui.cards.customer.manage_portal\")}\n <ExternalLink className=\"h-4 w-4 ms-1\" />\n </Button>\n </div>\n </CardContent>\n </Card>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { ChevronRight, ReceiptIcon } from \"lucide-react\";\nimport { Badge, Card, CardContent, CardHeader, CardTitle, Skeleton } from \"../../../../shadcnui\";\nimport { InvoiceStatus, StripeInvoiceInterface } from \"../../stripe-invoice\";\nimport { formatDate, useBillingFormatOptions } from \"../utils\";\n\ntype InvoicesSummaryCardProps = {\n invoices: StripeInvoiceInterface[];\n loading?: boolean;\n error?: string;\n onViewAllClick: () => void;\n};\n\n/** Statuses with a `billing.ui.invoice_status.*` label; anything else Stripe adds is shown raw. */\nconst INVOICE_STATUSES: string[] = Object.values(InvoiceStatus);\n\nfunction getStatusBadgeVariant(status: InvoiceStatus): \"default\" | \"secondary\" | \"destructive\" | \"outline\" {\n switch (status) {\n case InvoiceStatus.PAID:\n return \"default\";\n case InvoiceStatus.OPEN:\n return \"secondary\";\n case InvoiceStatus.UNCOLLECTIBLE:\n case InvoiceStatus.VOID:\n return \"destructive\";\n default:\n return \"outline\";\n }\n}\n\nfunction formatAmount(amount: number, currency: string, locale: string): string {\n const currencyCode = currency?.toUpperCase() || \"USD\";\n return new Intl.NumberFormat(locale, {\n style: \"currency\",\n currency: currencyCode,\n }).format(amount / 100);\n}\n\nexport function InvoicesSummaryCard({ invoices, loading, error, onViewAllClick }: InvoicesSummaryCardProps) {\n const t = useTranslations();\n const fmt = useBillingFormatOptions();\n if (loading) {\n return (\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle>{t(\"billing.ui.cards.invoices.title\")}</CardTitle>\n <ReceiptIcon className=\"h-4 w-4 text-muted-foreground\" />\n </CardHeader>\n <CardContent>\n <Skeleton className=\"h-6 w-24 mb-2\" />\n <Skeleton className=\"h-4 w-32 mb-1\" />\n <Skeleton className=\"h-4 w-20\" />\n </CardContent>\n </Card>\n );\n }\n\n if (error) {\n return (\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle>{t(\"billing.ui.cards.invoices.title\")}</CardTitle>\n <ReceiptIcon className=\"h-4 w-4 text-muted-foreground\" />\n </CardHeader>\n <CardContent>\n <p className=\"text-destructive text-xs/relaxed\">{error}</p>\n </CardContent>\n </Card>\n );\n }\n\n // The most recent invoice that could charge something: a void one is an unpaid first-payment\n // attempt that was replaced, which never charged the GM (fall back to it only when nothing else exists)\n const latestInvoice = invoices.find((inv) => inv.status !== InvoiceStatus.VOID) ?? invoices[0];\n const paidInvoices = invoices.filter((inv) => inv.status === InvoiceStatus.PAID);\n const openInvoices = invoices.filter((inv) => inv.status === InvoiceStatus.OPEN);\n\n return (\n <Card className=\"cursor-pointer hover:bg-accent/50 transition-colors\" onClick={onViewAllClick}>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle>{t(\"billing.ui.cards.invoices.title\")}</CardTitle>\n <ReceiptIcon className=\"h-4 w-4 text-muted-foreground\" />\n </CardHeader>\n <CardContent>\n {invoices.length === 0 ? (\n <div className=\"space-y-2\">\n <p className=\"text-xl font-bold text-muted-foreground\">{t(\"billing.ui.invoices.empty_title\")}</p>\n <p className=\"text-xs text-muted-foreground\">{t(\"billing.ui.cards.invoices.empty_description\")}</p>\n </div>\n ) : latestInvoice ? (\n <div className=\"space-y-2\">\n <div className=\"flex items-center gap-2\">\n <p className=\"text-xl font-bold\">\n {formatAmount(latestInvoice.total, latestInvoice.currency, fmt.locale)}\n </p>\n <Badge variant={getStatusBadgeVariant(latestInvoice.status)}>\n {INVOICE_STATUSES.includes(latestInvoice.status)\n ? t(`billing.ui.invoice_status.${latestInvoice.status}`)\n : latestInvoice.status}\n </Badge>\n </div>\n <p className=\"text-sm text-muted-foreground\">\n {latestInvoice.stripeInvoiceNumber ||\n t(\"billing.ui.cards.invoices.invoice_from\", { date: formatDate(latestInvoice.periodStart, fmt) })}\n </p>\n <div className=\"flex items-center gap-4 text-xs text-muted-foreground\">\n {paidInvoices.length > 0 && (\n <span>{t(\"billing.ui.cards.invoices.paid_count\", { count: paidInvoices.length })}</span>\n )}\n {openInvoices.length > 0 && (\n <span className=\"text-warning\">\n {t(\"billing.ui.cards.invoices.open_count\", { count: openInvoices.length })}\n </span>\n )}\n <span className=\"flex items-center\">\n {t(\"billing.ui.cards.view_all\")}\n <ChevronRight className=\"h-3 w-3 ms-1 rtl:rotate-180\" />\n </span>\n </div>\n </div>\n ) : null}\n </CardContent>\n </Card>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { Activity, ChevronRight } from \"lucide-react\";\nimport { Card, CardContent, CardHeader, CardTitle, Skeleton } from \"../../../../shadcnui\";\nimport { useI18nLocale } from \"../../../../i18n\";\nimport { MeterInterface, MeterSummaryInterface } from \"../../stripe-usage\";\n\ntype BillingUsageSummaryCardProps = {\n meters: MeterInterface[];\n summaries: Record<string, MeterSummaryInterface | null>;\n loading?: boolean;\n error?: string;\n onViewDetailsClick: () => void;\n};\n\nfunction formatNumber(value: number, locale: string): string {\n return new Intl.NumberFormat(locale, {\n notation: \"compact\",\n compactDisplay: \"short\",\n }).format(value);\n}\n\nexport function BillingUsageSummaryCard({\n meters,\n summaries,\n loading,\n error,\n onViewDetailsClick,\n}: BillingUsageSummaryCardProps) {\n const t = useTranslations();\n const locale = useI18nLocale();\n if (loading) {\n return (\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle>{t(\"billing.ui.cards.usage.title\")}</CardTitle>\n <Activity className=\"h-4 w-4 text-muted-foreground\" />\n </CardHeader>\n <CardContent>\n <Skeleton className=\"h-6 w-24 mb-2\" />\n <Skeleton className=\"h-4 w-32\" />\n </CardContent>\n </Card>\n );\n }\n\n if (error) {\n return (\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle>{t(\"billing.ui.cards.usage.title\")}</CardTitle>\n <Activity className=\"h-4 w-4 text-muted-foreground\" />\n </CardHeader>\n <CardContent>\n <p className=\"text-destructive text-xs/relaxed\">{error}</p>\n </CardContent>\n </Card>\n );\n }\n\n // Calculate total usage across all meters\n const totalUsage = Object.values(summaries).reduce((acc, summary) => {\n return acc + (summary?.aggregatedValue || 0);\n }, 0);\n\n // Get first meter with data for display\n const primaryMeter = meters.find((m) => summaries[m.id]?.aggregatedValue);\n const primarySummary = primaryMeter ? summaries[primaryMeter.id] : null;\n\n return (\n <Card className=\"cursor-pointer hover:bg-accent/50 transition-colors\" onClick={onViewDetailsClick}>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle>{t(\"billing.ui.cards.usage.title\")}</CardTitle>\n <Activity className=\"h-4 w-4 text-muted-foreground\" />\n </CardHeader>\n <CardContent>\n {meters.length === 0 ? (\n <div className=\"space-y-2\">\n <p className=\"text-xl font-bold text-muted-foreground\">{t(\"billing.ui.cards.usage.no_meters\")}</p>\n <p className=\"text-xs text-muted-foreground\">{t(\"billing.ui.cards.usage.no_meters_description\")}</p>\n </div>\n ) : (\n <div className=\"space-y-2\">\n <p className=\"text-xl font-bold\">\n {t(\"billing.ui.cards.usage.units\", { value: formatNumber(totalUsage, locale) })}\n </p>\n {primaryMeter && primarySummary && (\n <p className=\"text-sm text-muted-foreground\">\n {primaryMeter.displayName}: {formatNumber(primarySummary.aggregatedValue, locale)}\n </p>\n )}\n {meters.length > 1 && (\n <p className=\"text-xs text-muted-foreground\">\n {t(\"billing.ui.cards.usage.across_meters\", { count: meters.length })}\n </p>\n )}\n <span className=\"flex items-center text-xs text-muted-foreground\">\n {t(\"billing.ui.cards.view_details\")}\n <ChevronRight className=\"h-3 w-3 ms-1 rtl:rotate-180\" />\n </span>\n </div>\n )}\n </CardContent>\n </Card>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { CreditCard, Loader2, Wallet } from \"lucide-react\";\nimport { useSearchParams } from \"next/navigation\";\nimport { useCallback, useEffect, useMemo, useState } from \"react\";\nimport { Button, Card, CardContent, CardDescription, CardHeader, CardTitle } from \"../../../../shadcnui\";\nimport { PaymentMethodInterface, StripeCustomerInterface, StripeCustomerService } from \"../../stripe-customer\";\nimport { PaymentMethodsContainer } from \"../../stripe-customer/components\";\nimport { StripeInvoiceInterface, StripeInvoiceService } from \"../../stripe-invoice\";\nimport { InvoicesContainer } from \"../../stripe-invoice/components\";\nimport { StripeSubscriptionInterface, StripeSubscriptionService, SubscriptionStatus } from \"../../stripe-subscription\";\nimport { SubscriptionsContainer } from \"../../stripe-subscription/components\";\nimport { SubscriptionWizard } from \"../../stripe-subscription/components/wizards\";\nimport { MeterInterface, MeterSummaryInterface, StripeUsageService } from \"../../stripe-usage\";\nimport { UsageContainer } from \"../../stripe-usage/components\";\nimport {\n BillingUsageSummaryCard,\n CustomerInfoCard,\n InvoicesSummaryCard,\n PaymentMethodSummaryCard,\n SubscriptionSummaryCard,\n} from \"../cards\";\nimport { BillingDetailModal } from \"../modals/BillingDetailModal\";\nimport { urlWithoutParam } from \"../utils/url\";\nimport { BillingAlertBanner } from \"../widgets/BillingAlertBanner\";\n\ntype ModalType = \"subscriptions\" | \"payment-methods\" | \"invoices\" | \"usage\" | null;\n\ntype DataState = {\n customer: StripeCustomerInterface | null;\n subscriptions: StripeSubscriptionInterface[];\n paymentMethods: PaymentMethodInterface[];\n invoices: StripeInvoiceInterface[];\n meters: MeterInterface[];\n meterSummaries: Record<string, MeterSummaryInterface | null>;\n};\n\ntype LoadingState = {\n customer: boolean;\n subscriptions: boolean;\n paymentMethods: boolean;\n invoices: boolean;\n usage: boolean;\n};\n\ntype ErrorState = {\n customer: string | null;\n subscriptions: string | null;\n paymentMethods: string | null;\n invoices: string | null;\n usage: string | null;\n};\n\nexport function BillingDashboardContainer() {\n const t = useTranslations();\n const [data, setData] = useState<DataState>({\n customer: null,\n subscriptions: [],\n paymentMethods: [],\n invoices: [],\n meters: [],\n meterSummaries: {},\n });\n\n const [loading, setLoading] = useState<LoadingState>({\n customer: true,\n subscriptions: true,\n paymentMethods: true,\n invoices: true,\n usage: true,\n });\n\n // Error state holds message keys; they are translated at render time.\n const [errors, setErrors] = useState<ErrorState>({\n customer: null,\n subscriptions: null,\n paymentMethods: null,\n invoices: null,\n usage: null,\n });\n\n const [activeModal, setActiveModal] = useState<ModalType>(null);\n const [noCustomerExists, setNoCustomerExists] = useState(false);\n const [creatingCustomer, setCreatingCustomer] = useState(false);\n const searchParams = useSearchParams();\n\n // Wizard state - lifted from SubscriptionsContainer to avoid nested dialogs\n const [showWizard, setShowWizard] = useState(false);\n const [editingSubscription, setEditingSubscription] = useState<StripeSubscriptionInterface | null>(null);\n\n // Check if company has metered subscriptions\n const hasMeteredSubscriptions = useCallback((): boolean => {\n return data.subscriptions.some((sub) => sub.price?.recurring?.usageType === \"metered\");\n }, [data.subscriptions]);\n\n // Check if user has active recurring subscription (for wizard filtering)\n const hasActiveRecurringSubscription = useMemo(() => {\n return data.subscriptions.some(\n (sub) =>\n (sub.status === SubscriptionStatus.ACTIVE || sub.status === SubscriptionStatus.TRIALING) &&\n sub.price?.priceType === \"recurring\",\n );\n }, [data.subscriptions]);\n\n // Fetch all data - first check customer, then fetch rest if customer exists\n const fetchAllData = useCallback(async () => {\n setNoCustomerExists(false);\n\n // First, try to fetch customer\n let customer: StripeCustomerInterface | null = null;\n try {\n customer = await StripeCustomerService.getCustomer();\n setData((prev) => ({ ...prev, customer }));\n setErrors((prev) => ({ ...prev, customer: null }));\n setNoCustomerExists(false);\n } catch (error: unknown) {\n console.error(\"[BillingDashboard] Failed to load customer:\", error);\n // A 404 means the company has no billing customer yet - a setup prompt, not an error.\n // Match on the status, never on the message: `statusText` is empty over HTTP/2, so the\n // error text carries no reason phrase to match against.\n if ((error as { status?: number })?.status === 404) {\n setNoCustomerExists(true);\n } else {\n setErrors((prev) => ({ ...prev, customer: \"billing.ui.dashboard.errors.load_customer\" }));\n }\n } finally {\n setLoading((prev) => ({ ...prev, customer: false }));\n }\n\n // Without a customer there is nothing downstream to fetch. Release every section here, or the\n // cards that are only cleared inside their own fetchers stay skeletons forever.\n if (!customer) {\n setLoading({\n customer: false,\n subscriptions: false,\n paymentMethods: false,\n invoices: false,\n usage: false,\n });\n return;\n }\n\n // Fetch subscriptions\n const fetchSubscriptions = async () => {\n try {\n const subscriptions = await StripeSubscriptionService.listSubscriptions();\n setData((prev) => ({ ...prev, subscriptions }));\n setErrors((prev) => ({ ...prev, subscriptions: null }));\n return subscriptions;\n } catch (error) {\n console.error(\"[BillingDashboard] Failed to load subscriptions:\", error);\n setErrors((prev) => ({ ...prev, subscriptions: \"billing.ui.dashboard.errors.load_subscriptions\" }));\n return [];\n } finally {\n setLoading((prev) => ({ ...prev, subscriptions: false }));\n }\n };\n\n // Fetch payment methods\n const fetchPaymentMethods = async () => {\n try {\n const paymentMethods = await StripeCustomerService.listPaymentMethods();\n setData((prev) => ({ ...prev, paymentMethods }));\n setErrors((prev) => ({ ...prev, paymentMethods: null }));\n } catch (error) {\n console.error(\"[BillingDashboard] Failed to load payment methods:\", error);\n setErrors((prev) => ({ ...prev, paymentMethods: \"billing.ui.dashboard.errors.load_payment_methods\" }));\n } finally {\n setLoading((prev) => ({ ...prev, paymentMethods: false }));\n }\n };\n\n // Fetch invoices\n const fetchInvoices = async () => {\n try {\n const invoices = await StripeInvoiceService.listInvoices();\n setData((prev) => ({ ...prev, invoices }));\n setErrors((prev) => ({ ...prev, invoices: null }));\n } catch (error) {\n console.error(\"[BillingDashboard] Failed to load invoices:\", error);\n setErrors((prev) => ({ ...prev, invoices: \"billing.ui.dashboard.errors.load_invoices\" }));\n } finally {\n setLoading((prev) => ({ ...prev, invoices: false }));\n }\n };\n\n // Execute all in parallel\n const [subscriptions] = await Promise.all([fetchSubscriptions(), fetchPaymentMethods(), fetchInvoices()]);\n\n // Check if there are metered subscriptions and fetch usage data\n const hasMetered = subscriptions.some(\n (sub: StripeSubscriptionInterface) => sub.price?.recurring?.usageType === \"metered\",\n );\n\n if (hasMetered) {\n await fetchUsageData();\n } else {\n setLoading((prev) => ({ ...prev, usage: false }));\n }\n }, []);\n\n // Create a new Stripe customer for the company\n const handleCreateCustomer = async () => {\n setCreatingCustomer(true);\n try {\n await StripeCustomerService.createCustomer();\n setNoCustomerExists(false);\n // Refresh all data after customer creation\n await fetchAllData();\n } catch (error) {\n console.error(\"[BillingDashboard] Failed to create customer:\", error);\n setErrors((prev) => ({ ...prev, customer: \"billing.ui.dashboard.errors.setup\" }));\n } finally {\n setCreatingCustomer(false);\n }\n };\n\n // Fetch usage data (called conditionally)\n const fetchUsageData = async () => {\n try {\n const meters = await StripeUsageService.listMeters();\n setData((prev) => ({ ...prev, meters }));\n\n // Load summaries for each meter (current month)\n const summariesMap: Record<string, MeterSummaryInterface | null> = {};\n const now = new Date();\n const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);\n const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59, 999);\n\n for (const meter of meters) {\n try {\n const meterSummaries = await StripeUsageService.getMeterSummaries({\n meterId: meter.id,\n startTime: startOfMonth,\n endTime: endOfMonth,\n });\n summariesMap[meter.id] = meterSummaries.length > 0 ? meterSummaries[0] : null;\n } catch (error) {\n console.error(`[BillingDashboard] Failed to load summaries for meter ${meter.id}:`, error);\n summariesMap[meter.id] = null;\n }\n }\n\n setData((prev) => ({ ...prev, meterSummaries: summariesMap }));\n setErrors((prev) => ({ ...prev, usage: null }));\n } catch (error) {\n console.error(\"[BillingDashboard] Failed to load usage data:\", error);\n setErrors((prev) => ({ ...prev, usage: \"billing.ui.dashboard.errors.load_usage\" }));\n } finally {\n setLoading((prev) => ({ ...prev, usage: false }));\n }\n };\n\n // Refresh data after modal actions\n const refreshData = useCallback(async () => {\n // Reset loading states for a refresh\n setLoading({\n customer: true,\n subscriptions: true,\n paymentMethods: true,\n invoices: true,\n usage: true,\n });\n await fetchAllData();\n }, [fetchAllData]);\n\n // Handler to open wizard (can be called from SubscriptionsContainer)\n const handleOpenWizard = useCallback((subscription?: StripeSubscriptionInterface) => {\n setEditingSubscription(subscription || null);\n setShowWizard(true);\n }, []);\n\n // Handler to close wizard\n const handleWizardClose = useCallback(() => {\n setShowWizard(false);\n setEditingSubscription(null);\n refreshData();\n }, [refreshData]);\n\n // Initial data load\n useEffect(() => {\n fetchAllData();\n }, [fetchAllData]);\n\n // Handle URL action parameter for deep linking. `?action=subscribe` records the\n // intent to subscribe; the wizard itself only renders once a customer exists, so a\n // company without one sees the setup card first and the wizard opens right after\n // \"Set Up Billing Account\" succeeds.\n useEffect(() => {\n const action = searchParams.get(\"action\");\n if (action === \"subscribe\") {\n setShowWizard(true);\n // Drop only `action`, so a refresh does not re-trigger it. Every other parameter\n // must survive: a host page (the settings hub) selects this dashboard through\n // `?section=`, and losing it switches the page away and unmounts this component.\n window.history.replaceState(\n window.history.state,\n \"\",\n urlWithoutParam({\n pathname: window.location.pathname,\n search: window.location.search,\n hash: window.location.hash,\n param: \"action\",\n }),\n );\n }\n }, [searchParams]);\n\n // Detect critical subscriptions for alert banners\n const criticalSubscriptions = data.subscriptions.filter(\n (sub) =>\n sub.status === SubscriptionStatus.PAST_DUE ||\n (sub.status === SubscriptionStatus.TRIALING &&\n sub.trialEnd &&\n new Date(sub.trialEnd).getTime() - new Date().getTime() <= 7 * 24 * 60 * 60 * 1000),\n );\n\n // Handle modal close with refresh\n const handleModalClose = (open: boolean) => {\n if (!open) {\n setActiveModal(null);\n refreshData();\n }\n };\n\n // Get modal title based on type\n const getModalTitle = (type: ModalType): string => {\n switch (type) {\n case \"subscriptions\":\n return t(\"billing.ui.dashboard.modal.subscriptions\");\n case \"payment-methods\":\n return t(\"billing.ui.payment_methods.title\");\n case \"invoices\":\n return t(\"billing.ui.dashboard.modal.invoices\");\n case \"usage\":\n return t(\"billing.ui.usage.title\");\n default:\n return \"\";\n }\n };\n\n // Show loading state while checking for customer\n const isInitialLoading = loading.customer && !noCustomerExists && !data.customer;\n\n return (\n <div className=\"flex w-full flex-col gap-y-6\">\n {/* Header */}\n <div className=\"flex items-center gap-x-3\">\n <Wallet className=\"h-8 w-8\" />\n <h1 className=\"text-primary text-3xl font-semibold\">{t(\"billing.title\")}</h1>\n </div>\n\n {/* Initial Loading State */}\n {isInitialLoading && (\n <Card>\n <CardContent className=\"flex items-center justify-center py-12\">\n <Loader2 className=\"h-8 w-8 animate-spin text-muted-foreground\" />\n </CardContent>\n </Card>\n )}\n\n {/* No Customer State - Show Setup Prompt */}\n {noCustomerExists && !isInitialLoading && (\n <Card>\n <CardHeader className=\"text-center\">\n <div className=\"mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-primary/10\">\n <CreditCard className=\"h-8 w-8 text-primary\" />\n </div>\n <CardTitle>{t(\"billing.ui.dashboard.setup_title\")}</CardTitle>\n <CardDescription>{t(\"billing.ui.dashboard.setup_description\")}</CardDescription>\n </CardHeader>\n <CardContent className=\"flex justify-center pb-8\">\n <Button onClick={handleCreateCustomer} disabled={creatingCustomer} size=\"lg\">\n {creatingCustomer ? (\n <>\n <Loader2 className=\"me-2 h-4 w-4 animate-spin\" />\n {t(\"billing.ui.dashboard.setting_up\")}\n </>\n ) : (\n t(\"billing.ui.dashboard.setup_button\")\n )}\n </Button>\n </CardContent>\n {errors.customer && (\n <CardContent className=\"pt-0\">\n <p className=\"text-center text-destructive text-xs/relaxed\">{t(errors.customer)}</p>\n </CardContent>\n )}\n </Card>\n )}\n\n {/* Main Dashboard Content - Only shown when customer exists */}\n {!noCustomerExists && !isInitialLoading && (\n <>\n {/* Alert Banners */}\n {criticalSubscriptions.map((subscription) => (\n <BillingAlertBanner\n key={subscription.id}\n subscription={subscription}\n onUpdatePayment={() => setActiveModal(\"payment-methods\")}\n onAddPayment={() => setActiveModal(\"payment-methods\")}\n />\n ))}\n\n {/* Summary Cards Grid */}\n <div className=\"grid gap-4 md:grid-cols-2\">\n <SubscriptionSummaryCard\n subscriptions={data.subscriptions}\n loading={loading.subscriptions}\n error={errors.subscriptions ? t(errors.subscriptions) : undefined}\n onManageClick={() => {\n if (data.subscriptions.length === 0) {\n // No subscriptions - open wizard directly\n setShowWizard(true);\n } else {\n // Has subscriptions - open manage modal\n setActiveModal(\"subscriptions\");\n }\n }}\n />\n\n <PaymentMethodSummaryCard\n paymentMethods={data.paymentMethods}\n defaultPaymentMethodId={data.customer?.defaultPaymentMethodId}\n loading={loading.paymentMethods}\n error={errors.paymentMethods ? t(errors.paymentMethods) : undefined}\n onManageClick={() => setActiveModal(\"payment-methods\")}\n />\n\n <CustomerInfoCard\n customer={data.customer}\n loading={loading.customer}\n error={errors.customer ? t(errors.customer) : undefined}\n />\n\n <InvoicesSummaryCard\n invoices={data.invoices}\n loading={loading.invoices}\n error={errors.invoices ? t(errors.invoices) : undefined}\n onViewAllClick={() => setActiveModal(\"invoices\")}\n />\n\n {/* Usage Card - only shown when metered subscriptions exist */}\n {hasMeteredSubscriptions() && (\n <BillingUsageSummaryCard\n meters={data.meters}\n summaries={data.meterSummaries}\n loading={loading.usage}\n error={errors.usage ? t(errors.usage) : undefined}\n onViewDetailsClick={() => setActiveModal(\"usage\")}\n />\n )}\n </div>\n\n {/* Detail Modals */}\n <BillingDetailModal\n open={activeModal === \"subscriptions\"}\n onOpenChange={handleModalClose}\n title={getModalTitle(\"subscriptions\")}\n >\n {/* The dashboard's list, so a wizard success (refreshData) refreshes the dialog too */}\n <SubscriptionsContainer\n onOpenWizard={handleOpenWizard}\n hasActiveRecurringSubscription={hasActiveRecurringSubscription}\n subscriptions={data.subscriptions}\n onSubscriptionsChange={refreshData}\n />\n </BillingDetailModal>\n\n <BillingDetailModal\n open={activeModal === \"payment-methods\"}\n onOpenChange={handleModalClose}\n title={getModalTitle(\"payment-methods\")}\n >\n <PaymentMethodsContainer />\n </BillingDetailModal>\n\n <BillingDetailModal\n open={activeModal === \"invoices\"}\n onOpenChange={handleModalClose}\n title={getModalTitle(\"invoices\")}\n >\n <InvoicesContainer />\n </BillingDetailModal>\n\n <BillingDetailModal\n open={activeModal === \"usage\"}\n onOpenChange={handleModalClose}\n title={getModalTitle(\"usage\")}\n >\n <UsageContainer />\n </BillingDetailModal>\n\n {/* Subscription Wizard - rendered at dashboard level to avoid nested dialogs */}\n <SubscriptionWizard\n open={showWizard && !!data.customer}\n onOpenChange={(open) => !open && handleWizardClose()}\n onSuccess={refreshData}\n hasActiveRecurringSubscription={hasActiveRecurringSubscription}\n subscription={editingSubscription ?? undefined}\n />\n </>\n )}\n </div>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { CreditCard } from \"lucide-react\";\nimport { useEffect, useState } from \"react\";\nimport { Button } from \"../../../../../shadcnui\";\nimport { PaymentMethodInterface, StripeCustomerService } from \"../../data\";\nimport { PaymentMethodEditor } from \"../forms/PaymentMethodEditor\";\nimport { PaymentMethodsList } from \"../lists/PaymentMethodsList\";\n\nexport function PaymentMethodsContainer() {\n const t = useTranslations();\n const [paymentMethods, setPaymentMethods] = useState<PaymentMethodInterface[]>([]);\n const [loading, setLoading] = useState<boolean>(true);\n const [showAddPaymentMethod, setShowAddPaymentMethod] = useState<boolean>(false);\n\n const loadPaymentMethods = async () => {\n setLoading(true);\n try {\n const fetchedPaymentMethods = await StripeCustomerService.listPaymentMethods();\n setPaymentMethods(fetchedPaymentMethods);\n } catch (error) {\n console.error(\"[PaymentMethodsContainer] Failed to load payment methods:\", error);\n } finally {\n setLoading(false);\n }\n };\n\n useEffect(() => {\n loadPaymentMethods();\n }, []);\n\n if (loading) {\n return (\n <div className=\"flex h-64 items-center justify-center\">\n <p className=\"text-muted-foreground\">{t(\"billing.ui.payment_methods.loading\")}</p>\n </div>\n );\n }\n\n return (\n <div className=\"flex w-full flex-col gap-y-6\">\n {/* Header */}\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-x-3\">\n <CreditCard className=\"h-8 w-8\" />\n <h1 className=\"text-primary text-3xl font-semibold\">{t(\"billing.ui.payment_methods.title\")}</h1>\n </div>\n <Button onClick={() => setShowAddPaymentMethod(true)}>{t(\"billing.ui.payment_methods.add\")}</Button>\n </div>\n\n {/* Empty State */}\n {paymentMethods.length === 0 && (\n <div className=\"flex flex-col items-center justify-center gap-y-4 rounded-lg border-2 border-dashed border-gray-300 bg-muted/50 p-12\">\n <CreditCard className=\"h-16 w-16 text-muted-foreground\" />\n <div className=\"text-center\">\n <h3 className=\"mb-2 text-sm font-medium\">{t(\"billing.ui.payment_methods.empty_title\")}</h3>\n <p className=\"mb-4 text-muted-foreground\">{t(\"billing.ui.payment_methods.empty_description\")}</p>\n <Button onClick={() => setShowAddPaymentMethod(true)}>{t(\"billing.ui.payment_methods.add_first\")}</Button>\n </div>\n </div>\n )}\n\n {/* Payment Methods List */}\n {paymentMethods.length > 0 && (\n <PaymentMethodsList paymentMethods={paymentMethods} onUpdate={loadPaymentMethods} />\n )}\n\n {/* Add Payment Method Modal */}\n {showAddPaymentMethod && (\n <PaymentMethodEditor\n open={showAddPaymentMethod}\n onOpenChange={setShowAddPaymentMethod}\n onSuccess={loadPaymentMethods}\n />\n )}\n </div>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from \"../../../../../shadcnui\";\nimport { PaymentMethodForm } from \"./PaymentMethodForm\";\n\ntype PaymentMethodEditorProps = {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n onSuccess: () => void;\n};\n\nexport function PaymentMethodEditor({ open, onOpenChange, onSuccess }: PaymentMethodEditorProps) {\n const t = useTranslations();\n const handleSuccess = () => {\n onSuccess();\n onOpenChange(false);\n };\n\n const handleCancel = () => {\n onOpenChange(false);\n };\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent className=\"max-w-md\">\n <DialogHeader>\n <DialogTitle>{t(\"billing.ui.payment_methods.add\")}</DialogTitle>\n <DialogDescription>{t(\"billing.ui.payment_methods.add_description\")}</DialogDescription>\n </DialogHeader>\n {open && <PaymentMethodForm onSuccess={handleSuccess} onCancel={handleCancel} />}\n </DialogContent>\n </Dialog>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { CardElement, useElements, useStripe } from \"@stripe/react-stripe-js\";\nimport { useEffect, useState } from \"react\";\nimport { Alert, AlertDescription, Button, Checkbox, Label } from \"../../../../../shadcnui\";\nimport { StripeCustomerService } from \"../../data\";\n\ntype PaymentMethodFormProps = {\n onSuccess: () => void;\n onCancel: () => void;\n isLoading?: boolean;\n /**\n * Always make the new card the customer's default and hide the checkbox. The\n * subscription wizard needs this: the next attempt must charge the card just added.\n */\n alwaysSetDefault?: boolean;\n};\n\nexport function PaymentMethodForm({\n onSuccess,\n onCancel,\n isLoading = false,\n alwaysSetDefault = false,\n}: PaymentMethodFormProps) {\n const t = useTranslations();\n const stripe = useStripe();\n const elements = useElements();\n\n const [setupIntent, setSetupIntent] = useState<{ clientSecret: string } | null>(null);\n const [loading, setLoading] = useState<boolean>(true);\n const [isSubmitting, setIsSubmitting] = useState<boolean>(false);\n const [error, setError] = useState<string | null>(null);\n const [setAsDefault, setSetAsDefault] = useState<boolean>(true);\n\n // Fetch setup intent on component mount\n useEffect(() => {\n const fetchSetupIntent = async () => {\n setLoading(true);\n try {\n const intent = await StripeCustomerService.createSetupIntent();\n setSetupIntent(intent);\n } catch (err) {\n console.error(\"[PaymentMethodForm] Failed to create setup intent:\", err);\n setError(t(\"billing.ui.payment_form.errors.init\"));\n } finally {\n setLoading(false);\n }\n };\n\n fetchSetupIntent();\n }, []);\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n\n if (!stripe || !elements || !setupIntent) {\n return;\n }\n\n setIsSubmitting(true);\n setError(null);\n\n try {\n const cardElement = elements.getElement(CardElement);\n if (!cardElement) {\n throw new Error(\"Card element not found\");\n }\n\n // Confirm card setup with Stripe\n const { error: stripeError, setupIntent: confirmedSetupIntent } = await stripe.confirmCardSetup(\n setupIntent.clientSecret,\n {\n payment_method: {\n card: cardElement,\n },\n },\n );\n\n if (stripeError) {\n console.error(\"[PaymentMethodForm] Stripe error:\", stripeError);\n setError(stripeError.message || t(\"billing.ui.payment_form.errors.add\"));\n setIsSubmitting(false);\n return;\n }\n\n // Set as default if checkbox is checked\n if ((alwaysSetDefault || setAsDefault) && confirmedSetupIntent?.payment_method) {\n await StripeCustomerService.setDefaultPaymentMethod({\n paymentMethodId:\n typeof confirmedSetupIntent.payment_method === \"string\"\n ? confirmedSetupIntent.payment_method\n : confirmedSetupIntent.payment_method.id,\n });\n }\n\n onSuccess();\n } catch (err: any) {\n console.error(\"[PaymentMethodForm] Error:\", err);\n setError(err.message || t(\"billing.ui.payment_form.errors.unexpected\"));\n } finally {\n setIsSubmitting(false);\n }\n };\n\n if (loading) {\n return (\n <div className=\"flex items-center justify-center py-8\">\n <p className=\"text-muted-foreground\">{t(\"billing.ui.payment_form.loading\")}</p>\n </div>\n );\n }\n\n if (!setupIntent && error) {\n return (\n <Alert variant=\"destructive\" className=\"bg-red-50 border-red-200\">\n <AlertDescription>{error}</AlertDescription>\n </Alert>\n );\n }\n\n return (\n <form onSubmit={handleSubmit} className=\"flex flex-col gap-y-4\">\n {/* Card Element */}\n <div className=\"rounded-md border border-gray-300 p-3\">\n <CardElement\n options={{\n style: {\n base: {\n fontSize: \"16px\",\n color: \"#424770\",\n \"::placeholder\": {\n color: \"#aab7c4\",\n },\n },\n invalid: {\n color: \"#9e2146\",\n },\n },\n }}\n />\n </div>\n\n {/* Set as Default Checkbox */}\n {!alwaysSetDefault && (\n <div className=\"flex items-center gap-x-2\">\n <Checkbox\n id=\"setAsDefault\"\n checked={setAsDefault}\n onCheckedChange={(checked) => setSetAsDefault(!!checked)}\n />\n <Label htmlFor=\"setAsDefault\" className=\"cursor-pointer font-normal\">\n {t(\"billing.ui.payment_form.set_default\")}\n </Label>\n </div>\n )}\n\n {/* Error Alert */}\n {error && (\n <Alert variant=\"destructive\" className=\"bg-red-50 border-red-200\">\n <AlertDescription>{error}</AlertDescription>\n </Alert>\n )}\n\n {/* Action Buttons */}\n <div className=\"flex justify-end gap-x-2\">\n <Button type=\"button\" variant=\"outline\" onClick={onCancel} disabled={isSubmitting || isLoading}>\n {t(\"ui.buttons.cancel\")}\n </Button>\n <Button type=\"submit\" disabled={!stripe || isSubmitting || isLoading}>\n {isSubmitting ? t(\"billing.ui.wizard.review.processing\") : t(\"billing.ui.cards.payment_method.add_card\")}\n </Button>\n </div>\n </form>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { MoreVertical } from \"lucide-react\";\nimport { useEffect, useState } from \"react\";\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n Badge,\n Button,\n Card,\n CardContent,\n CardHeader,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from \"../../../../../shadcnui\";\nimport { PaymentMethodInterface, StripeCustomerInterface, StripeCustomerService } from \"../../data\";\n\ntype PaymentMethodCardProps = {\n paymentMethod: PaymentMethodInterface;\n onUpdate: () => void;\n};\n\n// Card brand icons mapping\nconst brandIcons: Record<string, string> = {\n visa: \"💳\",\n mastercard: \"💳\",\n amex: \"💳\",\n discover: \"💳\",\n};\n\nexport function PaymentMethodCard({ paymentMethod, onUpdate }: PaymentMethodCardProps) {\n const t = useTranslations();\n const [loading, setLoading] = useState<boolean>(false);\n const [customer, setCustomer] = useState<StripeCustomerInterface | null>(null);\n const [showRemoveDialog, setShowRemoveDialog] = useState<boolean>(false);\n\n // Load customer to check default payment method\n useEffect(() => {\n const loadCustomer = async () => {\n try {\n const fetchedCustomer = await StripeCustomerService.getCustomer();\n setCustomer(fetchedCustomer);\n } catch (error) {\n console.error(\"[PaymentMethodCard] Failed to load customer:\", error);\n }\n };\n\n loadCustomer();\n }, []);\n\n const isDefault = customer?.defaultPaymentMethodId === paymentMethod.id;\n const brand = paymentMethod.card?.brand || \"card\";\n const last4 = paymentMethod.card?.last4 || \"****\";\n const expMonth = paymentMethod.card?.expMonth || 0;\n const expYear = paymentMethod.card?.expYear || 0;\n const brandIcon = brandIcons[brand.toLowerCase()] || \"💳\";\n\n const handleSetDefault = async () => {\n setLoading(true);\n try {\n await StripeCustomerService.setDefaultPaymentMethod({ paymentMethodId: paymentMethod.id });\n onUpdate();\n } catch (error) {\n console.error(\"[PaymentMethodCard] Failed to set as default:\", error);\n } finally {\n setLoading(false);\n }\n };\n\n const handleRemove = async () => {\n setLoading(true);\n try {\n await StripeCustomerService.removePaymentMethod({ paymentMethodId: paymentMethod.id });\n setShowRemoveDialog(false);\n onUpdate();\n } catch (error) {\n console.error(\"[PaymentMethodCard] Failed to remove:\", error);\n setLoading(false);\n }\n };\n\n return (\n <>\n <Card className=\"relative\">\n {/* Default Badge */}\n {isDefault && (\n <Badge variant=\"softGreen\" className=\"absolute end-2 top-2\">\n {t(\"billing.ui.payment_methods.default\")}\n </Badge>\n )}\n\n <CardHeader className=\"flex flex-row items-center justify-between pb-2\">\n <div className=\"flex items-center gap-x-2\">\n <span className=\"text-2xl\">{brandIcon}</span>\n <span className=\"text-sm font-medium capitalize\">{brand}</span>\n </div>\n <DropdownMenu>\n <DropdownMenuTrigger>\n <Button\n render={<div />}\n nativeButton={false}\n variant=\"ghost\"\n size=\"sm\"\n disabled={loading}\n className=\"h-8 w-8 p-0\"\n >\n <MoreVertical className=\"h-4 w-4\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n {!isDefault && (\n <DropdownMenuItem onClick={handleSetDefault} disabled={loading}>\n {t(\"billing.ui.payment_methods.set_default\")}\n </DropdownMenuItem>\n )}\n <DropdownMenuItem\n onClick={() => setShowRemoveDialog(true)}\n disabled={loading}\n className=\"text-destructive\"\n >\n {t(\"ui.buttons.remove\")}\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n </CardHeader>\n\n <CardContent>\n <div className=\"flex flex-col gap-y-1\">\n <p className=\"text-lg font-semibold\">•••• {last4}</p>\n <p className=\"text-sm text-muted-foreground\">\n {t(\"billing.ui.payment_method.expires\", { date: `${String(expMonth).padStart(2, \"0\")}/${expYear}` })}\n </p>\n </div>\n </CardContent>\n </Card>\n\n {/* Remove Confirmation Dialog */}\n <AlertDialog open={showRemoveDialog} onOpenChange={setShowRemoveDialog}>\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>{t(\"billing.ui.payment_methods.remove_title\")}</AlertDialogTitle>\n <AlertDialogDescription>\n {t(\"billing.ui.payment_methods.remove_description\")}\n {isDefault && ` ${t(\"billing.ui.payment_methods.remove_default_warning\")}`}\n </AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel disabled={loading}>{t(\"ui.buttons.cancel\")}</AlertDialogCancel>\n <AlertDialogAction onClick={handleRemove} disabled={loading} className=\"bg-red-600 hover:bg-red-700\">\n {loading ? t(\"billing.ui.payment_methods.removing\") : t(\"ui.buttons.remove\")}\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n </>\n );\n}\n","\"use client\";\n\nimport { PaymentMethodInterface } from \"../../data\";\nimport { PaymentMethodCard } from \"../details/PaymentMethodCard\";\n\ntype PaymentMethodsListProps = {\n paymentMethods: PaymentMethodInterface[];\n onUpdate: () => void;\n};\n\nexport function PaymentMethodsList({ paymentMethods, onUpdate }: PaymentMethodsListProps) {\n return (\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3\">\n {paymentMethods.map((paymentMethod) => (\n <PaymentMethodCard key={paymentMethod.id} paymentMethod={paymentMethod} onUpdate={onUpdate} />\n ))}\n </div>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { useEffect, useState } from \"react\";\nimport { Tabs, TabsList, TabsTrigger } from \"../../../../../shadcnui\";\nimport { InvoiceStatus, StripeInvoiceInterface } from \"../../data/stripe-invoice.interface\";\nimport { StripeInvoiceService } from \"../../data/stripe-invoice.service\";\nimport { InvoicesList } from \"../lists/InvoicesList\";\n\ntype StatusFilter = InvoiceStatus | \"all\";\n\nexport function InvoicesContainer() {\n const t = useTranslations();\n const [invoices, setInvoices] = useState<StripeInvoiceInterface[]>([]);\n const [loading, setLoading] = useState<boolean>(true);\n const [statusFilter, setStatusFilter] = useState<StatusFilter>(\"all\");\n\n const loadInvoices = async () => {\n setLoading(true);\n try {\n const params = statusFilter !== \"all\" ? { status: statusFilter } : undefined;\n const data = await StripeInvoiceService.listInvoices(params);\n setInvoices(data);\n } catch (error) {\n console.error(\"[InvoicesContainer] Failed to load invoices:\", error);\n setInvoices([]);\n } finally {\n setLoading(false);\n }\n };\n\n useEffect(() => {\n loadInvoices();\n }, [statusFilter]);\n\n const handleFilterChange = (value: string) => {\n setStatusFilter(value as StatusFilter);\n };\n\n return (\n <div className=\"space-y-4\">\n {/* Status Filter Tabs */}\n <Tabs value={statusFilter} onValueChange={handleFilterChange}>\n <TabsList>\n <TabsTrigger value=\"all\">{t(\"billing.ui.invoices.filter_all\")}</TabsTrigger>\n <TabsTrigger value={InvoiceStatus.PAID}>{t(\"billing.ui.invoice_status.paid\")}</TabsTrigger>\n <TabsTrigger value={InvoiceStatus.OPEN}>{t(\"billing.ui.invoice_status.open\")}</TabsTrigger>\n <TabsTrigger value={InvoiceStatus.VOID}>{t(\"billing.ui.invoice_status.void\")}</TabsTrigger>\n <TabsTrigger value={InvoiceStatus.UNCOLLECTIBLE}>{t(\"billing.ui.invoice_status.uncollectible\")}</TabsTrigger>\n </TabsList>\n </Tabs>\n\n {/* Loading State */}\n {loading && <div className=\"text-center py-8 text-muted-foreground\">{t(\"billing.ui.invoices.loading\")}</div>}\n\n {/* Empty State */}\n {!loading && invoices.length === 0 && (\n <div className=\"border border-dashed border-gray-300 rounded-lg p-8 text-center\">\n <p className=\"text-lg font-medium text-muted-foreground mb-2\">{t(\"billing.ui.invoices.empty_title\")}</p>\n <p className=\"text-sm text-muted-foreground\">{t(\"billing.ui.invoices.empty_description\")}</p>\n </div>\n )}\n\n {/* Invoices List */}\n {!loading && invoices.length > 0 && <InvoicesList invoices={invoices} onInvoicesChange={loadInvoices} />}\n </div>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { useState } from \"react\";\nimport { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from \"../../../../../shadcnui\";\nimport { formatCurrency, formatDate, useBillingFormatOptions } from \"../../../components/utils\";\nimport { StripeInvoiceInterface } from \"../../data/stripe-invoice.interface\";\nimport { InvoiceDetails } from \"../details/InvoiceDetails\";\nimport { InvoiceStatusBadge } from \"../widgets/InvoiceStatusBadge\";\n\ntype InvoicesListProps = {\n invoices: StripeInvoiceInterface[];\n onInvoicesChange: () => void;\n};\n\nexport function InvoicesList({ invoices, onInvoicesChange }: InvoicesListProps) {\n const t = useTranslations();\n const fmt = useBillingFormatOptions();\n const [selectedInvoice, setSelectedInvoice] = useState<StripeInvoiceInterface | null>(null);\n\n const handleRowClick = (invoice: StripeInvoiceInterface) => {\n setSelectedInvoice(invoice);\n };\n\n const getInvoiceNumber = (invoice: StripeInvoiceInterface): string => {\n if (invoice.stripeInvoiceNumber) {\n return invoice.stripeInvoiceNumber;\n }\n // Use last 8 characters of stripeInvoiceId as fallback\n return invoice.stripeInvoiceId.slice(-8);\n };\n\n return (\n <>\n <div className=\"border rounded-lg overflow-clip\">\n <Table>\n <TableHeader className=\"bg-muted\">\n <TableRow>\n <TableHead>{t(\"billing.ui.columns.invoice_number\")}</TableHead>\n <TableHead>{t(\"billing.ui.columns.date\")}</TableHead>\n <TableHead>{t(\"billing.ui.columns.status\")}</TableHead>\n <TableHead className=\"text-end\">{t(\"billing.ui.columns.amount\")}</TableHead>\n <TableHead>{t(\"billing.ui.columns.period\")}</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {invoices.map((invoice) => {\n const invoiceNumber = getInvoiceNumber(invoice);\n const date = formatDate(invoice.periodStart, fmt);\n const amount = formatCurrency(invoice.total, invoice.currency, fmt);\n const period = `${formatDate(invoice.periodStart, fmt)} - ${formatDate(invoice.periodEnd, fmt)}`;\n\n return (\n <TableRow\n key={invoice.id}\n onClick={() => handleRowClick(invoice)}\n className=\"cursor-pointer hover:bg-muted/50\"\n >\n <TableCell className=\"font-medium\">{invoiceNumber}</TableCell>\n <TableCell className=\"text-muted-foreground text-xs\">{date}</TableCell>\n <TableCell>\n <InvoiceStatusBadge status={invoice.status} />\n </TableCell>\n <TableCell className=\"text-end font-medium\">{amount}</TableCell>\n <TableCell className=\"text-muted-foreground text-xs\">{period}</TableCell>\n </TableRow>\n );\n })}\n </TableBody>\n </Table>\n </div>\n\n {/* Invoice Details Dialog */}\n {selectedInvoice && (\n <InvoiceDetails\n invoice={selectedInvoice}\n open={!!selectedInvoice}\n onOpenChange={(open) => !open && setSelectedInvoice(null)}\n onInvoiceChange={() => {\n onInvoicesChange();\n setSelectedInvoice(null);\n }}\n />\n )}\n </>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { Download, ExternalLink, RefreshCw } from \"lucide-react\";\nimport { Button, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from \"../../../../../shadcnui\";\nimport { formatCurrency, formatDate, useBillingFormatOptions } from \"../../../components/utils\";\nimport { InvoiceStatus, StripeInvoiceInterface } from \"../../data/stripe-invoice.interface\";\nimport { InvoiceStatusBadge } from \"../widgets/InvoiceStatusBadge\";\n\ntype InvoiceDetailsProps = {\n invoice: StripeInvoiceInterface;\n open: boolean;\n onOpenChange: (open: boolean) => void;\n onInvoiceChange: () => void;\n};\n\nexport function InvoiceDetails({\n invoice,\n open,\n onOpenChange,\n onInvoiceChange: _onInvoiceChange,\n}: InvoiceDetailsProps) {\n const t = useTranslations();\n const fmt = useBillingFormatOptions();\n const handleDownloadPDF = () => {\n if (invoice.stripePdfUrl) {\n window.open(invoice.stripePdfUrl, \"_blank\");\n }\n };\n\n const handleRetryPayment = async () => {\n // TODO: Implement retry payment logic\n };\n\n const handleViewInStripe = () => {\n if (invoice.stripeHostedInvoiceUrl) {\n window.open(invoice.stripeHostedInvoiceUrl, \"_blank\");\n }\n };\n\n const getInvoiceNumber = (): string => {\n if (invoice.stripeInvoiceNumber) {\n return invoice.stripeInvoiceNumber;\n }\n return invoice.stripeInvoiceId.slice(-8);\n };\n\n const productName = invoice.subscription?.price?.product?.name || t(\"billing.ui.cards.subscriptions.fallback_name\");\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent className=\"max-w-2xl\">\n <DialogHeader>\n <DialogTitle>{t(\"billing.ui.invoice.title\", { number: getInvoiceNumber() })}</DialogTitle>\n <DialogDescription>{formatDate(invoice.periodStart, fmt)}</DialogDescription>\n </DialogHeader>\n\n <div className=\"space-y-6\">\n {/* Status */}\n <div className=\"flex items-center gap-x-3\">\n <span className=\"text-sm font-medium text-muted-foreground\">{t(\"billing.ui.labels.status\")}</span>\n <InvoiceStatusBadge status={invoice.status} />\n </div>\n\n {/* Invoice Info Grid */}\n <div className=\"grid grid-cols-2 gap-4\">\n <div>\n <span className=\"text-muted-foreground text-xs\">{t(\"billing.ui.invoice.billing_period\")}</span>\n <p className=\"text-sm\">\n {formatDate(invoice.periodStart, fmt)} - {formatDate(invoice.periodEnd, fmt)}\n </p>\n </div>\n\n {invoice.dueDate && (\n <div>\n <span className=\"text-muted-foreground text-xs\">{t(\"billing.ui.invoice.due_date\")}</span>\n <p className=\"text-sm\">{formatDate(invoice.dueDate, fmt)}</p>\n </div>\n )}\n\n {invoice.paidAt && (\n <div>\n <span className=\"text-muted-foreground text-xs\">{t(\"billing.ui.invoice.paid_date\")}</span>\n <p className=\"text-sm\">{formatDate(invoice.paidAt, fmt)}</p>\n </div>\n )}\n\n <div>\n <span className=\"text-muted-foreground text-xs\">{t(\"billing.ui.invoice.attempt_count\")}</span>\n <p className=\"text-sm\">{invoice.attemptCount}</p>\n </div>\n </div>\n\n {/* Line Items */}\n <div>\n <h4 className=\"text-sm font-medium text-muted-foreground mb-2\">{t(\"billing.ui.invoice.line_items\")}</h4>\n <div className=\"border rounded-lg overflow-clip\">\n <table className=\"w-full\">\n <thead className=\"bg-muted\">\n <tr>\n <th className=\"text-start p-3 text-sm font-medium\">{t(\"billing.ui.columns.description\")}</th>\n <th className=\"text-end p-3 text-sm font-medium\">{t(\"billing.ui.columns.amount\")}</th>\n </tr>\n </thead>\n <tbody>\n <tr className=\"border-t\">\n <td className=\"p-3\">{productName}</td>\n <td className=\"p-3 text-end\">{formatCurrency(invoice.subtotal, invoice.currency, fmt)}</td>\n </tr>\n </tbody>\n </table>\n </div>\n </div>\n\n {/* Amount Breakdown */}\n <div className=\"space-y-2 border-t pt-4\">\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground text-xs\">{t(\"billing.ui.invoice.subtotal\")}</span>\n <span className=\"text-sm\">{formatCurrency(invoice.subtotal, invoice.currency, fmt)}</span>\n </div>\n\n {invoice.tax !== undefined && invoice.tax > 0 && (\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground text-xs\">{t(\"billing.ui.invoice.tax\")}</span>\n <span className=\"text-sm\">{formatCurrency(invoice.tax, invoice.currency, fmt)}</span>\n </div>\n )}\n\n <div className=\"flex justify-between text-lg font-semibold border-t pt-2\">\n <span>{t(\"billing.ui.invoice.total\")}</span>\n <span>{formatCurrency(invoice.total, invoice.currency, fmt)}</span>\n </div>\n\n {invoice.amountPaid > 0 && (\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground text-xs\">{t(\"billing.ui.invoice.amount_paid\")}</span>\n <span className=\"text-sm text-success\">\n {formatCurrency(invoice.amountPaid, invoice.currency, fmt)}\n </span>\n </div>\n )}\n\n {invoice.amountRemaining > 0 && (\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground text-xs\">{t(\"billing.ui.invoice.amount_due\")}</span>\n <span className=\"text-sm text-destructive\">\n {formatCurrency(invoice.amountRemaining, invoice.currency, fmt)}\n </span>\n </div>\n )}\n </div>\n\n {/* Action Buttons */}\n <div className=\"flex flex-wrap gap-2 pt-4 border-t\">\n {invoice.stripePdfUrl && (\n <Button variant=\"outline\" onClick={handleDownloadPDF}>\n <Download className=\"me-2 h-4 w-4\" />\n {t(\"billing.ui.invoice.download_pdf\")}\n </Button>\n )}\n\n {invoice.status === InvoiceStatus.OPEN && invoice.attempted && (\n <Button variant=\"default\" onClick={handleRetryPayment}>\n <RefreshCw className=\"me-2 h-4 w-4\" />\n {t(\"billing.ui.invoice.retry_payment\")}\n </Button>\n )}\n\n {invoice.stripeHostedInvoiceUrl && (\n <Button variant=\"outline\" onClick={handleViewInStripe}>\n <ExternalLink className=\"me-2 h-4 w-4\" />\n {t(\"billing.ui.invoice.view_in_stripe\")}\n </Button>\n )}\n </div>\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { Badge, type BadgeProps } from \"../../../../../shadcnui\";\nimport { InvoiceStatus } from \"../../data/stripe-invoice.interface\";\n\ntype InvoiceStatusBadgeProps = {\n status: InvoiceStatus;\n};\n\ntype StatusConfig = {\n /** Message key, translated at render time. */\n label: string;\n variant: BadgeProps[\"variant\"];\n};\n\nconst statusConfig: Record<InvoiceStatus, StatusConfig> = {\n [InvoiceStatus.DRAFT]: {\n label: \"billing.ui.invoice_status.draft\",\n variant: \"softGray\",\n },\n [InvoiceStatus.OPEN]: {\n label: \"billing.ui.invoice_status.open\",\n variant: \"softBlue\",\n },\n [InvoiceStatus.PAID]: {\n label: \"billing.ui.invoice_status.paid\",\n variant: \"softGreen\",\n },\n [InvoiceStatus.VOID]: {\n label: \"billing.ui.invoice_status.void\",\n variant: \"softGray\",\n },\n [InvoiceStatus.UNCOLLECTIBLE]: {\n label: \"billing.ui.invoice_status.uncollectible\",\n variant: \"softRed\",\n },\n};\n\nexport function InvoiceStatusBadge({ status }: InvoiceStatusBadgeProps) {\n const t = useTranslations();\n const config = statusConfig[status] || statusConfig[InvoiceStatus.DRAFT];\n\n return <Badge variant={config.variant}>{t(config.label)}</Badge>;\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { CreditCard } from \"lucide-react\";\nimport { useCallback, useEffect, useState } from \"react\";\nimport { Button } from \"../../../../../shadcnui\";\nimport { BillingAlertBanner } from \"../../../components\";\nimport { StripeSubscriptionInterface, StripeSubscriptionService, SubscriptionStatus } from \"../../data\";\nimport { SubscriptionsList } from \"../lists\";\n\ntype SubscriptionsContainerProps = {\n onOpenWizard?: (subscription?: StripeSubscriptionInterface) => void;\n hasActiveRecurringSubscription?: boolean;\n /**\n * The list a parent already holds (the billing dashboard). When given, the container\n * shows it instead of loading its own, and a change asks the parent to refresh through\n * `onSubscriptionsChange`, so the dialog and the cards behind it never disagree.\n */\n subscriptions?: StripeSubscriptionInterface[];\n onSubscriptionsChange?: () => void;\n};\n\nexport function SubscriptionsContainer({\n onOpenWizard,\n hasActiveRecurringSubscription,\n subscriptions: parentSubscriptions,\n onSubscriptionsChange,\n}: SubscriptionsContainerProps) {\n const t = useTranslations();\n const isControlled = parentSubscriptions !== undefined;\n const [ownSubscriptions, setSubscriptions] = useState<StripeSubscriptionInterface[]>([]);\n const [ownLoading, setLoading] = useState<boolean>(!isControlled);\n const subscriptions = parentSubscriptions ?? ownSubscriptions;\n const loading = isControlled ? false : ownLoading;\n\n const loadSubscriptions = useCallback(async () => {\n if (isControlled) {\n onSubscriptionsChange?.();\n return;\n }\n setLoading(true);\n try {\n const fetchedSubscriptions = await StripeSubscriptionService.listSubscriptions();\n setSubscriptions(fetchedSubscriptions);\n } catch (error) {\n console.error(\"[SubscriptionsContainer] Failed to load subscriptions:\", error);\n } finally {\n setLoading(false);\n }\n }, [isControlled, onSubscriptionsChange]);\n\n useEffect(() => {\n if (!isControlled) loadSubscriptions();\n }, []);\n\n // Detect critical subscriptions\n const criticalSubscriptions = subscriptions.filter(\n (sub) =>\n sub.status === SubscriptionStatus.PAST_DUE ||\n (sub.status === SubscriptionStatus.TRIALING &&\n sub.trialEnd &&\n new Date(sub.trialEnd).getTime() - new Date().getTime() <= 7 * 24 * 60 * 60 * 1000),\n );\n\n if (loading) {\n return (\n <div className=\"flex h-64 items-center justify-center\">\n <p className=\"text-muted-foreground\">{t(\"billing.ui.subscriptions.loading\")}</p>\n </div>\n );\n }\n\n return (\n <div className=\"flex w-full flex-col gap-y-6\">\n {/* Header */}\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-x-3\">\n <CreditCard className=\"h-8 w-8\" />\n <h1 className=\"text-primary text-3xl font-semibold\">{t(\"billing.ui.subscriptions.title\")}</h1>\n </div>\n {subscriptions.length > 0 && (\n <Button onClick={() => onOpenWizard?.()}>\n {hasActiveRecurringSubscription\n ? t(\"billing.ui.subscriptions.purchase_addons\")\n : t(\"billing.ui.subscriptions.subscribe\")}\n </Button>\n )}\n </div>\n\n {/* Alert Banners */}\n {criticalSubscriptions.map((subscription) => (\n <BillingAlertBanner key={subscription.id} subscription={subscription} />\n ))}\n\n {/* Empty state CTA */}\n {subscriptions.length === 0 && (\n <div className=\"flex flex-col items-center justify-center py-12 space-y-4\">\n <CreditCard className=\"h-16 w-16 text-muted-foreground\" />\n <div className=\"text-center\">\n <h3 className=\"text-sm font-medium mb-2\">{t(\"billing.ui.subscriptions.empty_title\")}</h3>\n <p className=\"text-muted-foreground mb-6\">{t(\"billing.ui.subscriptions.empty_description\")}</p>\n <Button onClick={() => onOpenWizard?.()}>{t(\"billing.ui.subscriptions.subscribe\")}</Button>\n </div>\n </div>\n )}\n\n {/* Subscriptions List */}\n {subscriptions.length > 0 && (\n <SubscriptionsList\n subscriptions={subscriptions}\n onSubscriptionsChange={loadSubscriptions}\n onChangePlan={(sub) => onOpenWizard?.(sub)}\n />\n )}\n </div>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { useState } from \"react\";\nimport { Button, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from \"../../../../../shadcnui\";\nimport {\n applyDiscount,\n formatCurrency,\n formatDate,\n subscriptionDiscount,\n useBillingFormatOptions,\n} from \"../../../components/utils\";\nimport { StripePriceInterface } from \"../../../stripe-price/data/stripe-price.interface\";\nimport { StripeSubscriptionInterface, SubscriptionStatus } from \"../../data\";\nimport { SubscriptionDetails } from \"../details/SubscriptionDetails\";\nimport { SubscriptionStatusBadge } from \"../widgets/SubscriptionStatusBadge\";\n\n/**\n * Formats the plan name from price data.\n * Format: \"Product Name - Nickname (Interval)\" e.g., \"Only 35 - Pro (Monthly)\"\n */\nfunction formatPlanName(price: StripePriceInterface | undefined, t: ReturnType<typeof useTranslations>): string {\n if (!price) return t(\"billing.ui.not_available\");\n\n const productName = price.product?.name || \"\";\n const nickname = price.nickname || \"\";\n\n // Format interval: \"month\" -> \"Monthly\", \"year\" -> \"Yearly\", etc.\n let interval = \"\";\n if (price.recurring?.interval) {\n const intervalMap: Record<string, string> = {\n day: t(\"billing.admin.prices.interval.day\"),\n week: t(\"billing.admin.prices.interval.week\"),\n month: t(\"billing.admin.prices.interval.month\"),\n year: t(\"billing.admin.prices.interval.year\"),\n };\n interval = intervalMap[price.recurring.interval] || price.recurring.interval;\n }\n\n // Build the plan label: \"Product - Nickname\" or just \"Product\"\n const parts = [productName, nickname].filter(Boolean);\n const planLabel = parts.join(\" - \");\n\n // Add interval in parentheses if available\n return interval ? `${planLabel} (${interval})` : planLabel || t(\"billing.ui.not_available\");\n}\n\ntype SubscriptionsListProps = {\n subscriptions: StripeSubscriptionInterface[];\n onSubscriptionsChange: () => void;\n onChangePlan?: (subscription: StripeSubscriptionInterface) => void;\n};\n\nexport function SubscriptionsList({ subscriptions, onSubscriptionsChange, onChangePlan }: SubscriptionsListProps) {\n const t = useTranslations();\n const fmt = useBillingFormatOptions();\n const [selectedSub, setSelectedSub] = useState<StripeSubscriptionInterface | null>(null);\n\n const handleRowClick = (subscription: StripeSubscriptionInterface) => {\n setSelectedSub(subscription);\n };\n\n return (\n <>\n <div className=\"border rounded-lg overflow-clip\">\n <Table>\n <TableHeader className=\"bg-muted\">\n <TableRow>\n <TableHead>{t(\"billing.ui.columns.status\")}</TableHead>\n <TableHead>{t(\"billing.ui.columns.plan\")}</TableHead>\n <TableHead>{t(\"billing.ui.columns.period\")}</TableHead>\n <TableHead className=\"text-end\">{t(\"billing.ui.columns.amount\")}</TableHead>\n <TableHead className=\"text-end\">{t(\"billing.ui.columns.actions\")}</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {subscriptions.map((subscription) => {\n const price = subscription.price;\n const amount = price?.unitAmount ? formatCurrency(price.unitAmount, price.currency, fmt) : \"0\";\n // What Stripe bills: the price after the discount the subscription carries.\n const discountedAmount = price\n ? applyDiscount(price.unitAmount, subscriptionDiscount(subscription))\n : null;\n const period = `${formatDate(subscription.currentPeriodStart, fmt)} - ${formatDate(subscription.currentPeriodEnd, fmt)}`;\n\n return (\n <TableRow\n key={subscription.id}\n onClick={() => handleRowClick(subscription)}\n className=\"cursor-pointer hover:bg-muted/50\"\n >\n <TableCell>\n <SubscriptionStatusBadge\n status={subscription.status}\n cancelAtPeriodEnd={subscription.cancelAtPeriodEnd}\n />\n </TableCell>\n <TableCell className=\"font-medium\">{formatPlanName(price, t)}</TableCell>\n <TableCell className=\"text-muted-foreground text-sm\">{period}</TableCell>\n <TableCell className=\"text-end font-medium\">\n {discountedAmount !== null && price ? (\n <>\n <span className=\"line-through text-muted-foreground me-2\">{amount}</span>\n <span className=\"text-success\">{formatCurrency(discountedAmount, price.currency, fmt)}</span>\n </>\n ) : (\n amount\n )}\n </TableCell>\n <TableCell className=\"text-end\">\n {(subscription.status === SubscriptionStatus.ACTIVE ||\n subscription.status === SubscriptionStatus.TRIALING) &&\n onChangePlan && (\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={(e) => {\n e.stopPropagation();\n onChangePlan(subscription);\n }}\n >\n {t(\"billing.ui.subscription.upgrade\")}\n </Button>\n )}\n </TableCell>\n </TableRow>\n );\n })}\n </TableBody>\n </Table>\n </div>\n\n {/* Subscription Details Dialog */}\n {selectedSub && (\n <SubscriptionDetails\n subscription={selectedSub}\n open={!!selectedSub}\n onOpenChange={(open) => !open && setSelectedSub(null)}\n onSubscriptionChange={() => {\n onSubscriptionsChange();\n setSelectedSub(null);\n }}\n onChangePlan={\n onChangePlan\n ? (sub) => {\n setSelectedSub(null); // Close details dialog first\n onChangePlan(sub); // Then open wizard at parent level\n }\n : undefined\n }\n />\n )}\n </>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { useState } from \"react\";\nimport { Button, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from \"../../../../../shadcnui\";\nimport {\n type BillingFormatOptions,\n formatCurrency,\n formatDate,\n useBillingFormatOptions,\n} from \"../../../components/utils\";\nimport { StripeCustomerService } from \"../../../stripe-customer\";\nimport { StripePriceInterface } from \"../../../stripe-price/data/stripe-price.interface\";\nimport { StripeSubscriptionInterface, SubscriptionStatus } from \"../../data\";\nimport { CancelSubscriptionDialog } from \"../forms/CancelSubscriptionDialog\";\nimport { SubscriptionStatusBadge } from \"../widgets/SubscriptionStatusBadge\";\n\n/**\n * Formats the plan name from price data.\n * Format: \"Product Name - Nickname (Interval)\" e.g., \"Only 35 - Pro (Monthly)\"\n */\nfunction formatPlanName(price: StripePriceInterface | undefined, t: ReturnType<typeof useTranslations>): string {\n if (!price) return t(\"billing.ui.not_available\");\n\n const productName = price.product?.name || \"\";\n const nickname = price.nickname || \"\";\n\n // Format interval: \"month\" -> \"Monthly\", \"year\" -> \"Yearly\", etc.\n let interval = \"\";\n if (price.recurring?.interval) {\n const intervalMap: Record<string, string> = {\n day: t(\"billing.admin.prices.interval.day\"),\n week: t(\"billing.admin.prices.interval.week\"),\n month: t(\"billing.admin.prices.interval.month\"),\n year: t(\"billing.admin.prices.interval.year\"),\n };\n interval = intervalMap[price.recurring.interval] || price.recurring.interval;\n }\n\n // Build the plan label: \"Product - Nickname\" or just \"Product\"\n const parts = [productName, nickname].filter(Boolean);\n const planLabel = parts.join(\" - \");\n\n // Add interval in parentheses if available\n return interval ? `${planLabel} (${interval})` : planLabel || t(\"billing.ui.not_available\");\n}\n\n/**\n * Formats the billing amount from price data.\n */\nfunction formatBillingAmount(\n price: StripePriceInterface | undefined,\n notAvailable: string,\n fmt: BillingFormatOptions,\n): string {\n if (!price?.unitAmount) return notAvailable;\n return formatCurrency(price.unitAmount, price.currency, fmt);\n}\n\ntype SubscriptionDetailsProps = {\n subscription: StripeSubscriptionInterface;\n open: boolean;\n onOpenChange: (open: boolean) => void;\n onSubscriptionChange: () => void;\n onChangePlan?: (subscription: StripeSubscriptionInterface) => void;\n};\n\nexport function SubscriptionDetails({\n subscription,\n open,\n onOpenChange,\n onSubscriptionChange,\n onChangePlan,\n}: SubscriptionDetailsProps) {\n const t = useTranslations();\n const fmt = useBillingFormatOptions();\n const [showCancel, setShowCancel] = useState<boolean>(false);\n\n const handleManageViaPortal = async () => {\n try {\n const { url } = await StripeCustomerService.createPortalSession();\n window.open(url, \"_blank\");\n } catch (error) {\n console.error(\"[SubscriptionDetails] Failed to create portal session:\", error);\n }\n };\n\n const canCancel =\n subscription.status === SubscriptionStatus.ACTIVE ||\n subscription.status === SubscriptionStatus.TRIALING ||\n subscription.status === SubscriptionStatus.PAUSED;\n\n return (\n <>\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent className=\"max-w-2xl\">\n <DialogHeader>\n <DialogTitle>{t(\"billing.ui.subscription.details_title\")}</DialogTitle>\n <DialogDescription>{t(\"billing.ui.subscription.details_description\")}</DialogDescription>\n </DialogHeader>\n\n <div className=\"space-y-6\">\n {/* Status */}\n <div className=\"flex items-center gap-x-3\">\n <span className=\"text-muted-foreground text-xs\">{t(\"billing.ui.labels.status\")}</span>\n <SubscriptionStatusBadge\n status={subscription.status}\n cancelAtPeriodEnd={subscription.cancelAtPeriodEnd}\n />\n </div>\n\n {/* Plan Info */}\n <div className=\"space-y-2\">\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground text-xs\">{t(\"billing.ui.labels.plan\")}</span>\n <span className=\"text-sm\">{formatPlanName(subscription.price, t)}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground text-xs\">{t(\"billing.ui.subscription.billing_amount\")}</span>\n <span className=\"text-sm\">\n {formatBillingAmount(subscription.price, t(\"billing.ui.not_available\"), fmt)}\n </span>\n </div>\n </div>\n\n {/* Current Period */}\n <div className=\"space-y-2\">\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground text-xs\">{t(\"billing.ui.subscription.current_period\")}</span>\n <span className=\"text-sm\">\n {formatDate(subscription.currentPeriodStart, fmt)} - {formatDate(subscription.currentPeriodEnd, fmt)}\n </span>\n </div>\n </div>\n\n {/* Trial Info */}\n {subscription.trialEnd && (\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground text-xs\">{t(\"billing.ui.subscription.trial_ends\")}</span>\n <span className=\"text-sm\">{formatDate(subscription.trialEnd, fmt)}</span>\n </div>\n )}\n\n {/* Cancellation Warning */}\n {subscription.cancelAtPeriodEnd && (\n <div className=\"bg-warning/15 border-warning/30 rounded-lg border p-3\">\n <p className=\"text-warning text-xs/relaxed\">\n {t(\"billing.ui.subscription.cancel_at_period_end\", {\n date: formatDate(subscription.currentPeriodEnd, fmt),\n })}\n </p>\n </div>\n )}\n\n {/* Action Buttons */}\n <div className=\"flex flex-wrap gap-2 pt-4 border-t\">\n {onChangePlan && (\n <Button variant=\"default\" onClick={() => onChangePlan(subscription)}>\n {t(\"billing.ui.subscription.change_plan\")}\n </Button>\n )}\n\n {canCancel && (\n <Button variant=\"destructive\" onClick={() => setShowCancel(true)}>\n {t(\"ui.buttons.cancel\")}\n </Button>\n )}\n\n <Button variant=\"outline\" onClick={handleManageViaPortal}>\n {t(\"billing.ui.subscription.manage_portal\")}\n </Button>\n </div>\n </div>\n </DialogContent>\n </Dialog>\n\n {/* Cancel Subscription Dialog */}\n {showCancel && (\n <CancelSubscriptionDialog\n subscription={subscription}\n open={showCancel}\n onOpenChange={setShowCancel}\n onSuccess={() => {\n onSubscriptionChange();\n setShowCancel(false);\n }}\n />\n )}\n </>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { useState } from \"react\";\nimport { SubmitHandler, useForm } from \"react-hook-form\";\nimport { z } from \"zod\";\nimport { FormTextarea } from \"../../../../../components\";\nimport {\n Button,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n Form,\n} from \"../../../../../shadcnui\";\nimport { formatDate, useBillingFormatOptions } from \"../../../components/utils\";\nimport { StripeSubscriptionInterface, StripeSubscriptionService } from \"../../data\";\n\ntype CancelSubscriptionDialogProps = {\n subscription: StripeSubscriptionInterface;\n open: boolean;\n onOpenChange: (open: boolean) => void;\n onSuccess: () => void;\n};\n\nconst formSchema = z.object({\n reason: z.string().optional(),\n});\n\nexport function CancelSubscriptionDialog({\n subscription,\n open,\n onOpenChange,\n onSuccess,\n}: CancelSubscriptionDialogProps) {\n const t = useTranslations();\n const fmt = useBillingFormatOptions();\n const [isSubmitting, setIsSubmitting] = useState<boolean>(false);\n\n const form = useForm<z.infer<typeof formSchema>>({\n resolver: zodResolver(formSchema),\n defaultValues: {\n reason: \"\",\n },\n });\n\n const onSubmit: SubmitHandler<z.infer<typeof formSchema>> = async (_values) => {\n setIsSubmitting(true);\n\n try {\n await StripeSubscriptionService.cancelSubscription({\n id: subscription.id,\n cancelImmediately: false,\n });\n\n onSuccess();\n onOpenChange(false);\n } catch (error) {\n console.error(\"[CancelSubscriptionDialog] Failed to cancel subscription:\", error);\n } finally {\n setIsSubmitting(false);\n }\n };\n\n const periodEndDate = formatDate(subscription.currentPeriodEnd, fmt);\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent className=\"max-w-md\">\n <DialogHeader>\n <DialogTitle>{t(\"billing.ui.subscription.cancel.title\")}</DialogTitle>\n <DialogDescription>{t(\"billing.ui.subscription.cancel.description\")}</DialogDescription>\n </DialogHeader>\n\n <Form {...form}>\n <form onSubmit={form.handleSubmit(onSubmit)} className=\"flex flex-col gap-y-4\">\n <div className=\"bg-primary/10 border-primary/30 text-foreground rounded-lg border p-3 text-sm\">\n {t(\"billing.ui.subscription.cancel.remains_active\", { date: periodEndDate })}\n </div>\n\n <FormTextarea\n form={form}\n id=\"reason\"\n name={t(\"billing.ui.subscription.cancel.reason\")}\n placeholder={t(\"billing.ui.subscription.cancel.reason_placeholder\")}\n className=\"min-h-24\"\n />\n\n <div className=\"flex gap-x-2 justify-end pt-2\">\n <Button type=\"button\" variant=\"outline\" onClick={() => onOpenChange(false)} disabled={isSubmitting}>\n {t(\"billing.ui.subscription.cancel.keep\")}\n </Button>\n <Button type=\"submit\" variant=\"destructive\" disabled={isSubmitting}>\n {isSubmitting\n ? t(\"billing.ui.subscription.cancel.canceling\")\n : t(\"billing.ui.subscription.cancel.confirm\")}\n </Button>\n </div>\n </form>\n </Form>\n </DialogContent>\n </Dialog>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { Badge, type BadgeProps } from \"../../../../../shadcnui\";\nimport { SubscriptionStatus } from \"../../data\";\n\ntype SubscriptionStatusBadgeProps = {\n status: SubscriptionStatus;\n cancelAtPeriodEnd?: boolean;\n};\n\ntype StatusConfig = {\n /** Message key, translated at render time. */\n label: string;\n variant: BadgeProps[\"variant\"];\n};\n\nconst statusConfig: Record<SubscriptionStatus, StatusConfig> = {\n [SubscriptionStatus.ACTIVE]: {\n label: \"billing.ui.subscription_status.active\",\n variant: \"softGreen\",\n },\n [SubscriptionStatus.TRIALING]: {\n label: \"billing.ui.subscription_status.trialing\",\n variant: \"softBlue\",\n },\n [SubscriptionStatus.PAST_DUE]: {\n label: \"billing.ui.subscription_status.past_due\",\n variant: \"softRed\",\n },\n [SubscriptionStatus.CANCELED]: {\n label: \"billing.ui.subscription_status.canceled\",\n variant: \"softGray\",\n },\n [SubscriptionStatus.PAUSED]: {\n label: \"billing.ui.subscription_status.paused\",\n variant: \"softYellow\",\n },\n [SubscriptionStatus.UNPAID]: {\n label: \"billing.ui.subscription_status.unpaid\",\n variant: \"softOrange\",\n },\n [SubscriptionStatus.INCOMPLETE]: {\n label: \"billing.ui.subscription_status.incomplete\",\n variant: \"softGray\",\n },\n [SubscriptionStatus.INCOMPLETE_EXPIRED]: {\n label: \"billing.ui.subscription_status.incomplete_expired\",\n variant: \"softGray\",\n },\n};\n\nconst cancelingConfig: StatusConfig = {\n label: \"billing.ui.cards.subscriptions.canceling\",\n variant: \"softAmber\",\n};\n\nexport function SubscriptionStatusBadge({ status, cancelAtPeriodEnd }: SubscriptionStatusBadgeProps) {\n const t = useTranslations();\n // Show \"Canceling\" when subscription is set to cancel at period end\n const config = cancelAtPeriodEnd\n ? cancelingConfig\n : statusConfig[status] || statusConfig[SubscriptionStatus.CANCELED];\n\n return <Badge variant={config.variant}>{t(config.label)}</Badge>;\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { Tabs, TabsList, TabsTrigger } from \"../../../../../shadcnui\";\n\nexport type BillingInterval = \"month\" | \"year\";\n\nexport type IntervalToggleProps = {\n value: BillingInterval;\n onChange: (interval: BillingInterval) => void;\n hasMonthly: boolean;\n hasYearly: boolean;\n};\n\nexport function IntervalToggle({ value, onChange, hasMonthly, hasYearly }: IntervalToggleProps) {\n const t = useTranslations();\n // Only render if BOTH intervals are available\n if (!hasMonthly || !hasYearly) {\n return null;\n }\n\n return (\n <Tabs value={value} onValueChange={(v) => onChange(v as BillingInterval)}>\n <TabsList>\n <TabsTrigger value=\"month\">{t(\"billing.admin.prices.interval.month\")}</TabsTrigger>\n <TabsTrigger value=\"year\">{t(\"billing.admin.prices.interval.year\")}</TabsTrigger>\n </TabsList>\n </Tabs>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { Check } from \"lucide-react\";\nimport { KeyboardEvent } from \"react\";\nimport { Badge, Button, Card, CardContent, CardFooter, CardHeader } from \"../../../../../shadcnui\";\nimport { formatCurrency, formatInterval, useBillingFormatOptions } from \"../../../components/utils\";\nimport { StripePriceInterface } from \"../../../stripe-price/data/stripe-price.interface\";\nimport { cn } from \"../../../../../utils/cn\";\n\nexport type PricingCardProps = {\n price: StripePriceInterface;\n isCurrentPlan?: boolean;\n isSelected?: boolean;\n isDisabled?: boolean;\n isLoading?: boolean;\n onSelect: (price: StripePriceInterface) => void;\n};\n\nexport function PricingCard({\n price,\n isCurrentPlan = false,\n isSelected = false,\n isDisabled = false,\n isLoading = false,\n onSelect,\n}: PricingCardProps) {\n const t = useTranslations();\n const fmt = useBillingFormatOptions();\n const description = price.description || price.nickname || t(\"billing.ui.pricing.standard\");\n const features = price.features || [];\n const formattedPrice = formatCurrency(price.unitAmount, price.currency, fmt);\n const interval = formatInterval(price, fmt);\n\n const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n if ((e.key === \"Enter\" || e.key === \" \") && !isDisabled && !isCurrentPlan) {\n e.preventDefault();\n onSelect(price);\n }\n };\n\n const handleClick = () => {\n if (!isDisabled && !isCurrentPlan && !isLoading) {\n onSelect(price);\n }\n };\n\n return (\n <Card\n role=\"radio\"\n aria-checked={isSelected}\n aria-label={t(\"billing.ui.pricing.card_aria\", { description, price: formattedPrice, interval })}\n tabIndex={isDisabled ? -1 : 0}\n onKeyDown={handleKeyDown}\n onClick={handleClick}\n className={cn(\n \"relative cursor-pointer transition-all duration-200 flex flex-col h-full\",\n \"focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2\",\n isCurrentPlan && \"bg-muted/30\",\n isSelected && !isCurrentPlan && \"ring-2 ring-primary\",\n !isDisabled && !isCurrentPlan && \"hover:shadow-md hover:border-primary/50\",\n isDisabled && \"opacity-50 pointer-events-none\",\n isLoading && \"pointer-events-none\",\n )}\n >\n {isCurrentPlan && (\n <Badge variant=\"secondary\" className=\"absolute top-2 end-2\">\n {t(\"billing.ui.pricing.current\")}\n </Badge>\n )}\n\n <CardHeader className=\"pb-2\">\n <h3 className=\"text-sm font-medium\">{description}</h3>\n </CardHeader>\n\n <CardContent className=\"pb-4 grow\">\n <div className=\"mb-4\">\n <span className=\"text-3xl font-bold\">{formattedPrice}</span>\n <span className=\"text-muted-foreground ms-1\">{interval}</span>\n </div>\n\n {features.length > 0 && (\n <ul className=\"space-y-2\">\n {features.map((feature, index) => (\n <li key={index} className=\"flex items-start gap-2\">\n <Check className=\"text-success mt-0.5 h-4 w-4 shrink-0\" />\n <span className=\"text-sm text-muted-foreground\">{feature}</span>\n </li>\n ))}\n </ul>\n )}\n </CardContent>\n\n <CardFooter>\n <Button\n variant={isCurrentPlan ? \"secondary\" : isSelected ? \"default\" : \"outline\"}\n className=\"w-full\"\n disabled={isDisabled || isCurrentPlan || isLoading}\n >\n {isLoading\n ? t(\"billing.ui.wizard.review.processing\")\n : isCurrentPlan\n ? t(\"billing.ui.pricing.current_plan\")\n : isSelected\n ? t(\"billing.ui.pricing.selected\")\n : t(\"billing.ui.wizard.steps.select_plan\")}\n </Button>\n </CardFooter>\n </Card>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { Skeleton } from \"../../../../../shadcnui\";\nimport { StripePriceInterface } from \"../../../stripe-price/data/stripe-price.interface\";\nimport { StripeProductInterface } from \"../../../stripe-product\";\nimport { BillingInterval } from \"./IntervalToggle\";\nimport { ProductPricingRow } from \"./ProductPricingRow\";\n\nexport type ProductPricingListProps = {\n products: StripeProductInterface[];\n selectedInterval: BillingInterval;\n currentPriceId?: string;\n selectedPriceId?: string;\n loadingPriceId?: string;\n loading?: boolean;\n onSelectPrice: (price: StripePriceInterface) => void;\n hideRecurringPrices?: boolean;\n hideOneTimePrices?: boolean;\n};\n\nfunction isRecurringProduct(prices: StripePriceInterface[]): boolean {\n return prices.some((p) => p.priceType === \"recurring\");\n}\n\nfunction getFilteredPrices(prices: StripePriceInterface[], selectedInterval: BillingInterval): StripePriceInterface[] {\n const isRecurring = isRecurringProduct(prices);\n\n if (!isRecurring) {\n return prices.filter((p) => p.priceType === \"one_time\");\n }\n\n const intervalPrices = prices.filter(\n (p) => p.priceType === \"recurring\" && p.recurring?.interval === selectedInterval,\n );\n\n if (intervalPrices.length === 0) {\n const fallbackInterval = selectedInterval === \"month\" ? \"year\" : \"month\";\n return prices.filter((p) => p.priceType === \"recurring\" && p.recurring?.interval === fallbackInterval);\n }\n\n return intervalPrices;\n}\n\nexport function ProductPricingList({\n products,\n selectedInterval,\n currentPriceId,\n selectedPriceId,\n loadingPriceId,\n loading = false,\n onSelectPrice,\n hideRecurringPrices = false,\n hideOneTimePrices = false,\n}: ProductPricingListProps) {\n const t = useTranslations();\n if (loading) {\n return <ProductPricingListSkeleton />;\n }\n\n if (products.length === 0) {\n return <div className=\"text-center py-8 text-muted-foreground\">{t(\"billing.ui.pricing.no_plans\")}</div>;\n }\n\n const sortedProducts = [...products].sort((a, b) => {\n const aRecurring = isRecurringProduct(a.stripePrices || []);\n const bRecurring = isRecurringProduct(b.stripePrices || []);\n if (aRecurring && !bRecurring) return -1;\n if (!aRecurring && bRecurring) return 1;\n return 0;\n });\n\n return (\n <div className=\"space-y-6\">\n {sortedProducts.map((product) => {\n const allPrices = product.stripePrices || [];\n\n // Filter prices based on hideRecurringPrices and hideOneTimePrices flags\n let pricesToFilter = allPrices;\n\n if (hideRecurringPrices) {\n pricesToFilter = pricesToFilter.filter((price) => price.priceType !== \"recurring\");\n }\n\n if (hideOneTimePrices) {\n pricesToFilter = pricesToFilter.filter((price) => price.priceType !== \"one_time\");\n }\n\n // Filter out trial prices - users shouldn't manually select trial plans\n pricesToFilter = pricesToFilter.filter((price) => !price.isTrial);\n\n const filteredPrices = getFilteredPrices(pricesToFilter, selectedInterval);\n\n if (filteredPrices.length === 0) {\n return null;\n }\n\n return (\n <ProductPricingRow\n key={product.id}\n product={product}\n prices={filteredPrices}\n currentPriceId={currentPriceId}\n selectedPriceId={selectedPriceId}\n loadingPriceId={loadingPriceId}\n onSelectPrice={onSelectPrice}\n />\n );\n })}\n </div>\n );\n}\n\nfunction ProductPricingListSkeleton() {\n return (\n <div className=\"space-y-6\">\n {[1, 2].map((rowIndex) => (\n <div key={rowIndex} className=\"space-y-3\">\n <Skeleton className=\"h-6 w-32\" />\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4\">\n {[1, 2, 3].map((cardIndex) => (\n <div key={cardIndex} className=\"p-4 rounded-lg border animate-pulse space-y-3\">\n <Skeleton className=\"h-6 w-24\" />\n <Skeleton className=\"h-8 w-32\" />\n <div className=\"space-y-2\">\n <Skeleton className=\"h-4 w-full\" />\n <Skeleton className=\"h-4 w-3/4\" />\n </div>\n <Skeleton className=\"h-10 w-full\" />\n </div>\n ))}\n </div>\n </div>\n ))}\n </div>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { StripePriceInterface } from \"../../../stripe-price/data/stripe-price.interface\";\nimport { StripeProductInterface } from \"../../../stripe-product\";\nimport { SectionHeader } from \"../../../../../components/typography\";\nimport { PricingCard } from \"./PricingCard\";\n\nexport type ProductPricingRowProps = {\n product: StripeProductInterface;\n prices: StripePriceInterface[]; // Multiple prices for this product\n currentPriceId?: string;\n selectedPriceId?: string;\n loadingPriceId?: string;\n onSelectPrice: (price: StripePriceInterface) => void;\n};\n\nexport function ProductPricingRow({\n product,\n prices,\n currentPriceId,\n selectedPriceId,\n loadingPriceId,\n onSelectPrice,\n}: ProductPricingRowProps) {\n const t = useTranslations();\n if (prices.length === 0) {\n return null;\n }\n\n return (\n <div className=\"space-y-3\">\n {/* Product name header */}\n <SectionHeader>{product.name}</SectionHeader>\n\n {/* Price cards in columns */}\n <div\n className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4\"\n role=\"radiogroup\"\n aria-label={t(\"billing.ui.pricing.options_aria\", { name: product.name })}\n >\n {prices\n .sort((a, b) => (a.unitAmount ?? 0) - (b.unitAmount ?? 0))\n .map((price) => (\n <PricingCard\n key={price.id}\n price={price}\n isCurrentPlan={price.id === currentPriceId}\n isSelected={price.id === selectedPriceId}\n isLoading={price.id === loadingPriceId}\n onSelect={onSelectPrice}\n />\n ))}\n </div>\n </div>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { ProrationPreviewInterface } from \"../../../stripe-invoice/data/stripe-invoice.interface\";\nimport { formatCurrency, formatDate, useBillingFormatOptions } from \"../../../components/utils\";\n\ntype ProrationPreviewProps = {\n preview: ProrationPreviewInterface;\n};\n\nexport function ProrationPreview({ preview }: ProrationPreviewProps) {\n const t = useTranslations();\n const fmt = useBillingFormatOptions();\n return (\n <div className=\"bg-primary/10 border-primary/30 rounded-lg border p-4\">\n <h4 className=\"text-primary mb-3 font-semibold\">{t(\"billing.ui.proration.title\")}</h4>\n\n <div className=\"space-y-2\">\n {preview.lineItems.map((item, index) => (\n <div key={index} className=\"flex justify-between text-sm\">\n <span className=\"text-muted-foreground\">{item.description}</span>\n <span className={`font-medium ${item.amount < 0 ? \"text-success\" : \"text-foreground\"}`}>\n {formatCurrency(item.amount, preview.currency, fmt)}\n </span>\n </div>\n ))}\n\n <div className=\"border-primary/30 mt-2 border-t pt-2\">\n <div className=\"flex justify-between font-semibold\">\n <span className=\"text-foreground\">{t(\"billing.ui.proration.net_due_today\")}</span>\n <span className=\"text-foreground\">{formatCurrency(preview.immediateCharge, preview.currency, fmt)}</span>\n </div>\n </div>\n\n {preview.lineItems.length > 0 && preview.lineItems[0].period && (\n <div className=\"text-muted-foreground mt-2 text-xs\">\n {t(\"billing.ui.proration.next_invoice\", {\n date: formatDate(preview.lineItems[0].period.end, fmt),\n amount: formatCurrency(preview.amountDue, preview.currency, fmt),\n })}\n </div>\n )}\n </div>\n </div>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { Check, Loader2 } from \"lucide-react\";\nimport { Button } from \"../../../../../shadcnui\";\nimport { formatCurrency, formatInterval, useBillingFormatOptions } from \"../../../components/utils\";\nimport { StripePriceInterface } from \"../../../stripe-price/data/stripe-price.interface\";\n\ntype SubscriptionConfirmationProps = {\n price: StripePriceInterface;\n isLoading: boolean;\n onConfirm: () => void;\n onCancel: () => void;\n};\n\nexport function SubscriptionConfirmation({ price, isLoading, onConfirm, onCancel }: SubscriptionConfirmationProps) {\n const t = useTranslations();\n const fmt = useBillingFormatOptions();\n const productName = price.product?.name || price.nickname || t(\"billing.ui.wizard.review.selected_plan\");\n const productDescription = price.product?.description || price.description;\n const features = price.features || [];\n\n return (\n <div className=\"bg-accent/10 border border-accent/30 rounded-lg p-4\">\n <h4 className=\"font-semibold mb-3\">{t(\"billing.ui.confirmation.title\")}</h4>\n\n <div className=\"space-y-3\">\n {/* Plan name and description */}\n <div>\n <div className=\"font-medium\">{productName}</div>\n {productDescription && <div className=\"text-sm text-muted-foreground\">{productDescription}</div>}\n </div>\n\n {/* Price */}\n <div className=\"text-lg font-semibold\">\n {formatCurrency(price.unitAmount, price.currency, fmt)}\n <span className=\"text-sm font-normal text-muted-foreground\">{formatInterval(price, fmt)}</span>\n </div>\n\n {/* Features */}\n {features.length > 0 && (\n <div className=\"space-y-1\">\n {features.map((feature, index) => (\n <div key={index} className=\"flex items-center gap-2 text-sm \">\n <Check className=\"h-4 w-4 text-primary\" />\n <span>{feature}</span>\n </div>\n ))}\n </div>\n )}\n\n {/* Action buttons */}\n <div className=\"flex justify-end gap-3 pt-2 border-t border-accent/30\">\n <Button variant=\"outline\" onClick={onCancel} disabled={isLoading}>\n {t(\"ui.buttons.cancel\")}\n </Button>\n <Button onClick={onConfirm} disabled={isLoading}>\n {isLoading ? (\n <>\n <Loader2 className=\"h-4 w-4 animate-spin me-2\" />\n {t(\"billing.ui.wizard.review.processing\")}\n </>\n ) : (\n t(\"billing.ui.confirmation.subscribe\")\n )}\n </Button>\n </div>\n </div>\n </div>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { useCallback, useEffect, useRef } from \"react\";\nimport { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from \"../../../../../shadcnui\";\nimport { StripeSubscriptionInterface } from \"../../data\";\nimport { useSubscriptionWizard } from \"../../hooks/useSubscriptionWizard\";\nimport { WizardProgressIndicator } from \"./WizardProgressIndicator\";\nimport { WizardStepPlanSelection } from \"./WizardStepPlanSelection\";\nimport { WizardStepReview } from \"./WizardStepReview\";\nimport { WizardStepPaymentMethod } from \"./WizardStepPaymentMethod\";\n\nexport type SubscriptionWizardProps = {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n onSuccess: () => void;\n hasActiveRecurringSubscription: boolean;\n subscription?: StripeSubscriptionInterface;\n};\n\nexport function SubscriptionWizard({\n open,\n onOpenChange,\n onSuccess,\n hasActiveRecurringSubscription,\n subscription,\n}: SubscriptionWizardProps) {\n const t = useTranslations();\n const handleClose = useCallback(() => onOpenChange(false), [onOpenChange]);\n\n const { state, actions } = useSubscriptionWizard({\n subscription,\n onSuccess,\n onClose: handleClose,\n });\n\n // Track if we've already checked payment method for this open state\n const hasCheckedRef = useRef(false);\n\n // Check payment method on mount\n useEffect(() => {\n if (open && !hasCheckedRef.current) {\n hasCheckedRef.current = true;\n actions.checkPaymentMethod();\n }\n if (!open) {\n hasCheckedRef.current = false;\n }\n }, [open, actions.checkPaymentMethod]);\n\n // Reset state when dialog closes\n useEffect(() => {\n if (!open) {\n actions.reset();\n }\n }, [open, actions.reset]);\n\n const isPurchasingAddons = hasActiveRecurringSubscription && !subscription;\n const isChangePlanMode = !!subscription;\n\n const dialogTitle = subscription\n ? t(\"billing.ui.wizard.change_title\")\n : isPurchasingAddons\n ? t(\"billing.ui.subscriptions.purchase_addons\")\n : t(\"billing.ui.subscriptions.subscribe\");\n\n const dialogDescription = subscription\n ? t(\"billing.ui.wizard.change_description\")\n : isPurchasingAddons\n ? t(\"billing.ui.wizard.addons_description\")\n : t(\"billing.ui.wizard.subscribe_description\");\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent className=\"max-w-2xl max-h-[90vh] overflow-y-auto\">\n <DialogHeader>\n <DialogTitle>{dialogTitle}</DialogTitle>\n <DialogDescription>{dialogDescription}</DialogDescription>\n </DialogHeader>\n\n <WizardProgressIndicator currentStep={state.step} />\n\n {state.step === \"plan-selection\" && (\n <WizardStepPlanSelection\n selectedPrice={state.selectedPrice}\n selectedInterval={state.selectedInterval}\n currentPriceId={subscription?.price?.id}\n hideRecurringPrices={isPurchasingAddons}\n hideOneTimePrices={isChangePlanMode}\n onSelectPrice={actions.selectPrice}\n onIntervalChange={actions.setInterval}\n onNext={actions.goToReview}\n isProcessing={state.isProcessing}\n />\n )}\n\n {state.step === \"review\" && (\n <WizardStepReview\n selectedPrice={state.selectedPrice}\n subscription={subscription}\n prorationPreview={state.prorationPreview}\n hasPaymentMethod={state.hasPaymentMethod}\n error={state.error}\n isProcessing={state.isProcessing}\n onBack={() => actions.goToStep(\"plan-selection\")}\n onAddPaymentMethod={() => actions.goToStep(\"payment-method\")}\n onConfirm={actions.confirmSubscription}\n promotionCode={state.promotionCode}\n isValidatingPromoCode={state.isValidatingPromoCode}\n promoCodeError={state.promoCodeError}\n onApplyPromoCode={actions.validatePromoCode}\n onRemovePromoCode={actions.clearPromoCode}\n isTrialUpgrade={state.isTrialSubscription}\n />\n )}\n\n {state.step === \"payment-method\" && (\n <WizardStepPaymentMethod\n onBack={() => actions.goToStep(\"review\")}\n onSuccess={actions.handlePaymentMethodSuccess}\n isProcessing={state.isProcessing}\n />\n )}\n </DialogContent>\n </Dialog>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { useCallback, useMemo, useReducer, useRef } from \"react\";\nimport { v4 } from \"uuid\";\nimport { useI18nLocale } from \"../../../../i18n\";\nimport { StripeCustomerService } from \"../../stripe-customer/data/stripe-customer.service\";\nimport { ProrationPreviewInterface } from \"../../stripe-invoice/data/stripe-invoice.interface\";\nimport { StripePriceInterface } from \"../../stripe-price/data/stripe-price.interface\";\nimport { StripeCouponCodeService } from \"../../stripe-coupon/data/stripe-coupon-code.service\";\nimport { toPromotionCodeResult } from \"../../stripe-coupon/utils/to-promotion-code-result\";\nimport { PromotionCodeValidationResult, StripePromotionCodeService } from \"../../stripe-promotion-code\";\nimport { BillingInterval } from \"../components/widgets/IntervalToggle\";\nimport {\n StripeSubscriptionCreateResponse,\n StripeSubscriptionInterface,\n StripeSubscriptionService,\n SubscriptionStatus,\n} from \"../data\";\nimport {\n isDiscountSoldOut,\n isDiscountUnavailable,\n promotionCodeToSend,\n shouldLookUpAutomatic,\n} from \"./wizard-promotion\";\nimport { useConfirmSubscriptionPayment } from \"./useConfirmSubscriptionPayment\";\nimport { clientSecretToConfirm, subscriptionIdToSync } from \"./wizard-payment\";\n\nexport type WizardStep = \"plan-selection\" | \"review\" | \"payment-method\";\n\nexport type WizardState = {\n step: WizardStep;\n selectedPrice: StripePriceInterface | null;\n selectedInterval: BillingInterval;\n hasPaymentMethod: boolean;\n isProcessing: boolean;\n error: string | null;\n prorationPreview: ProrationPreviewInterface | null;\n // Promotion code state\n promotionCode: PromotionCodeValidationResult | null;\n isValidatingPromoCode: boolean;\n promoCodeError: string | null;\n // Trial state\n isTrialSubscription: boolean;\n};\n\ntype WizardAction =\n | { type: \"SET_STEP\"; step: WizardStep }\n | { type: \"SELECT_PRICE\"; price: StripePriceInterface }\n | { type: \"SET_INTERVAL\"; interval: BillingInterval }\n | { type: \"SET_HAS_PAYMENT_METHOD\"; hasMethod: boolean }\n | { type: \"SET_PROCESSING\"; isProcessing: boolean }\n | { type: \"SET_ERROR\"; error: string | null }\n | { type: \"SET_PRORATION_PREVIEW\"; preview: ProrationPreviewInterface | null }\n | { type: \"SET_PROMOTION_CODE\"; code: PromotionCodeValidationResult | null }\n | { type: \"SET_VALIDATING_PROMO_CODE\"; isValidating: boolean }\n | { type: \"SET_PROMO_CODE_ERROR\"; error: string | null }\n | { type: \"SET_IS_TRIAL_SUBSCRIPTION\"; isTrial: boolean }\n | { type: \"RESET\" };\n\nconst initialState: WizardState = {\n step: \"plan-selection\",\n selectedPrice: null,\n selectedInterval: \"month\",\n hasPaymentMethod: false,\n isProcessing: false,\n error: null,\n prorationPreview: null,\n promotionCode: null,\n isValidatingPromoCode: false,\n promoCodeError: null,\n isTrialSubscription: false,\n};\n\nfunction wizardReducer(state: WizardState, action: WizardAction): WizardState {\n switch (action.type) {\n case \"SET_STEP\":\n return { ...state, step: action.step, error: null };\n case \"SELECT_PRICE\":\n return { ...state, selectedPrice: action.price };\n case \"SET_INTERVAL\":\n return { ...state, selectedInterval: action.interval };\n case \"SET_HAS_PAYMENT_METHOD\":\n return { ...state, hasPaymentMethod: action.hasMethod };\n case \"SET_PROCESSING\":\n return { ...state, isProcessing: action.isProcessing };\n case \"SET_ERROR\":\n return { ...state, error: action.error };\n case \"SET_PRORATION_PREVIEW\":\n return { ...state, prorationPreview: action.preview };\n case \"SET_PROMOTION_CODE\":\n return { ...state, promotionCode: action.code, promoCodeError: null };\n case \"SET_VALIDATING_PROMO_CODE\":\n return { ...state, isValidatingPromoCode: action.isValidating };\n case \"SET_PROMO_CODE_ERROR\":\n return { ...state, promoCodeError: action.error };\n case \"SET_IS_TRIAL_SUBSCRIPTION\":\n return { ...state, isTrialSubscription: action.isTrial };\n case \"RESET\":\n return initialState;\n default:\n return state;\n }\n}\n\nexport type UseSubscriptionWizardOptions = {\n subscription?: StripeSubscriptionInterface;\n onSuccess: () => void;\n onClose: () => void;\n};\n\nexport function useSubscriptionWizard({ subscription, onSuccess, onClose }: UseSubscriptionWizardOptions) {\n const t = useTranslations();\n const locale = useI18nLocale();\n const { confirmPayment } = useConfirmSubscriptionPayment();\n const [state, dispatch] = useReducer(wizardReducer, {\n ...initialState,\n selectedPrice: subscription?.price || null,\n });\n\n // Use refs for callbacks to avoid dependency changes\n const onSuccessRef = useRef(onSuccess);\n const onCloseRef = useRef(onClose);\n onSuccessRef.current = onSuccess;\n onCloseRef.current = onClose;\n\n const checkPaymentMethod = useCallback(async () => {\n try {\n const methods = await StripeCustomerService.listPaymentMethods();\n dispatch({ type: \"SET_HAS_PAYMENT_METHOD\", hasMethod: methods.length > 0 });\n } catch (error) {\n console.error(\"[useSubscriptionWizard] Failed to check payment methods:\", error);\n dispatch({ type: \"SET_HAS_PAYMENT_METHOD\", hasMethod: false });\n }\n }, []);\n\n const selectPrice = useCallback((price: StripePriceInterface) => {\n dispatch({ type: \"SELECT_PRICE\", price });\n }, []);\n\n const setInterval = useCallback((interval: BillingInterval) => {\n dispatch({ type: \"SET_INTERVAL\", interval });\n }, []);\n\n const goToStep = useCallback((step: WizardStep) => {\n dispatch({ type: \"SET_STEP\", step });\n }, []);\n\n /**\n * Puts the automatic code for this price (or none) in the wizard state, so the\n * review shows what the server would really charge. Returns false when the\n * lookup itself failed: the caller must then not show a price as the truth.\n */\n const lookUpAutomaticCode = useCallback(async (priceId: string): Promise<boolean> => {\n try {\n const automaticCode = await StripeCouponCodeService.findAutomatic({ priceId });\n dispatch({\n type: \"SET_PROMOTION_CODE\",\n code: automaticCode ? toPromotionCodeResult(automaticCode) : null,\n });\n return true;\n } catch (error) {\n console.error(\"[useSubscriptionWizard] Failed to look up the automatic promotion code:\", error);\n dispatch({ type: \"SET_PROMOTION_CODE\", code: null });\n return false;\n }\n }, []);\n\n /**\n * Re-reads the automatic code after the review's code changed. When the lookup\n * fails, the review price cannot be trusted: back to plan selection.\n */\n const refreshAutomaticCodeOrLeaveReview = useCallback(\n async (priceId: string): Promise<void> => {\n const found = await lookUpAutomaticCode(priceId);\n if (!found) {\n // SET_STEP clears the error, so the error goes second\n dispatch({ type: \"SET_STEP\", step: \"plan-selection\" });\n dispatch({ type: \"SET_ERROR\", error: t(\"billing.ui.wizard.errors.prepare_review\") });\n }\n },\n [lookUpAutomaticCode, t],\n );\n\n const goToReview = useCallback(async () => {\n if (!state.selectedPrice) return;\n\n dispatch({ type: \"SET_PROCESSING\", isProcessing: true });\n\n try {\n // Check payment method first\n await checkPaymentMethod();\n\n // Check if current subscription is trial\n const isTrialUpgrade = subscription?.status === SubscriptionStatus.TRIALING;\n dispatch({ type: \"SET_IS_TRIAL_SUBSCRIPTION\", isTrial: isTrialUpgrade });\n\n // For trial upgrades, require payment method before review\n if (isTrialUpgrade && !state.hasPaymentMethod) {\n const methods = await StripeCustomerService.listPaymentMethods();\n if (methods.length === 0) {\n dispatch({ type: \"SET_STEP\", step: \"payment-method\" });\n dispatch({\n type: \"SET_ERROR\",\n error: t(\"billing.ui.wizard.errors.payment_method_required\"),\n });\n return;\n }\n }\n\n // Fill the automatic code for this price, unless the GM typed one\n // A failed lookup never shows full price as if nothing applied: stay off the review\n if (shouldLookUpAutomatic(state.promotionCode)) {\n const found = await lookUpAutomaticCode(state.selectedPrice.id);\n if (!found) {\n dispatch({ type: \"SET_ERROR\", error: t(\"billing.ui.wizard.errors.prepare_review\") });\n return;\n }\n }\n\n // If editing subscription, get proration preview\n if (subscription && state.selectedPrice.id !== subscription.price?.id) {\n const preview = await StripeSubscriptionService.getProrationPreview({\n subscriptionId: subscription.id,\n newPriceId: state.selectedPrice.id,\n });\n dispatch({ type: \"SET_PRORATION_PREVIEW\", preview });\n }\n\n dispatch({ type: \"SET_STEP\", step: \"review\" });\n } catch (error: any) {\n console.error(\"[useSubscriptionWizard] Error preparing review:\", error);\n dispatch({ type: \"SET_ERROR\", error: error?.message || t(\"billing.ui.wizard.errors.prepare_review\") });\n } finally {\n dispatch({ type: \"SET_PROCESSING\", isProcessing: false });\n }\n }, [\n state.selectedPrice,\n state.hasPaymentMethod,\n state.promotionCode,\n subscription,\n checkPaymentMethod,\n lookUpAutomaticCode,\n t,\n ]);\n\n const confirmSubscription = useCallback(async () => {\n if (!state.selectedPrice) return;\n\n dispatch({ type: \"SET_PROCESSING\", isProcessing: true });\n dispatch({ type: \"SET_ERROR\", error: null });\n\n try {\n let result: StripeSubscriptionCreateResponse;\n if (subscription) {\n // Change existing subscription\n const changePlanParams = {\n id: subscription.id,\n newPriceId: state.selectedPrice.id,\n promotionCode: promotionCodeToSend(state.promotionCode),\n };\n result = await StripeSubscriptionService.changePlanWithMeta(changePlanParams);\n } else {\n // Create new subscription\n const createParams = {\n id: v4(),\n priceId: state.selectedPrice.id,\n promotionCode: promotionCodeToSend(state.promotionCode),\n };\n result = await StripeSubscriptionService.createSubscription(createParams);\n }\n\n // The first payment waits on the browser (confirmation, 3D Secure): until it\n // is confirmed nothing is paid, so the wizard stays open on a failure\n const clientSecret = clientSecretToConfirm(result.meta);\n if (clientSecret) {\n const confirmation = await confirmPayment(clientSecret);\n if (!confirmation.success) {\n console.error(\"[useSubscriptionWizard] Payment not confirmed:\", confirmation.error);\n dispatch({\n type: \"SET_ERROR\",\n error: t(\"billing.ui.wizard.errors.payment_not_confirmed\", { reason: confirmation.error ?? \"\" }),\n });\n return;\n }\n\n // Our copy is still incomplete until the webhooks land: store what Stripe has now,\n // so the billing page shows the plan at once. A failed sync never blocks success.\n const subscriptionId = subscriptionIdToSync(result.subscription, subscription);\n if (subscriptionId) {\n try {\n await StripeSubscriptionService.syncSubscription({ subscriptionId });\n } catch (syncError) {\n console.error(\"[useSubscriptionWizard] Subscription sync after payment failed:\", syncError);\n }\n }\n }\n\n onSuccessRef.current();\n onCloseRef.current();\n } catch (error: any) {\n // 410 discount_sold_out (the automatic discount's last place has just gone) or\n // 422 discount_unavailable (the automatic code shown is no longer valid): nothing\n // was charged. Never retry: drop the code and re-read the automatic one, so the\n // review shows the price that would really be charged now.\n const soldOut = isDiscountSoldOut(error);\n if (soldOut || isDiscountUnavailable(error)) {\n dispatch({\n type: \"SET_ERROR\",\n error: soldOut\n ? t(\"billing.ui.wizard.errors.discount_sold_out\")\n : t(\"billing.ui.wizard.errors.discount_unavailable\"),\n });\n dispatch({ type: \"SET_PROMOTION_CODE\", code: null });\n await refreshAutomaticCodeOrLeaveReview(state.selectedPrice.id);\n return;\n }\n\n // Handle 409 Conflict - duplicate recurring subscription\n if (error?.status === 409 || error?.response?.status === 409) {\n dispatch({\n type: \"SET_ERROR\",\n error: t(\"billing.ui.wizard.errors.already_subscribed\"),\n });\n return;\n }\n\n // Handle 402 - payment method required\n if (error?.status === 402 || error?.response?.status === 402) {\n dispatch({ type: \"SET_HAS_PAYMENT_METHOD\", hasMethod: false });\n dispatch({ type: \"SET_STEP\", step: \"payment-method\" });\n return;\n }\n\n dispatch({ type: \"SET_ERROR\", error: error?.message || t(\"billing.ui.wizard.errors.process_subscription\") });\n } finally {\n dispatch({ type: \"SET_PROCESSING\", isProcessing: false });\n }\n }, [state.selectedPrice, state.promotionCode, subscription, refreshAutomaticCodeOrLeaveReview, confirmPayment, t]);\n\n const handlePaymentMethodSuccess = useCallback(async () => {\n dispatch({ type: \"SET_HAS_PAYMENT_METHOD\", hasMethod: true });\n // Prepare the review exactly as \"Next: Review\" does: the trial path sent the GM\n // here before the automatic-code lookup and the proration preview had run\n await goToReview();\n }, [goToReview]);\n\n const reset = useCallback(() => {\n dispatch({ type: \"RESET\" });\n }, []);\n\n const validatePromoCode = useCallback(\n async (code: string) => {\n dispatch({ type: \"SET_VALIDATING_PROMO_CODE\", isValidating: true });\n dispatch({ type: \"SET_PROMO_CODE_ERROR\", error: null });\n\n try {\n const result = await StripePromotionCodeService.validatePromotionCode({\n code,\n stripePriceId: state.selectedPrice?.id,\n language: locale,\n });\n\n if (result.valid) {\n dispatch({ type: \"SET_PROMOTION_CODE\", code: result });\n } else {\n dispatch({\n type: \"SET_PROMO_CODE_ERROR\",\n error: result.errorMessage || t(\"billing.ui.wizard.errors.invalid_promo_code\"),\n });\n }\n } catch (error: any) {\n console.error(\"[useSubscriptionWizard] Promo code validation error:\", error);\n dispatch({\n type: \"SET_PROMO_CODE_ERROR\",\n error: error?.message || t(\"billing.ui.wizard.errors.validate_promo_code\"),\n });\n } finally {\n dispatch({ type: \"SET_VALIDATING_PROMO_CODE\", isValidating: false });\n }\n },\n [state.selectedPrice?.id, t, locale],\n );\n\n const clearPromoCode = useCallback(async () => {\n dispatch({ type: \"SET_PROMOTION_CODE\", code: null });\n dispatch({ type: \"SET_PROMO_CODE_ERROR\", error: null });\n dispatch({ type: \"SET_ERROR\", error: null });\n\n if (!state.selectedPrice) return;\n\n // Removing a typed code: the server attaches the automatic code again, so the\n // review must show it again\n dispatch({ type: \"SET_PROCESSING\", isProcessing: true });\n try {\n await refreshAutomaticCodeOrLeaveReview(state.selectedPrice.id);\n } finally {\n dispatch({ type: \"SET_PROCESSING\", isProcessing: false });\n }\n }, [state.selectedPrice, refreshAutomaticCodeOrLeaveReview]);\n\n const actions = useMemo(\n () => ({\n selectPrice,\n setInterval,\n goToStep,\n goToReview,\n confirmSubscription,\n handlePaymentMethodSuccess,\n checkPaymentMethod,\n reset,\n validatePromoCode,\n clearPromoCode,\n }),\n [\n selectPrice,\n setInterval,\n goToStep,\n goToReview,\n confirmSubscription,\n handlePaymentMethodSuccess,\n checkPaymentMethod,\n reset,\n validatePromoCode,\n clearPromoCode,\n ],\n );\n\n return {\n state,\n actions,\n };\n}\n","import { PromotionCodeValidationResult } from \"../../stripe-promotion-code/data/stripe-promotion-code.interface\";\n\n/**\n * The promotion code the wizard sends to the API: the one the review shows,\n * typed or automatic. Sending the automatic code too lets the server refuse\n * the payment when that code is no longer valid, instead of charging a price\n * the GM was not shown.\n */\nexport function promotionCodeToSend(code: PromotionCodeValidationResult | null): string | undefined {\n if (!code) return undefined;\n return code.promotionCodeId;\n}\n\n/**\n * True when the wizard should ask for the automatic code: no code yet, or the\n * current one is automatic. A code the GM typed always wins.\n */\nexport function shouldLookUpAutomatic(code: PromotionCodeValidationResult | null): boolean {\n return !code || code.automatic === true;\n}\n\n/** `value[key]` when `value` is an object, else undefined. */\nfunction read(value: unknown, key: string): unknown {\n return typeof value === \"object\" && value !== null ? Reflect.get(value, key) : undefined;\n}\n\nfunction errorStatus(error: unknown): number | undefined {\n const status = read(error, \"status\") ?? read(read(error, \"response\"), \"status\");\n return typeof status === \"number\" ? status : undefined;\n}\n\n/**\n * The `code` of an API error. The nestjs-neo4jsonapi `HttpExceptionFilter`\n * answers with a JSON:API error document, which `AbstractService.callApi`\n * attaches to the thrown error as `body`:\n * `{ message, errors: [{ status, code, title, detail, meta: { context: { code } } }] }`.\n */\nfunction errorCode(error: unknown): string | undefined {\n const body = read(error, \"body\");\n const errors = read(body, \"errors\");\n const first = Array.isArray(errors) ? errors[0] : undefined;\n const code = read(first, \"code\") ?? read(read(read(first, \"meta\"), \"context\"), \"code\") ?? read(body, \"code\");\n return typeof code === \"string\" ? code : undefined;\n}\n\n/**\n * True when the API refused the payment because the automatic discount has no\n * places left (HTTP 410, code `discount_sold_out`). Nothing was charged.\n */\nexport function isDiscountSoldOut(error: unknown): boolean {\n return errorStatus(error) === 410 && errorCode(error) === \"discount_sold_out\";\n}\n\n/**\n * True when the API refused the payment because the automatic code the review\n * showed is no longer valid (HTTP 422, code `discount_unavailable`). Nothing was\n * charged.\n */\nexport function isDiscountUnavailable(error: unknown): boolean {\n return errorStatus(error) === 422 && errorCode(error) === \"discount_unavailable\";\n}\n","\"use client\";\n\nimport { useStripe } from \"@stripe/react-stripe-js\";\nimport { useTranslations } from \"next-intl\";\nimport { useCallback, useState } from \"react\";\n\nexport interface ConfirmPaymentResult {\n success: boolean;\n error?: string;\n}\n\nexport interface UseConfirmSubscriptionPaymentReturn {\n confirmPayment: (clientSecret: string) => Promise<ConfirmPaymentResult>;\n isConfirming: boolean;\n}\n\n/**\n * Hook for confirming subscription payments using Stripe's SCA-compliant flow.\n *\n * This hook wraps stripe.confirmCardPayment() and handles:\n * - Loading state during confirmation\n * - Error handling for Stripe errors\n * - 3D Secure authentication popups (handled automatically by Stripe)\n *\n * @example\n * ```tsx\n * const { confirmPayment, isConfirming } = useConfirmSubscriptionPayment();\n *\n * const handleCreateSubscription = async () => {\n * const result = await StripeSubscriptionService.createSubscription({ priceId });\n *\n * if (result.meta.requiresAction && result.meta.clientSecret) {\n * const confirmation = await confirmPayment(result.meta.clientSecret);\n * if (!confirmation.success) {\n * setError(confirmation.error);\n * return;\n * }\n * }\n *\n * // Subscription is now active\n * };\n * ```\n */\nexport function useConfirmSubscriptionPayment(): UseConfirmSubscriptionPaymentReturn {\n const stripe = useStripe();\n const t = useTranslations();\n const [isConfirming, setIsConfirming] = useState(false);\n\n const confirmPayment = useCallback(\n async (clientSecret: string): Promise<ConfirmPaymentResult> => {\n if (!stripe) {\n console.error(\"[useConfirmSubscriptionPayment] Stripe not initialized\");\n return {\n success: false,\n error: t(\"billing.ui.payment_confirm.errors.not_initialized\"),\n };\n }\n\n if (!clientSecret) {\n console.error(\"[useConfirmSubscriptionPayment] No client secret provided\");\n return {\n success: false,\n error: t(\"billing.ui.payment_confirm.errors.missing_details\"),\n };\n }\n\n setIsConfirming(true);\n\n try {\n const { error: stripeError, paymentIntent } = await stripe.confirmCardPayment(clientSecret);\n\n if (stripeError) {\n console.error(\"[useConfirmSubscriptionPayment] Stripe error:\", stripeError);\n return {\n success: false,\n error: stripeError.message || t(\"billing.ui.payment_confirm.errors.failed\"),\n };\n }\n\n if (paymentIntent?.status === \"succeeded\") {\n return { success: true };\n }\n\n if (paymentIntent?.status === \"requires_action\") {\n // This shouldn't happen as Stripe handles 3DS inline, but handle it just in case\n return {\n success: false,\n error: t(\"billing.ui.payment_confirm.errors.requires_action\"),\n };\n }\n\n return {\n success: false,\n error: t(\"billing.ui.payment_confirm.errors.not_completed\"),\n };\n } catch (err: any) {\n console.error(\"[useConfirmSubscriptionPayment] Unexpected error:\", err);\n return {\n success: false,\n error: err.message || t(\"billing.ui.payment_confirm.errors.unexpected\"),\n };\n } finally {\n setIsConfirming(false);\n }\n },\n [stripe, t],\n );\n\n return {\n confirmPayment,\n isConfirming,\n };\n}\n","import { StripeSubscriptionCreateMeta } from \"../data/stripe-subscription.interface\";\n\n/**\n * The client secret the wizard must confirm in the browser after a subscribe\n * or a trial upgrade, or null when the payment needs nothing more.\n *\n * The API creates a subscription with `payment_behavior: \"default_incomplete\"`:\n * its first PaymentIntent waits on the browser (`requires_confirmation`, or 3D\n * Secure). Until it is confirmed nothing is paid and the subscription stays\n * `incomplete`, so closing the wizard on the API answer alone is wrong.\n */\nexport function clientSecretToConfirm(meta: Partial<StripeSubscriptionCreateMeta> | null | undefined): string | null {\n if (!meta?.requiresAction) return null;\n return meta.clientSecret || null;\n}\n\n/**\n * The subscription to re-read from Stripe after the browser confirmed its first\n * payment: the one the API just answered with (subscribe), else the one the wizard\n * was opened on (trial upgrade). Null when there is none to sync.\n *\n * Right after the confirmation our copy is still `incomplete` (the webhooks land a\n * second later), so the billing page would show no plan until a reload.\n */\nexport function subscriptionIdToSync(\n answered: { id?: string } | null | undefined,\n opened: { id?: string } | null | undefined,\n): string | null {\n return answered?.id || opened?.id || null;\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { Check } from \"lucide-react\";\nimport { WizardStep } from \"../../hooks/useSubscriptionWizard\";\n\ntype WizardProgressIndicatorProps = {\n currentStep: WizardStep;\n};\n\n// `label` is a message key, translated at render time.\nconst STEPS: { key: WizardStep; label: string }[] = [\n { key: \"plan-selection\", label: \"billing.ui.wizard.steps.select_plan\" },\n { key: \"review\", label: \"billing.ui.wizard.steps.review\" },\n { key: \"payment-method\", label: \"billing.ui.wizard.steps.payment\" },\n];\n\nfunction getStepIndex(step: WizardStep): number {\n return STEPS.findIndex((s) => s.key === step);\n}\n\nexport function WizardProgressIndicator({ currentStep }: WizardProgressIndicatorProps) {\n const t = useTranslations();\n const currentIndex = getStepIndex(currentStep);\n\n return (\n <div className=\"flex items-center justify-center gap-x-2 py-4\">\n {STEPS.map((step, index) => {\n const isCompleted = index < currentIndex;\n const isCurrent = index === currentIndex;\n\n return (\n <div key={step.key} className=\"flex items-center gap-x-2\">\n {/* Step Indicator */}\n <div\n className={`flex h-8 w-8 items-center justify-center rounded-full text-sm font-medium ${\n isCompleted\n ? \"bg-primary text-primary-foreground\"\n : isCurrent\n ? \"bg-primary text-primary-foreground\"\n : \"bg-muted text-muted-foreground\"\n }`}\n >\n {isCompleted ? <Check className=\"h-4 w-4\" /> : index + 1}\n </div>\n\n {/* Step Label */}\n <span className={`text-sm ${isCurrent ? \"font-medium text-foreground\" : \"text-muted-foreground\"}`}>\n {t(step.label)}\n </span>\n\n {/* Connector */}\n {index < STEPS.length - 1 && (\n <div className={`h-0.5 w-8 ${index < currentIndex ? \"bg-primary\" : \"bg-muted\"}`} />\n )}\n </div>\n );\n })}\n </div>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { useEffect, useMemo, useState } from \"react\";\nimport { Button } from \"../../../../../shadcnui\";\nimport { StripeProductInterface, StripeProductService } from \"../../../stripe-product\";\nimport { StripePriceInterface } from \"../../../stripe-price/data/stripe-price.interface\";\nimport { BillingInterval, IntervalToggle } from \"../widgets/IntervalToggle\";\nimport { ProductPricingList } from \"../widgets/ProductPricingList\";\n\ntype WizardStepPlanSelectionProps = {\n selectedPrice: StripePriceInterface | null;\n selectedInterval: BillingInterval;\n currentPriceId?: string;\n hideRecurringPrices: boolean;\n hideOneTimePrices: boolean;\n onSelectPrice: (price: StripePriceInterface) => void;\n onIntervalChange: (interval: BillingInterval) => void;\n onNext: () => void;\n isProcessing: boolean;\n};\n\nexport function WizardStepPlanSelection({\n selectedPrice,\n selectedInterval,\n currentPriceId,\n hideRecurringPrices,\n hideOneTimePrices,\n onSelectPrice,\n onIntervalChange,\n onNext,\n isProcessing,\n}: WizardStepPlanSelectionProps) {\n const t = useTranslations();\n const [products, setProducts] = useState<StripeProductInterface[]>([]);\n const [loading, setLoading] = useState(true);\n\n useEffect(() => {\n const loadProducts = async () => {\n try {\n const fetchedProducts = await StripeProductService.listProducts({ active: true });\n setProducts(fetchedProducts);\n } catch (error) {\n console.error(\"[WizardStepPlanSelection] Failed to load products:\", error);\n } finally {\n setLoading(false);\n }\n };\n\n loadProducts();\n }, []);\n\n // Compute available intervals from products\n const { hasMonthly, hasYearly } = useMemo(() => {\n let hasMonthly = false;\n let hasYearly = false;\n\n for (const product of products) {\n for (const price of product.stripePrices || []) {\n if (price.priceType === \"recurring\" && price.recurring?.interval === \"month\") {\n hasMonthly = true;\n }\n if (price.priceType === \"recurring\" && price.recurring?.interval === \"year\") {\n hasYearly = true;\n }\n }\n }\n\n return { hasMonthly, hasYearly };\n }, [products]);\n\n const handleSelectPrice = (price: StripePriceInterface) => {\n onSelectPrice(price);\n };\n\n return (\n <div className=\"space-y-6\">\n {/* Interval Toggle - only show for recurring products */}\n {!hideRecurringPrices && (\n <div className=\"flex justify-center\">\n <IntervalToggle\n value={selectedInterval}\n onChange={onIntervalChange}\n hasMonthly={hasMonthly}\n hasYearly={hasYearly}\n />\n </div>\n )}\n\n {/* Product Pricing List */}\n <ProductPricingList\n products={products}\n selectedInterval={selectedInterval}\n currentPriceId={currentPriceId}\n selectedPriceId={selectedPrice?.id}\n loading={loading}\n hideRecurringPrices={hideRecurringPrices}\n hideOneTimePrices={hideOneTimePrices}\n onSelectPrice={handleSelectPrice}\n />\n\n {/* Action Buttons */}\n <div className=\"flex justify-end pt-4 border-t\">\n <Button onClick={onNext} disabled={!selectedPrice || isProcessing}>\n {isProcessing ? t(\"common.loading\") : t(\"billing.ui.wizard.next_review\")}\n </Button>\n </div>\n </div>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { AlertCircle } from \"lucide-react\";\nimport { useEffect, useState } from \"react\";\nimport { Alert, AlertDescription, Badge, Button } from \"../../../../../shadcnui\";\nimport { SectionHeader } from \"../../../../../components/typography\";\nimport {\n applyDiscount,\n formatCurrency,\n promotionCodeDiscount,\n subscriptionDiscount,\n useBillingFormatOptions,\n} from \"../../../components/utils\";\nimport { StripePriceInterface } from \"../../../stripe-price/data/stripe-price.interface\";\nimport { ProrationPreviewInterface } from \"../../../stripe-invoice/data/stripe-invoice.interface\";\nimport { PromotionCodeValidationResult } from \"../../../stripe-promotion-code\";\nimport { PromoCodeInput } from \"../../../stripe-promotion-code/components/PromoCodeInput\";\nimport { StripeSubscriptionInterface } from \"../../data\";\nimport { StripeCustomerService } from \"../../../stripe-customer/data/stripe-customer.service\";\nimport { ReviewCard, reviewCard } from \"./review-card\";\n\ntype WizardStepReviewProps = {\n selectedPrice: StripePriceInterface | null;\n subscription?: StripeSubscriptionInterface;\n prorationPreview: ProrationPreviewInterface | null;\n hasPaymentMethod: boolean;\n error: string | null;\n isProcessing: boolean;\n onBack: () => void;\n onAddPaymentMethod: () => void;\n onConfirm: () => void;\n // Promotion code props\n promotionCode: PromotionCodeValidationResult | null;\n isValidatingPromoCode: boolean;\n promoCodeError: string | null;\n onApplyPromoCode: (code: string) => void;\n onRemovePromoCode: () => void;\n // Trial upgrade flag\n isTrialUpgrade: boolean;\n};\n\nexport function WizardStepReview({\n selectedPrice,\n subscription,\n prorationPreview,\n hasPaymentMethod,\n error,\n isProcessing,\n onBack,\n onAddPaymentMethod,\n onConfirm,\n promotionCode,\n isValidatingPromoCode,\n promoCodeError,\n onApplyPromoCode,\n onRemovePromoCode,\n isTrialUpgrade,\n}: WizardStepReviewProps) {\n const t = useTranslations();\n const fmt = useBillingFormatOptions();\n const [card, setCard] = useState<ReviewCard | null>(null);\n\n // Show which card is on file. The step remounts after the payment step, so a\n // card just added is read again here.\n useEffect(() => {\n if (!hasPaymentMethod) {\n setCard(null);\n return;\n }\n let cancelled = false;\n Promise.all([StripeCustomerService.listPaymentMethods(), StripeCustomerService.getCustomer()])\n .then(([methods, customer]) => {\n if (!cancelled) setCard(reviewCard(methods, customer?.defaultPaymentMethodId));\n })\n .catch((err) => {\n console.error(\"[WizardStepReview] Failed to load the card on file:\", err);\n if (!cancelled) setCard(null);\n });\n return () => {\n cancelled = true;\n };\n }, [hasPaymentMethod]);\n\n if (!selectedPrice) {\n return <div className=\"text-center py-8 text-muted-foreground\">{t(\"billing.ui.wizard.review.no_plan\")}</div>;\n }\n\n const isChangingPlan = subscription && subscription.price?.id !== selectedPrice.id;\n\n const formatInterval = (price: StripePriceInterface) => {\n if (price.priceType === \"one_time\") return t(\"billing.ui.wizard.review.interval_one_time\");\n const interval = price.recurring?.interval || \"month\";\n return interval === \"year\"\n ? t(\"billing.ui.wizard.review.interval_yearly\")\n : t(\"billing.ui.wizard.review.interval_monthly\");\n };\n\n // A code (typed or automatic) replaces the subscription's discount in Stripe.\n // Without one, a plan change keeps the discount the subscription already carries.\n const codeDiscount = promotionCodeDiscount(promotionCode);\n const carriedDiscount = isChangingPlan && !codeDiscount ? subscriptionDiscount(subscription) : null;\n const discount = codeDiscount ?? carriedDiscount;\n\n const discountedPrice = applyDiscount(selectedPrice.unitAmount, discount);\n\n // Calculate discounted immediate charge for proration preview\n const discountedImmediateCharge = applyDiscount(prorationPreview?.immediateCharge, discount);\n\n // Format the discount description\n const getDiscountDescription = (): string | null => {\n if (!discount) return null;\n\n if (discount.discountType === \"percent_off\") {\n return t(\"billing.ui.wizard.review.percent_off\", { value: discount.discountValue });\n }\n\n return t(\"billing.ui.wizard.review.amount_off\", {\n amount: formatCurrency(discount.discountValue, discount.currency || selectedPrice.currency, fmt),\n });\n };\n\n const discountDescription = getDiscountDescription();\n\n return (\n <div className=\"space-y-6\">\n {/* Selected Plan Summary */}\n <div className=\"bg-muted/50 rounded-lg p-4 space-y-3\">\n <SectionHeader>{t(\"billing.ui.wizard.review.selected_plan\")}</SectionHeader>\n <div className=\"flex justify-between items-center\">\n <div>\n <p className=\"font-medium\">{selectedPrice.product?.name}</p>\n {selectedPrice.nickname && <p className=\"text-sm text-muted-foreground\">{selectedPrice.nickname}</p>}\n </div>\n <div className=\"text-end\">\n {discountedPrice !== null ? (\n <>\n <p className=\"text-sm text-muted-foreground line-through\">\n {formatCurrency(selectedPrice.unitAmount || 0, selectedPrice.currency, fmt)}\n </p>\n <p className=\"text-success text-lg font-semibold\">\n {formatCurrency(discountedPrice, selectedPrice.currency, fmt)}\n </p>\n {discountDescription && <Badge variant=\"softGreen\">{discountDescription}</Badge>}\n {carriedDiscount && (\n <p className=\"text-sm text-muted-foreground\">{t(\"billing.ui.wizard.review.discount_carries_over\")}</p>\n )}\n </>\n ) : (\n <p className=\"font-semibold text-lg\">\n {formatCurrency(selectedPrice.unitAmount || 0, selectedPrice.currency, fmt)}\n </p>\n )}\n <p className=\"text-sm text-muted-foreground\">{formatInterval(selectedPrice)}</p>\n </div>\n </div>\n </div>\n\n {/* Proration Preview (for plan changes) */}\n {isChangingPlan && prorationPreview && (\n <div\n className={`${isTrialUpgrade ? \"bg-warning/15 border-warning/30\" : \"bg-primary/10 border-primary/30\"} space-y-2 rounded-lg border p-4`}\n >\n <h4 className={`font-medium ${isTrialUpgrade ? \"text-warning\" : \"text-primary\"}`}>\n {isTrialUpgrade\n ? t(\"billing.ui.wizard.review.trial_upgrade\")\n : t(\"billing.ui.wizard.review.proration_summary\")}\n </h4>\n <p className={`text-sm ${isTrialUpgrade ? \"text-warning\" : \"text-foreground\"}`}>\n {isTrialUpgrade\n ? discountedImmediateCharge !== null\n ? t(\"billing.ui.wizard.review.trial_upgrade_description_discounted\")\n : t(\"billing.ui.wizard.review.trial_upgrade_description\")\n : t(\"billing.ui.wizard.review.proration_description\")}\n </p>\n <div className=\"flex justify-between text-sm\">\n <span className={`${isTrialUpgrade ? \"text-warning\" : \"text-muted-foreground\"}`}>\n {isTrialUpgrade ? t(\"billing.ui.wizard.review.charge_now\") : t(\"billing.ui.wizard.review.due_now\")}\n </span>\n <span className={`font-medium ${isTrialUpgrade ? \"text-warning\" : \"text-foreground\"}`}>\n {discountedImmediateCharge !== null ? (\n <>\n <span className=\"line-through text-muted-foreground me-2\">\n {formatCurrency(prorationPreview.immediateCharge, prorationPreview.currency, fmt)}\n </span>\n <span className=\"text-success\">\n {formatCurrency(discountedImmediateCharge, prorationPreview.currency, fmt)}\n </span>\n </>\n ) : (\n formatCurrency(prorationPreview.immediateCharge, prorationPreview.currency, fmt)\n )}\n </span>\n </div>\n </div>\n )}\n\n {/* Promotion Code */}\n <div className=\"border rounded-lg p-4 space-y-3\">\n <h4 className=\"font-medium\">{t(\"billing.ui.wizard.review.promotion_code\")}</h4>\n <PromoCodeInput\n appliedCode={promotionCode}\n isValidating={isValidatingPromoCode}\n error={promoCodeError}\n onApply={onApplyPromoCode}\n onRemove={onRemovePromoCode}\n disabled={isProcessing}\n />\n </div>\n\n {/* Payment Method Status */}\n <div className=\"border rounded-lg p-4\">\n <div className=\"flex justify-between items-center\">\n <div>\n <h4 className=\"font-medium\">{t(\"billing.ui.payment_method.title\")}</h4>\n <p className=\"text-sm text-muted-foreground\">\n {hasPaymentMethod\n ? card\n ? t(\"billing.ui.wizard.review.card_on_file\", { brand: card.brand, last4: card.last4 })\n : t(\"billing.ui.wizard.review.payment_on_file\")\n : t(\"billing.ui.wizard.review.no_payment_on_file\")}\n </p>\n </div>\n {hasPaymentMethod ? (\n <Button variant=\"outline\" onClick={onAddPaymentMethod} disabled={isProcessing}>\n {t(\"billing.ui.wizard.review.use_another_card\")}\n </Button>\n ) : (\n <Button variant=\"outline\" onClick={onAddPaymentMethod}>\n {t(\"billing.ui.payment_methods.add\")}\n </Button>\n )}\n </div>\n </div>\n\n {/* Error Display */}\n {error && (\n <Alert variant=\"destructive\">\n <AlertCircle className=\"h-4 w-4\" />\n <AlertDescription>{error}</AlertDescription>\n </Alert>\n )}\n\n {/* Action Buttons */}\n <div className=\"flex justify-between pt-4 border-t\">\n <Button variant=\"outline\" onClick={onBack} disabled={isProcessing}>\n {t(\"billing.ui.wizard.back\")}\n </Button>\n <Button onClick={hasPaymentMethod ? onConfirm : onAddPaymentMethod} disabled={isProcessing}>\n {isProcessing\n ? t(\"billing.ui.wizard.review.processing\")\n : hasPaymentMethod\n ? isChangingPlan\n ? t(\"billing.ui.wizard.review.confirm_change\")\n : t(\"billing.ui.wizard.review.subscribe_now\")\n : t(\"billing.ui.payment_methods.add\")}\n </Button>\n </div>\n </div>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { useState } from \"react\";\nimport { Check, X, Loader2 } from \"lucide-react\";\nimport { Button, Input } from \"../../../../shadcnui\";\nimport { useI18nLocale } from \"../../../../i18n\";\nimport { formatFixed } from \"../../components/utils\";\nimport { PromotionCodeValidationResult } from \"../data/stripe-promotion-code.interface\";\n\ntype PromoCodeInputProps = {\n appliedCode: PromotionCodeValidationResult | null;\n isValidating: boolean;\n error: string | null;\n onApply: (code: string) => void;\n onRemove: () => void;\n disabled?: boolean;\n};\n\nexport function PromoCodeInput({\n appliedCode,\n isValidating,\n error,\n onApply,\n onRemove,\n disabled = false,\n}: PromoCodeInputProps) {\n const t = useTranslations();\n const locale = useI18nLocale();\n const [code, setCode] = useState(\"\");\n\n // Clear the typed text whenever a code becomes applied (typed or automatic):\n // the box then reads empty after the applied code is removed, instead of\n // still holding the removed code. Adjusting state during render on a prop\n // change (React's \"storing information from previous renders\" pattern) —\n // a failed validation keeps the text so the user can fix a typo.\n const appliedKey = appliedCode?.valid\n ? `${appliedCode.automatic ? \"auto\" : \"typed\"}:${appliedCode.code ?? \"\"}`\n : null;\n const [previousAppliedKey, setPreviousAppliedKey] = useState(appliedKey);\n if (appliedKey !== previousAppliedKey) {\n setPreviousAppliedKey(appliedKey);\n if (appliedKey !== null) setCode(\"\");\n }\n\n const handleRemove = () => {\n setCode(\"\");\n onRemove();\n };\n\n const handleApply = () => {\n if (code.trim()) {\n onApply(code.trim().toUpperCase());\n }\n };\n\n const handleKeyDown = (e: React.KeyboardEvent) => {\n if (e.key === \"Enter\") {\n e.preventDefault();\n handleApply();\n }\n };\n\n // Format discount for display\n const formatDiscount = (result: PromotionCodeValidationResult): string => {\n if (result.discountType === \"percent_off\") {\n return t(\"billing.ui.wizard.review.percent_off\", { value: String(result.discountValue) });\n }\n // amount_off is in cents, convert to dollars\n const amount = (result.discountValue || 0) / 100;\n const currency = result.currency?.toUpperCase() || \"USD\";\n return t(\"billing.ui.wizard.review.amount_off\", { amount: `${currency} ${formatFixed(amount, locale, 2)}` });\n };\n\n // Format duration for display\n const formatDuration = (result: PromotionCodeValidationResult): string => {\n switch (result.duration) {\n case \"forever\":\n return t(\"billing.ui.promo_code.duration_forever\");\n case \"once\":\n return t(\"billing.ui.promo_code.duration_once\");\n case \"repeating\":\n return t(\"billing.ui.promo_code.duration_repeating\", { months: String(result.durationInMonths) });\n default:\n return \"\";\n }\n };\n\n const codeInput = (\n <div className=\"space-y-2\">\n <div className=\"flex gap-2\">\n <Input\n placeholder={t(\"billing.ui.promo_code.placeholder\")}\n value={code}\n onChange={(e) => setCode(e.target.value.toUpperCase())}\n onKeyDown={handleKeyDown}\n disabled={disabled || isValidating}\n className=\"flex-1\"\n />\n <Button variant=\"outline\" onClick={handleApply} disabled={disabled || isValidating || !code.trim()}>\n {isValidating ? <Loader2 className=\"h-4 w-4 animate-spin\" /> : t(\"billing.ui.promo_code.apply\")}\n </Button>\n </div>\n {error && <p className=\"text-destructive text-xs/relaxed\">{error}</p>}\n </div>\n );\n\n // Show the automatic code as applied, with the option to type a different one\n if (appliedCode?.valid && appliedCode.automatic) {\n return (\n <div className=\"space-y-3\">\n <div className=\"space-y-2\">\n <div className=\"bg-success/15 border-success/30 flex items-center gap-2 rounded-lg border p-3\">\n <Check className=\"text-success h-4 w-4\" />\n <span className=\"text-success font-medium\">{t(\"billing.ui.promo_code.automatic_label\")}</span>\n <span className=\"text-success text-sm\">{formatDiscount(appliedCode)}</span>\n </div>\n <p className=\"text-success text-sm\">{formatDuration(appliedCode)}</p>\n </div>\n <div className=\"space-y-2\">\n <p className=\"text-muted-foreground text-xs/relaxed\">{t(\"billing.ui.promo_code.use_different\")}</p>\n {codeInput}\n </div>\n </div>\n );\n }\n\n // Show applied code state\n if (appliedCode?.valid) {\n return (\n <div className=\"space-y-2\">\n <div className=\"bg-success/15 border-success/30 flex items-center justify-between rounded-lg border p-3\">\n <div className=\"flex items-center gap-2\">\n <Check className=\"text-success h-4 w-4\" />\n <span className=\"text-success font-medium\">{appliedCode.code}</span>\n <span className=\"text-success text-sm\">{formatDiscount(appliedCode)}</span>\n </div>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={handleRemove}\n disabled={disabled}\n className=\"text-success hover:bg-success/25 hover:text-success\"\n >\n <X className=\"h-4 w-4\" />\n </Button>\n </div>\n <p className=\"text-success text-sm\">{formatDuration(appliedCode)}</p>\n </div>\n );\n }\n\n return codeInput;\n}\n","import { PaymentMethodInterface } from \"../../../stripe-customer/data/payment-method.interface\";\n\nexport type ReviewCard = { brand: string; last4: string };\n\n/**\n * The card the review shows as the one on file: the customer's default, or the\n * first card listed when no default is set (or the default is no longer listed).\n * Null when no card with a brand and last 4 digits is known.\n */\nexport function reviewCard(\n methods: PaymentMethodInterface[] | null | undefined,\n defaultPaymentMethodId: string | null | undefined,\n): ReviewCard | null {\n if (!methods || methods.length === 0) return null;\n\n const method = (defaultPaymentMethodId && methods.find((m) => m.id === defaultPaymentMethodId)) || methods[0];\n if (!method.card?.last4) return null;\n\n const brand = method.card.brand\n ? method.card.brand.charAt(0).toUpperCase() + method.card.brand.slice(1)\n : method.type.charAt(0).toUpperCase() + method.type.slice(1);\n\n return { brand, last4: method.card.last4 };\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { PaymentMethodForm } from \"../../../stripe-customer/components/forms/PaymentMethodForm\";\nimport { SectionHeader } from \"../../../../../components/typography\";\n\ntype WizardStepPaymentMethodProps = {\n onBack: () => void;\n onSuccess: () => void;\n isProcessing: boolean;\n};\n\nexport function WizardStepPaymentMethod({ onBack, onSuccess, isProcessing }: WizardStepPaymentMethodProps) {\n const t = useTranslations();\n return (\n <div className=\"space-y-6\">\n <div className=\"text-center\">\n <SectionHeader>{t(\"billing.ui.payment_methods.add\")}</SectionHeader>\n <p className=\"text-sm text-muted-foreground\">{t(\"billing.ui.wizard.payment_description\")}</p>\n </div>\n\n <PaymentMethodForm onSuccess={onSuccess} onCancel={onBack} isLoading={isProcessing} alwaysSetDefault />\n </div>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { Activity } from \"lucide-react\";\nimport { useEffect, useState } from \"react\";\nimport { StripeSubscriptionInterface, StripeSubscriptionService } from \"../../../stripe-subscription\";\nimport { MeterInterface, MeterSummaryInterface } from \"../../data/stripe-usage.interface\";\nimport { StripeUsageService } from \"../../data/stripe-usage.service\";\nimport { UsageSummaryCards } from \"../widgets/UsageSummaryCards\";\n\nexport function UsageContainer() {\n const t = useTranslations();\n const [meters, setMeters] = useState<MeterInterface[]>([]);\n const [summaries, setSummaries] = useState<Record<string, MeterSummaryInterface | null>>({});\n const [loading, setLoading] = useState<boolean>(true);\n const [subscriptions, setSubscriptions] = useState<StripeSubscriptionInterface[]>([]);\n\n useEffect(() => {\n loadUsageData();\n }, []);\n\n const loadUsageData = async () => {\n setLoading(true);\n\n try {\n // First, check if there are any metered subscriptions\n const fetchedSubscriptions = await StripeSubscriptionService.listSubscriptions();\n setSubscriptions(fetchedSubscriptions);\n\n const hasMeteredSubscriptions = fetchedSubscriptions.some((sub) => sub.price?.recurring?.usageType === \"metered\");\n\n if (!hasMeteredSubscriptions) {\n setLoading(false);\n return;\n }\n\n // Load meters\n const fetchedMeters = await StripeUsageService.listMeters();\n setMeters(fetchedMeters);\n\n // Load summaries for each meter (current month)\n const summariesMap: Record<string, MeterSummaryInterface | null> = {};\n const now = new Date();\n const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);\n const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59, 999);\n\n for (const meter of fetchedMeters) {\n try {\n const meterSummaries = await StripeUsageService.getMeterSummaries({\n meterId: meter.id,\n startTime: startOfMonth,\n endTime: endOfMonth,\n });\n // Use the first (most recent) summary\n summariesMap[meter.id] = meterSummaries.length > 0 ? meterSummaries[0] : null;\n } catch (error) {\n console.error(`[UsageContainer] Failed to load summaries for meter ${meter.id}:`, error);\n summariesMap[meter.id] = null;\n }\n }\n\n setSummaries(summariesMap);\n } catch (error) {\n console.error(\"[UsageContainer] Failed to load usage data:\", error);\n } finally {\n setLoading(false);\n }\n };\n\n // Check if there are any metered subscriptions\n const hasMeteredSubscriptions = subscriptions.some((sub) => sub.price?.recurring?.usageType === \"metered\");\n\n // Don't render if no metered subscriptions\n if (!loading && !hasMeteredSubscriptions) {\n return null;\n }\n\n if (loading) {\n return (\n <div className=\"flex h-64 items-center justify-center\">\n <p className=\"text-muted-foreground\">{t(\"billing.ui.usage.loading\")}</p>\n </div>\n );\n }\n\n return (\n <div className=\"flex w-full flex-col gap-y-6\">\n {/* Header */}\n <div className=\"flex items-center gap-x-3\">\n <Activity className=\"h-8 w-8\" />\n <h1 className=\"text-primary text-3xl font-semibold\">{t(\"billing.ui.usage.title\")}</h1>\n </div>\n\n {/* Empty State */}\n {meters.length === 0 && (\n <div className=\"bg-muted/50 flex flex-col items-center justify-center gap-y-4 rounded-lg border-2 border-dashed border-gray-300 p-12\">\n <Activity className=\"text-muted-foreground h-16 w-16\" />\n <div className=\"text-center\">\n <h3 className=\"mb-2 text-sm font-medium\">{t(\"billing.ui.usage.empty_title\")}</h3>\n <p className=\"text-muted-foreground\">{t(\"billing.ui.usage.empty_description\")}</p>\n </div>\n </div>\n )}\n\n {/* Usage Summary Cards */}\n {meters.length > 0 && <UsageSummaryCards meters={meters} summaries={summaries} />}\n </div>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { Activity } from \"lucide-react\";\nimport { Card, CardContent, CardHeader } from \"../../../../../shadcnui\";\nimport { dateFormatLocale } from \"../../../../../utils\";\nimport { type BillingFormatOptions, formatFixed, useBillingFormatOptions } from \"../../../components/utils\";\nimport { MeterInterface, MeterSummaryInterface } from \"../../data/stripe-usage.interface\";\n\ntype UsageSummaryCardProps = {\n meter: MeterInterface;\n summary: MeterSummaryInterface | null;\n};\n\n/**\n * Get progress bar color based on usage percentage\n */\nfunction getProgressColor(percentage: number | null): string {\n if (percentage === null) return \"bg-blue-500\";\n if (percentage >= 90) return \"bg-red-500\";\n if (percentage >= 75) return \"bg-orange-500\";\n return \"bg-green-500\";\n}\n\n/**\n * Format a Unix timestamp to readable date\n */\nfunction formatDate(date: Date | undefined, { locale, t }: Required<BillingFormatOptions>): string {\n if (!date) return t(\"billing.ui.not_available\");\n\n try {\n return new Intl.DateTimeFormat(dateFormatLocale(locale), {\n month: \"short\",\n day: \"numeric\",\n }).format(new Date(date));\n } catch (_error) {\n return t(\"billing.ui.invalid_date\");\n }\n}\n\nexport function UsageSummaryCard({ meter, summary }: UsageSummaryCardProps) {\n const t = useTranslations();\n const fmt = useBillingFormatOptions();\n const currentUsage = summary?.aggregatedValue ?? 0;\n const limit = (meter as any).limit; // Meters may have optional limit field\n const percentage = limit && limit > 0 ? (currentUsage / limit) * 100 : null;\n const progressColor = getProgressColor(percentage);\n const progressWidth = percentage !== null ? Math.min(percentage, 100) : 0;\n\n const displayName = meter.displayName || meter.eventName;\n const hasLimit = limit !== null && limit !== undefined;\n\n return (\n <Card>\n <CardHeader className=\"flex flex-row items-center gap-x-3 pb-3\">\n <div className=\"flex h-10 w-10 items-center justify-center rounded-lg bg-blue-100 text-blue-600\">\n <Activity className=\"h-5 w-5\" />\n </div>\n <div className=\"flex flex-col\">\n <h3 className=\"text-sm font-medium\">{displayName}</h3>\n <p className=\"text-muted-foreground text-xs\">{meter.id}</p>\n </div>\n </CardHeader>\n\n <CardContent className=\"flex flex-col gap-y-4\">\n {/* Current Usage */}\n <div>\n <p className=\"text-3xl font-bold\">{currentUsage.toLocaleString(fmt.locale)}</p>\n {hasLimit && (\n <p className=\"text-muted-foreground text-sm\">\n {t(\"billing.ui.usage.of_limit_used\", { limit: limit.toLocaleString(fmt.locale) })}\n </p>\n )}\n </div>\n\n {/* Progress Bar */}\n {hasLimit ? (\n <div className=\"flex flex-col gap-y-2\">\n <div className=\"h-2 w-full overflow-hidden rounded-full bg-gray-200\">\n <div className={`h-full transition-all ${progressColor}`} style={{ width: `${progressWidth}%` }} />\n </div>\n <p className=\"text-muted-foreground text-sm\">\n {t(\"billing.ui.usage.percent_used\", {\n value: percentage !== null ? formatFixed(percentage, fmt.locale, 1) : \"\",\n })}\n </p>\n </div>\n ) : (\n <p className=\"text-muted-foreground text-sm\">{t(\"billing.ui.usage.no_limit\")}</p>\n )}\n\n {/* Period Information */}\n {summary && summary.start && summary.end && (\n <div className=\"border-t pt-3\">\n <p className=\"text-muted-foreground text-xs\">\n {t(\"billing.ui.usage.period\", {\n start: formatDate(summary.start, fmt),\n end: formatDate(summary.end, fmt),\n })}\n </p>\n </div>\n )}\n </CardContent>\n </Card>\n );\n}\n","\"use client\";\n\nimport { MeterInterface, MeterSummaryInterface } from \"../../data/stripe-usage.interface\";\nimport { UsageSummaryCard } from \"../details/UsageSummaryCard\";\n\ntype UsageSummaryCardsProps = {\n meters: MeterInterface[];\n summaries: Record<string, MeterSummaryInterface | null>;\n};\n\nexport function UsageSummaryCards({ meters, summaries }: UsageSummaryCardsProps) {\n return (\n <div className=\"grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3\">\n {meters.map((meter) => (\n <UsageSummaryCard key={meter.id} meter={meter} summary={summaries[meter.id] || null} />\n ))}\n </div>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from \"../../../../../shadcnui\";\nimport { SectionHeader } from \"../../../../../components/typography\";\nimport { dateFormatLocale } from \"../../../../../utils\";\nimport { type BillingFormatOptions, useBillingFormatOptions } from \"../../../components/utils\";\nimport { StripeUsageInterface } from \"../../data/stripe-usage.interface\";\n\ntype UsageHistoryTableProps = {\n usageRecords: StripeUsageInterface[];\n};\n\n/**\n * Format a date with time for usage history\n */\nfunction formatDateTime(date: Date | string | undefined, { locale, t }: Required<BillingFormatOptions>): string {\n if (!date) return t(\"billing.ui.not_available\");\n\n const dateObj = typeof date === \"string\" ? new Date(date) : date;\n\n try {\n return new Intl.DateTimeFormat(dateFormatLocale(locale), {\n month: \"short\",\n day: \"numeric\",\n year: \"numeric\",\n hour: \"numeric\",\n minute: \"2-digit\",\n }).format(dateObj);\n } catch (_error) {\n return t(\"billing.ui.invalid_date\");\n }\n}\n\nexport function UsageHistoryTable({ usageRecords }: UsageHistoryTableProps) {\n const t = useTranslations();\n const fmt = useBillingFormatOptions();\n if (usageRecords.length === 0) {\n return (\n <div className=\"rounded-lg border p-8 text-center\">\n <p className=\"text-muted-foreground\">{t(\"billing.ui.usage.history_empty\")}</p>\n </div>\n );\n }\n\n return (\n <div className=\"flex w-full flex-col gap-y-4\">\n <SectionHeader level={2}>{t(\"billing.ui.usage.history_title\")}</SectionHeader>\n <div className=\"overflow-clip rounded-lg border\">\n <Table>\n <TableHeader className=\"bg-muted\">\n <TableRow>\n <TableHead>{t(\"billing.ui.columns.date_time\")}</TableHead>\n <TableHead>{t(\"billing.ui.columns.meter_event\")}</TableHead>\n <TableHead className=\"text-end\">{t(\"billing.ui.columns.quantity\")}</TableHead>\n <TableHead>{t(\"billing.ui.columns.event_id\")}</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {usageRecords.map((record) => {\n const dateTime = formatDateTime(record.timestamp, fmt);\n const quantity = record.quantity.toLocaleString(fmt.locale);\n\n return (\n <TableRow key={record.id}>\n <TableCell className=\"font-medium\">{dateTime}</TableCell>\n <TableCell className=\"text-muted-foreground\">{record.meterEventName}</TableCell>\n <TableCell className=\"text-end font-medium\">{quantity}</TableCell>\n <TableCell className=\"text-muted-foreground text-sm tabular-nums\">{record.stripeEventId}</TableCell>\n </TableRow>\n );\n })}\n </TableBody>\n </Table>\n </div>\n </div>\n );\n}\n","\"use client\";\n\nimport { ReactNode } from \"react\";\nimport { Dialog, DialogContent, DialogHeader, DialogTitle } from \"../../../../shadcnui\";\n\ntype BillingDetailModalProps = {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n title: string;\n children: ReactNode;\n className?: string;\n};\n\nexport function BillingDetailModal({ open, onOpenChange, title, children, className }: BillingDetailModalProps) {\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent className={className ?? \"max-w-4xl max-h-[90vh] overflow-y-auto\"}>\n <DialogHeader>\n <DialogTitle>{title}</DialogTitle>\n </DialogHeader>\n {children}\n </DialogContent>\n </Dialog>\n );\n}\n","/**\n * The URL to write back after consuming a one-shot query parameter (a deep-link\n * `?action=`). Drops only `param`; every other parameter and the hash stay, so a\n * host page that keys its state on the URL (the settings hub's `?section=`) keeps\n * showing the same place.\n */\nexport function urlWithoutParam(params: { pathname: string; search: string; hash?: string; param: string }): string {\n const query = new URLSearchParams(params.search);\n query.delete(params.param);\n const remaining = query.toString();\n return `${params.pathname}${remaining ? `?${remaining}` : \"\"}${params.hash ?? \"\"}`;\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { AlertCircle } from \"lucide-react\";\nimport { Button } from \"../../../../shadcnui\";\nimport { StripeSubscriptionInterface, SubscriptionStatus } from \"../../stripe-subscription\";\n\ntype BillingAlertBannerProps = {\n subscription: StripeSubscriptionInterface;\n onUpdatePayment?: () => void;\n onAddPayment?: () => void;\n};\n\nexport function BillingAlertBanner({ subscription, onUpdatePayment, onAddPayment }: BillingAlertBannerProps) {\n const t = useTranslations();\n // Payment failed alert (past_due)\n if (subscription.status === SubscriptionStatus.PAST_DUE) {\n return (\n <div className=\"bg-red-50 border border-red-200 rounded-lg p-4 flex items-start gap-x-3\">\n <AlertCircle className=\"text-destructive mt-0.5 h-5 w-5\" />\n <div className=\"flex-1\">\n <h3 className=\"text-destructive text-sm font-medium\">{t(\"billing.ui.alert.payment_failed_title\")}</h3>\n <p className=\"text-destructive mt-1 text-sm\">{t(\"billing.ui.alert.payment_failed_description\")}</p>\n </div>\n {onUpdatePayment && (\n <Button variant=\"outline\" size=\"sm\" onClick={onUpdatePayment} className=\"text-destructive border-red-300\">\n {t(\"billing.ui.alert.update_payment_method\")}\n </Button>\n )}\n </div>\n );\n }\n\n // Trial ending soon alert (trialing with ≤7 days remaining)\n if (subscription.status === SubscriptionStatus.TRIALING && subscription.trialEnd) {\n const trialEnd = new Date(subscription.trialEnd);\n const now = new Date();\n const daysRemaining = Math.ceil((trialEnd.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));\n\n if (daysRemaining <= 7) {\n return (\n <div className=\"bg-yellow-50 border border-yellow-200 rounded-lg p-4 flex items-start gap-x-3\">\n <AlertCircle className=\"text-warning mt-0.5 h-5 w-5\" />\n <div className=\"flex-1\">\n <h3 className=\"text-warning text-sm font-medium\">{t(\"billing.ui.alert.trial_ending_title\")}</h3>\n <p className=\"text-warning mt-1 text-sm\">\n {t(\"billing.ui.alert.trial_ending_description\", { count: daysRemaining })}\n </p>\n </div>\n {onAddPayment && (\n <Button variant=\"outline\" size=\"sm\" onClick={onAddPayment} className=\"text-warning border-yellow-300\">\n {t(\"billing.ui.payment_methods.add\")}\n </Button>\n )}\n </div>\n );\n }\n }\n\n // No critical state\n return null;\n}\n","\"use client\";\n\nimport { Elements } from \"@stripe/react-stripe-js\";\nimport { loadStripe, Stripe } from \"@stripe/stripe-js\";\nimport { ReactNode, useMemo } from \"react\";\nimport { getStripePublishableKey } from \"../../../../client/config\";\n\n// Cache the stripe promise to avoid recreating on each render\nlet stripePromiseCache: { key: string; promise: Promise<Stripe | null> } | null = null;\n\nfunction getStripePromise(publishableKey: string | undefined): Promise<Stripe | null> {\n if (!publishableKey) {\n return Promise.resolve(null);\n }\n\n // Return cached promise if key matches\n if (stripePromiseCache?.key === publishableKey) {\n return stripePromiseCache.promise;\n }\n\n // Create and cache new promise\n const promise = loadStripe(publishableKey);\n stripePromiseCache = { key: publishableKey, promise };\n return promise;\n}\n\nexport function StripeProvider({ children }: { children: ReactNode }) {\n // Evaluate key at render time, not module load time\n const publishableKey = getStripePublishableKey();\n const stripePromise = useMemo(() => getStripePromise(publishableKey), [publishableKey]);\n const options = useMemo(() => ({}), []);\n\n // If no Stripe key is configured, just render children without Stripe context\n if (!publishableKey) {\n return <>{children}</>;\n }\n\n return (\n <Elements stripe={stripePromise} options={options}>\n {children}\n </Elements>\n );\n}\n\n// Helper function to check if Stripe is configured\nexport function isStripeConfigured(): boolean {\n return !!getStripePublishableKey();\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { createContext, ReactNode, useCallback, useContext, useMemo, useState } from \"react\";\nimport { SharedProvider } from \"../../../contexts/SharedContext\";\nimport { JsonApiHydratedDataInterface, Modules, rehydrate } from \"../../../core\";\nimport { usePageUrlGenerator } from \"../../../hooks\";\nimport { BreadcrumbItemData } from \"../../../interfaces\";\nimport { getRoleId } from \"../../../roles\";\nimport { useCurrentUserContext } from \"../../user/contexts\";\nimport {\n type BillingFormatOptions,\n formatCurrency,\n formatInterval,\n useBillingFormatOptions,\n} from \"../components/utils\";\nimport { PriceArchiver } from \"../stripe-price/components/forms/PriceArchiver\";\nimport PriceEditor from \"../stripe-price/components/forms/PriceEditor\";\nimport { StripePriceInput, StripePriceInterface } from \"../stripe-price/data/stripe-price.interface\";\nimport { StripePriceService } from \"../stripe-price/data/stripe-price.service\";\n\nexport interface PriceContextType {\n price: StripePriceInterface | undefined;\n setPrice: (value: StripePriceInterface | undefined) => void;\n reloadPrice: () => Promise<void>;\n createPrice: (input: StripePriceInput) => Promise<StripePriceInterface>;\n updatePrice: (input: StripePriceInput) => Promise<StripePriceInterface>;\n archivePrice: (id?: string) => Promise<void>;\n restorePrice: (id?: string) => Promise<void>;\n /** Bumped by every mutation. Mounted lists re-fetch on it. */\n priceVersion: number;\n refreshPrices: () => void;\n /** Product a newly created price attaches to. Set in list mode (prices tab). */\n productId: string | undefined;\n}\n\nconst PriceContext = createContext<PriceContextType | undefined>(undefined);\n\nexport type PriceProviderProps = {\n children: ReactNode;\n dehydratedPrice?: JsonApiHydratedDataInterface;\n /**\n * Publish breadcrumbs + title into SharedProvider. Pass `false` when this\n * provider is mounted INSIDE another entity's page (the prices tab mounts\n * PriceProvider that way): SharedProvider replaces its value for the subtree\n * rather than merging (SharedContext.tsx:35-37), so a nested provider that\n * publishes chrome would blank the host page's title.\n */\n publishChrome?: boolean;\n /** Required in list mode (the prices tab) so new prices attach to the right product. */\n productId?: string;\n};\n\nconst defaultContextValue: PriceContextType = {\n price: undefined,\n setPrice: () => {},\n reloadPrice: async () => {},\n createPrice: async () => {\n throw new Error(\"createPrice requires a PriceProvider\");\n },\n updatePrice: async () => {\n throw new Error(\"updatePrice requires a PriceProvider\");\n },\n archivePrice: async () => {},\n restorePrice: async () => {},\n priceVersion: 0,\n refreshPrices: () => {},\n productId: undefined,\n};\n\n/**\n * Human label for a price: its nickname, else amount + interval. Pass the\n * caller's `useBillingFormatOptions()` so the amount and interval follow the\n * active locale; without it the label is formatted in English.\n */\nexport function priceLabel(price: StripePriceInterface, fmt: BillingFormatOptions = {}): string {\n return (\n price.nickname ?? `${formatCurrency(price.unitAmount, price.currency, fmt)} ${formatInterval(price, fmt)}`.trim()\n );\n}\n\nexport const PriceProvider = ({\n children,\n dehydratedPrice,\n publishChrome = true,\n productId: productIdProp,\n}: PriceProviderProps) => {\n const t = useTranslations();\n const fmt = useBillingFormatOptions();\n const generateUrl = usePageUrlGenerator();\n const { hasRole } = useCurrentUserContext();\n\n const [price, setPrice] = useState<StripePriceInterface | undefined>(\n dehydratedPrice ? rehydrate<StripePriceInterface>(Modules.StripePrice, dehydratedPrice) : undefined,\n );\n const [priceVersion, setPriceVersion] = useState<number>(0);\n\n const refreshPrices = useCallback(() => setPriceVersion((value) => value + 1), []);\n\n // NOT `price.productId`: that getter throws (\"productId is not defined\") when\n // the attribute is absent, and it always is — the API's mapStripePrice sets\n // `stripeProduct: undefined`, and the serialiser derives the productId\n // attribute from exactly that field, so a price never carries one over the\n // wire. Reading it here crashed the whole price page on mount. Fall back to\n // the hydrated relationship, which is a safe optional getter.\n const productId = productIdProp ?? price?.product?.id;\n\n const reloadPrice = useCallback(async () => {\n if (!price?.id) return;\n setPrice(await StripePriceService.getPrice({ id: price.id }));\n }, [price?.id]);\n\n const createPrice = useCallback(\n async (input: StripePriceInput) => {\n const saved = await StripePriceService.createPrice(input);\n refreshPrices();\n return saved;\n },\n [refreshPrices],\n );\n\n const updatePrice = useCallback(\n async (input: StripePriceInput) => {\n const saved = await StripePriceService.updatePrice(input);\n setPrice(saved);\n refreshPrices();\n return saved;\n },\n [refreshPrices],\n );\n\n // The archive/reactivate routes answer 204 NO_CONTENT with an empty body\n // (stripe-price.controller.ts:129-155) even though the service signature\n // claims it returns void — so the response is discarded and the entity is\n // re-read instead.\n const archivePrice = useCallback(\n async (id?: string) => {\n const target = id ?? price?.id;\n if (!target) return;\n await StripePriceService.archivePrice({ id: target });\n await reloadPrice();\n refreshPrices();\n },\n [price?.id, reloadPrice, refreshPrices],\n );\n\n const restorePrice = useCallback(\n async (id?: string) => {\n const target = id ?? price?.id;\n if (!target) return;\n await StripePriceService.reactivatePrice({ id: target });\n await reloadPrice();\n refreshPrices();\n },\n [price?.id, reloadPrice, refreshPrices],\n );\n\n const value = useMemo<PriceContextType>(\n () => ({\n price,\n setPrice,\n reloadPrice,\n createPrice,\n updatePrice,\n archivePrice,\n restorePrice,\n priceVersion,\n refreshPrices,\n productId,\n }),\n [price, reloadPrice, createPrice, updatePrice, archivePrice, restorePrice, priceVersion, refreshPrices, productId],\n );\n\n if (!publishChrome) return <PriceContext.Provider value={value}>{children}</PriceContext.Provider>;\n\n const breadcrumb = (): BreadcrumbItemData[] => {\n const items: BreadcrumbItemData[] = [\n { name: t(\"billing.admin.products.title\"), href: generateUrl({ page: Modules.StripeProduct }) },\n ];\n if (price?.product)\n items.push({\n name: price.product.name,\n href: generateUrl({ page: Modules.StripeProduct, id: price.product.id }),\n });\n if (price) items.push({ name: priceLabel(price, fmt) });\n return items;\n };\n\n const title = () => {\n const response: any = { type: t(\"billing.admin.prices.title\") };\n if (!price) return response;\n\n response.element = priceLabel(price, fmt);\n\n // narr8 note: gate on hasRole ONLY. hasPermissionToModule always returns\n // false in this app, so gating an editor on it hides the editor outright.\n if (hasRole(getRoleId().Administrator)) {\n response.functions = [\n <PriceArchiver key=\"priceArchiver\" price={price} />,\n <PriceEditor key=\"priceEditor\" price={price} propagateChanges={setPrice} />,\n ];\n }\n\n return response;\n };\n\n return (\n <SharedProvider value={{ breadcrumbs: breadcrumb(), title: title() }}>\n <PriceContext.Provider value={value}>{children}</PriceContext.Provider>\n </SharedProvider>\n );\n};\n\nexport const usePriceContext = (): PriceContextType => useContext(PriceContext) ?? defaultContextValue;\n","\"use client\";\n\nimport { ArchiveIcon, RotateCcwIcon } from \"lucide-react\";\nimport { useTranslations } from \"next-intl\";\nimport { useState } from \"react\";\nimport { errorToast } from \"../../../../../components/errors\";\nimport { Button, ConfirmDialog } from \"../../../../../shadcnui\";\nimport { useBillingFormatOptions } from \"../../../components/utils\";\nimport { priceLabel, usePriceContext } from \"../../../contexts/PriceContext\";\nimport { StripePriceInterface } from \"../../data/stripe-price.interface\";\n\ntype PriceArchiverProps = {\n price: StripePriceInterface;\n};\n\n/**\n * Archive / restore control for a price. Built on ConfirmDialog, which is\n * controlled and trigger-less by design — the caller owns the trigger button,\n * so none of Base UI's trigger-composition pitfalls apply (no asChild, no\n * nested <button>).\n */\nexport function PriceArchiver({ price }: PriceArchiverProps) {\n const t = useTranslations();\n const fmt = useBillingFormatOptions();\n const { archivePrice, restorePrice } = usePriceContext();\n const [open, setOpen] = useState<boolean>(false);\n\n const isArchived = !price.active;\n const scope = isArchived ? \"restore\" : \"archive\";\n\n return (\n <>\n <Button variant=\"outline\" size=\"sm\" onClick={() => setOpen(true)}>\n {isArchived ? <RotateCcwIcon /> : <ArchiveIcon />}\n {t(`billing.admin.prices.${scope}.confirm`)}\n </Button>\n <ConfirmDialog\n open={open}\n onOpenChange={setOpen}\n title={t(`billing.admin.prices.${scope}.title`)}\n description={t(`billing.admin.prices.${scope}.description`, { label: priceLabel(price, fmt) })}\n confirmLabel={t(`billing.admin.prices.${scope}.confirm`)}\n cancelLabel={t(\"ui.buttons.cancel\")}\n destructive={!isArchived}\n onConfirm={async () => {\n try {\n if (isArchived) await restorePrice(price.id);\n else await archivePrice(price.id);\n } catch (error) {\n errorToast({ title: t(`billing.admin.prices.${scope}.title`), error });\n throw error; // keeps the dialog open — ConfirmDialog swallows rejections\n }\n }}\n />\n </>\n );\n}\n","\"use client\";\n\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { PlusIcon, XIcon } from \"lucide-react\";\nimport { useTranslations } from \"next-intl\";\nimport { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { v4 } from \"uuid\";\nimport { z } from \"zod\";\nimport { EditorSheet, FieldLabel, FormCheckbox, FormInput, FormSelect, FormTextarea } from \"../../../../../components\";\nimport { Modules } from \"../../../../../core\";\nimport { useI18nRouter } from \"../../../../../i18n\";\nimport { Alert, AlertDescription, AlertTitle, Button, Checkbox, Input } from \"../../../../../shadcnui\";\nimport { showToast } from \"../../../../../utils/toast\";\nimport { FeatureInterface, FeatureService } from \"../../../../feature\";\nimport { useBillingFormatOptions } from \"../../../components/utils\";\nimport { priceLabel, usePriceContext } from \"../../../contexts/PriceContext\";\nimport { StripePriceInput, StripePriceInterface } from \"../../data/stripe-price.interface\";\nimport { StripePriceSeed } from \"../../data/stripe-price.transfer\";\n\nexport type PriceEditorProps = {\n productId?: string;\n price?: StripePriceInterface;\n /**\n * Pre-fills a NEW price — the Clone (a row in the prices list) and Import (a\n * JSON document) paths both hand over the same seed.\n *\n * The seed deliberately carries NO identity: no Stripe price id, no internal\n * id and no product id. A clone/import is a brand-new price, minted fresh by\n * Stripe and attached to whatever product this editor is mounted for\n * (`productId` / the context), so copying any identity across would either\n * collide or silently re-point the new price at the source environment's\n * objects. Seeding therefore never makes this an edit — see `isEdit` below.\n */\n seed?: StripePriceSeed;\n propagateChanges?: (price: StripePriceInterface) => void;\n onSuccess?: () => void | Promise<void>;\n trigger?: ReactNode;\n forceShow?: boolean;\n onClose?: () => void;\n dialogOpen?: boolean;\n onDialogOpenChange?: (open: boolean) => void;\n};\n\nfunction PriceEditorInternal({\n productId,\n price,\n seed,\n propagateChanges,\n onSuccess,\n trigger,\n forceShow,\n onClose,\n dialogOpen,\n onDialogOpenChange,\n}: PriceEditorProps) {\n const t = useTranslations();\n const fmt = useBillingFormatOptions();\n const router = useI18nRouter();\n const { createPrice, updatePrice, productId: contextProductId } = usePriceContext();\n const [allFeatures, setAllFeatures] = useState<FeatureInterface[]>([]);\n // Stays `!!price`, never `!!price || !!seed`: a seeded editor is a CREATE, so\n // the create branch of onSubmit (fresh v4(), targetProductId, no id reuse)\n // must run for it exactly as it does for an empty form.\n const isEdit = !!price;\n // NOT `price.productId` — that getter throws when the attribute is absent, and\n // it always is (see the note in PriceContext). Only create mode needs this,\n // and there it arrives as a prop or from the context.\n const targetProductId = productId ?? price?.product?.id ?? contextProductId;\n\n useEffect(() => {\n const fetchFeatures = async () => {\n setAllFeatures(await FeatureService.findMany({}));\n };\n void fetchFeatures();\n }, []);\n\n const formSchema = useMemo(\n () =>\n z.object({\n unitAmount: z.preprocess(\n (value) => (typeof value === \"string\" ? parseFloat(value) : value),\n z.number().min(0, { message: t(\"billing.admin.prices.errors.amount\") }),\n ),\n currency: z.string().min(1, { message: t(\"billing.admin.prices.errors.currency\") }),\n interval: z.enum([\"one_time\", \"day\", \"week\", \"month\", \"year\"]),\n intervalCount: z.preprocess(\n (value) =>\n value === \"\" || value === undefined ? undefined : typeof value === \"string\" ? parseInt(value, 10) : value,\n z.number().min(1).optional(),\n ),\n usageType: z.enum([\"licensed\", \"metered\"]).optional(),\n nickname: z.string().optional(),\n isTrial: z.boolean(),\n description: z.string().optional(),\n features: z.array(z.string()),\n // Normalised, not a bare `z.string()`: the field renders as\n // `<FormInput type=\"number\">`, which hands back a number, so a plain\n // string schema rejects every value with \"expected string, received\n // number\".\n //\n // Normalised by hand rather than with `z.coerce.string()`, because\n // zod 4's coercion is `String(input)` — a cleared number input yields\n // NaN, which would become the TRUTHY string \"NaN\" and reach\n // `parseInt` as a NaN token.\n //\n // Deliberately NOT converted to the `z.preprocess(... z.number())`\n // idiom used by unitAmount/intervalCount above. The whole token\n // pipeline is string-based — seeded with `.toString()` and read back\n // with `values.token ? parseInt(...)` on both submit branches — and a\n // number pipeline would make `0` FALSY, silently dropping the token\n // from the payload. A price with no token is \"tokens not configured\"\n // (AI stays enabled), so that would make a zero-token / no-AI plan\n // impossible to create from this form.\n token: z.preprocess(\n (value) =>\n value === undefined || value === null || value === \"\" || (typeof value === \"number\" && Number.isNaN(value))\n ? \"\"\n : String(value),\n z.string(),\n ),\n featureIds: z.array(z.string()),\n }),\n [t],\n );\n\n type PriceFormValues = z.infer<typeof formSchema>;\n\n // Seeds carry platform features as NAMES, never ids: Feature ids differ per\n // environment, names are the stable handle. A name this environment does not\n // know is DROPPED rather than failing the whole seed — the effect below\n // reports the dropped ones once.\n const resolvePlatformFeatures = useCallback(\n (names: string[] | undefined): { ids: string[]; unknown: string[] } => {\n const ids: string[] = [];\n const unknown: string[] = [];\n for (const name of names ?? []) {\n const match = allFeatures.find((feature) => feature.name === name);\n if (match) ids.push(match.id);\n else unknown.push(name);\n }\n return { ids, unknown };\n },\n [allFeatures],\n );\n\n // Fed to BOTH useForm and EditorSheet.onReset. The previous implementation\n // reseeded with its own `useEffect(… form.reset)` on open; EditorSheet already\n // owns that (EditorSheet.tsx:163-190) and running both fights over the form.\n //\n // `active` is absent on purpose: archiving is the single deactivation path\n // (PriceArchiver), and a second toggle here would let the two disagree.\n const getDefaultValues = useCallback((): PriceFormValues => {\n const coreFeatureIds = allFeatures.filter((feature) => feature.isCore).map((feature) => feature.id);\n\n // Seed branch: create mode only (`price` always wins — an edit form must\n // show the price it edits, never a seed). Mirrors the `price` branch below\n // field for field, including the /100: the seed keeps Stripe's MINOR units\n // while the form field is in MAJOR units.\n if (!price && seed) {\n return {\n unitAmount: seed.unitAmount / 100,\n currency: seed.currency,\n interval: seed.interval,\n intervalCount: seed.intervalCount ?? 1,\n usageType: seed.usageType ?? \"licensed\",\n nickname: seed.nickname ?? \"\",\n isTrial: seed.isTrial ?? false,\n description: seed.description ?? \"\",\n features: seed.features ?? [],\n // `.toString()`, for the same reason the schema keeps the token\n // pipeline string-based: a seeded `0` must survive as \"0\" and not be\n // dropped as falsy.\n token: seed.token?.toString() ?? \"\",\n featureIds: [...new Set([...resolvePlatformFeatures(seed.platformFeatures).ids, ...coreFeatureIds])],\n };\n }\n\n return {\n unitAmount: price?.unitAmount ? price.unitAmount / 100 : 0,\n currency: price?.currency ?? \"usd\",\n interval: price?.priceType === \"one_time\" ? \"one_time\" : (price?.recurring?.interval ?? \"month\"),\n intervalCount: price?.recurring?.intervalCount ?? 1,\n usageType: price?.recurring?.usageType ?? \"licensed\",\n nickname: price?.nickname ?? \"\",\n isTrial: price?.isTrial ?? false,\n description: price?.description ?? \"\",\n features: price?.features ?? [],\n token: price?.token?.toString() ?? \"\",\n featureIds: [...new Set([...(price?.priceFeatures?.map((f) => f.id) ?? []), ...coreFeatureIds])],\n };\n }, [price, seed, allFeatures, resolvePlatformFeatures]);\n\n const form = useForm<PriceFormValues>({\n resolver: zodResolver(formSchema) as any,\n defaultValues: getDefaultValues(),\n });\n\n // ── The seeding race ──────────────────────────────────────────────────────\n // `getDefaultValues` reads `allFeatures`, which the fetch effect above\n // resolves asynchronously — so `useForm` has ALREADY seeded from an empty\n // list by the time the features land, and every id-bearing default\n // (`featureIds`: the core features, plus whatever the seed named) is missing.\n //\n // Nothing else fixes it: EditorSheet re-seeds on open only when `isEdit`\n // (EditorSheet.tsx:209 — create defaults usually contain a fresh uuid, so an\n // unconditional reset there would fire on every open and clobber parents that\n // pre-fill via form.setValue). That is exactly why the core-feature\n // checkboxes come up empty on a first create today.\n //\n // So catch up here, once, and only when it is safe:\n // - `!isEdit` — the edit path is EditorSheet's, do not fight it.\n // - `length > 0` — this is the empty → loaded transition, not the initial\n // render (and not a genuinely empty feature list).\n // - the ref — one shot per mount, so a later `allFeatures` identity\n // change cannot re-reset a form the user is using.\n // - `!isDirty` — never clobber anything already typed; a user who edited\n // before the fetch returned keeps their input, and the\n // one-shot ref is burned either way.\n const hasCaughtUpWithFeatures = useRef(false);\n useEffect(() => {\n if (isEdit || hasCaughtUpWithFeatures.current || allFeatures.length === 0) return;\n hasCaughtUpWithFeatures.current = true;\n if (form.formState.isDirty) return;\n form.reset(getDefaultValues());\n }, [allFeatures, isEdit, form, getDefaultValues]);\n\n // Unknown platform feature names are reported ONCE per seed, not per render:\n // `seed` typically arrives as an object literal from the list row or the\n // import dialog, so its identity changes on every parent render — the guard\n // keys on the names themselves instead. Waits for `allFeatures`, since before\n // the fetch resolves every name looks unknown.\n const warnedUnknownFor = useRef<string | undefined>(undefined);\n useEffect(() => {\n if (isEdit || !seed || allFeatures.length === 0) return;\n const fingerprint = JSON.stringify(seed.platformFeatures ?? []);\n if (warnedUnknownFor.current === fingerprint) return;\n warnedUnknownFor.current = fingerprint;\n\n const { unknown } = resolvePlatformFeatures(seed.platformFeatures);\n if (unknown.length > 0)\n showToast(t(\"billing.admin.prices.import.warnings.unknownFeatures\", { features: unknown.join(\", \") }));\n }, [seed, isEdit, allFeatures, resolvePlatformFeatures, t]);\n\n const watchInterval = form.watch(\"interval\");\n const isRecurring = watchInterval !== \"one_time\";\n\n const currencyOptions = [\n { id: \"usd\", text: \"USD ($)\" },\n { id: \"eur\", text: \"EUR (€)\" },\n { id: \"gbp\", text: \"GBP (£)\" },\n ];\n\n const intervalOptions = [\n { id: \"one_time\", text: t(\"billing.admin.prices.interval.one_time\") },\n { id: \"day\", text: t(\"billing.admin.prices.interval.day\") },\n { id: \"week\", text: t(\"billing.admin.prices.interval.week\") },\n { id: \"month\", text: t(\"billing.admin.prices.interval.month\") },\n { id: \"year\", text: t(\"billing.admin.prices.interval.year\") },\n ];\n\n const usageTypeOptions = [\n { id: \"licensed\", text: t(\"billing.admin.prices.usage.licensed\") },\n { id: \"metered\", text: t(\"billing.admin.prices.usage.metered\") },\n ];\n\n return (\n <EditorSheet\n form={form}\n entityType={t(\"billing.admin.prices.entity\")}\n entityName={price ? priceLabel(price, fmt) : undefined}\n isEdit={isEdit}\n module={Modules.StripePrice}\n size=\"lg\"\n propagateChanges={propagateChanges}\n onSuccess={onSuccess}\n onNavigate={(url) => router.push(url)}\n onSubmit={async (values) => {\n const trimmedFeatures = values.features.filter((feature) => feature.trim());\n\n if (isEdit) {\n const patch: StripePriceInput = {\n id: price.id,\n nickname: values.nickname || undefined,\n description: values.description || undefined,\n features: trimmedFeatures.length > 0 ? trimmedFeatures : undefined,\n token: values.token ? parseInt(values.token, 10) : undefined,\n // Stripe one-time prices carry neither a trial flag nor platform\n // features, so those two only travel for recurring prices.\n ...(price.priceType === \"recurring\" ? { isTrial: values.isTrial, featureIds: values.featureIds } : {}),\n };\n return await updatePrice(patch);\n }\n\n if (!targetProductId) throw new Error(\"PriceEditor requires a productId to create a price\");\n\n const payload: StripePriceInput = {\n id: v4(),\n productId: targetProductId,\n currency: values.currency,\n unitAmount: Math.round(values.unitAmount * 100),\n };\n if (isRecurring) {\n payload.recurring = {\n interval: values.interval as \"day\" | \"week\" | \"month\" | \"year\",\n intervalCount: values.intervalCount ?? 1,\n usageType: values.usageType ?? \"licensed\",\n };\n payload.isTrial = values.isTrial;\n if (values.featureIds.length > 0) payload.featureIds = values.featureIds;\n }\n if (values.nickname) payload.nickname = values.nickname;\n if (values.description) payload.description = values.description;\n if (trimmedFeatures.length > 0) payload.features = trimmedFeatures;\n if (values.token) payload.token = parseInt(values.token, 10);\n\n return await createPrice(payload);\n }}\n onReset={getDefaultValues}\n trigger={trigger}\n forceShow={forceShow}\n onClose={onClose}\n dialogOpen={dialogOpen}\n onDialogOpenChange={onDialogOpenChange}\n >\n <div className=\"flex w-full flex-col gap-y-4\">\n {isEdit && (\n <Alert>\n <AlertTitle>{t(\"billing.admin.prices.immutability.title\")}</AlertTitle>\n <AlertDescription>{t(\"billing.admin.prices.immutability.description\")}</AlertDescription>\n </Alert>\n )}\n\n <div className=\"grid grid-cols-2 gap-x-4\">\n {/* type=\"decimal\", NOT type=\"currency\": the currency variant hardcodes\n a Euro symbol (FormInput.tsx:105-106), which contradicts the\n currency selector sitting next to it. */}\n <FormInput\n form={form}\n id=\"unitAmount\"\n type=\"decimal\"\n name={t(\"billing.admin.prices.fields.amount\")}\n placeholder={t(\"billing.admin.prices.placeholders.amount\")}\n disabled={isEdit}\n isRequired\n />\n <FormSelect\n form={form}\n id=\"currency\"\n name={t(\"billing.admin.prices.fields.currency\")}\n values={currencyOptions}\n disabled={isEdit}\n />\n </div>\n\n <FormSelect\n form={form}\n id=\"interval\"\n name={t(\"billing.admin.prices.fields.interval\")}\n values={intervalOptions}\n disabled={isEdit}\n />\n\n {isRecurring && (\n <div className=\"grid grid-cols-2 gap-x-4\">\n <FormInput\n form={form}\n id=\"intervalCount\"\n type=\"number\"\n name={t(\"billing.admin.prices.fields.intervalCount\")}\n placeholder={t(\"billing.admin.prices.placeholders.intervalCount\")}\n disabled={isEdit}\n />\n <FormSelect\n form={form}\n id=\"usageType\"\n name={t(\"billing.admin.prices.fields.usageType\")}\n values={usageTypeOptions}\n disabled={isEdit}\n />\n </div>\n )}\n\n <FormInput\n form={form}\n id=\"nickname\"\n name={t(\"billing.admin.prices.fields.nickname\")}\n placeholder={t(\"billing.admin.prices.placeholders.nickname\")}\n />\n\n <FormTextarea\n form={form}\n id=\"description\"\n name={t(\"billing.admin.prices.fields.description\")}\n placeholder={t(\"billing.admin.prices.placeholders.description\")}\n className=\"min-h-24\"\n />\n\n <FormInput\n form={form}\n id=\"token\"\n type=\"number\"\n name={t(\"billing.admin.prices.fields.token\")}\n placeholder={t(\"billing.admin.prices.placeholders.token\")}\n />\n\n <div className=\"flex flex-col gap-y-2\">\n <FieldLabel>{t(\"billing.admin.prices.fields.features\")}</FieldLabel>\n {form.watch(\"features\").map((_, index) => (\n <div key={index} className=\"flex gap-2\">\n <Input\n {...form.register(`features.${index}`)}\n placeholder={t(\"billing.admin.prices.placeholders.feature\", { index: index + 1 })}\n className=\"flex-1\"\n />\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"icon\"\n aria-label={t(\"billing.admin.prices.actions.removeFeature\")}\n onClick={() =>\n form.setValue(\n \"features\",\n form.getValues(\"features\").filter((_, i) => i !== index),\n )\n }\n >\n <XIcon />\n </Button>\n </div>\n ))}\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"self-start\"\n onClick={() => form.setValue(\"features\", [...form.getValues(\"features\"), \"\"])}\n >\n <PlusIcon />\n {t(\"billing.admin.prices.actions.addFeature\")}\n </Button>\n </div>\n\n {isRecurring && allFeatures.length > 0 && (\n <div className=\"flex flex-col gap-y-2\">\n <FieldLabel>{t(\"billing.admin.prices.fields.platformFeatures\")}</FieldLabel>\n <div className=\"flex max-h-48 flex-col gap-y-2 overflow-y-auto rounded-md border p-4\">\n {allFeatures.map((feature) => {\n const isChecked = form.watch(\"featureIds\").includes(feature.id);\n return (\n <div key={feature.id} className=\"flex items-center gap-x-2\">\n <Checkbox\n id={`feature-${feature.id}`}\n checked={isChecked}\n disabled={feature.isCore}\n onCheckedChange={(checked) => {\n const current = form.getValues(\"featureIds\");\n if (checked) form.setValue(\"featureIds\", [...new Set([...current, feature.id])]);\n else if (!feature.isCore)\n form.setValue(\n \"featureIds\",\n current.filter((id) => id !== feature.id),\n );\n }}\n />\n <FieldLabel htmlFor={`feature-${feature.id}`}>{feature.name}</FieldLabel>\n {/* Typography role 12 (caption) — token colour, never text-gray-*. */}\n {feature.isCore && (\n <span className=\"text-muted-foreground text-xs\">{t(\"billing.admin.prices.help.core\")}</span>\n )}\n </div>\n );\n })}\n </div>\n </div>\n )}\n\n {isRecurring && (\n <FormCheckbox\n form={form}\n id=\"isTrial\"\n name={t(\"billing.admin.prices.fields.trial\")}\n description={t(\"billing.admin.prices.help.trial\")}\n />\n )}\n </div>\n </EditorSheet>\n );\n}\n\nexport default function PriceEditor(props: PriceEditorProps) {\n return <PriceEditorInternal {...props} />;\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { createContext, ReactNode, useCallback, useContext, useMemo, useState } from \"react\";\nimport { SharedProvider } from \"../../../contexts/SharedContext\";\nimport { JsonApiHydratedDataInterface, Modules, rehydrate } from \"../../../core\";\nimport { usePageUrlGenerator } from \"../../../hooks\";\nimport { BreadcrumbItemData } from \"../../../interfaces\";\nimport { getRoleId } from \"../../../roles\";\nimport { useCurrentUserContext } from \"../../user/contexts\";\nimport { ProductArchiver } from \"../stripe-product/components/forms/ProductArchiver\";\nimport ProductEditor from \"../stripe-product/components/forms/ProductEditor\";\nimport { StripeProductInput, StripeProductInterface } from \"../stripe-product/data/stripe-product.interface\";\nimport { StripeProductService } from \"../stripe-product/data/stripe-product.service\";\n\nexport interface ProductContextType {\n product: StripeProductInterface | undefined;\n setProduct: (value: StripeProductInterface | undefined) => void;\n reloadProduct: () => Promise<void>;\n createProduct: (input: StripeProductInput) => Promise<StripeProductInterface>;\n updateProduct: (input: StripeProductInput) => Promise<StripeProductInterface>;\n archiveProduct: (id?: string) => Promise<void>;\n restoreProduct: (id?: string) => Promise<void>;\n /** Bumped by every mutation. Mounted lists re-fetch on it. */\n catalogueVersion: number;\n refreshCatalogue: () => void;\n}\n\nconst ProductContext = createContext<ProductContextType | undefined>(undefined);\n\nexport type ProductProviderProps = {\n children: ReactNode;\n dehydratedProduct?: JsonApiHydratedDataInterface;\n /**\n * Publish breadcrumbs + title into SharedProvider. Pass `false` when this\n * provider is mounted INSIDE another entity's page (the prices tab mounts\n * PriceProvider that way): SharedProvider replaces its value for the subtree\n * rather than merging (SharedContext.tsx:35-37), so a nested provider that\n * publishes chrome would blank the host page's title.\n */\n publishChrome?: boolean;\n};\n\nconst defaultContextValue: ProductContextType = {\n product: undefined,\n setProduct: () => {},\n reloadProduct: async () => {},\n createProduct: async () => {\n throw new Error(\"createProduct requires a ProductProvider\");\n },\n updateProduct: async () => {\n throw new Error(\"updateProduct requires a ProductProvider\");\n },\n archiveProduct: async () => {},\n restoreProduct: async () => {},\n catalogueVersion: 0,\n refreshCatalogue: () => {},\n};\n\nexport const ProductProvider = ({ children, dehydratedProduct, publishChrome = true }: ProductProviderProps) => {\n const t = useTranslations();\n const generateUrl = usePageUrlGenerator();\n const { hasRole } = useCurrentUserContext();\n\n const [product, setProduct] = useState<StripeProductInterface | undefined>(\n dehydratedProduct ? rehydrate<StripeProductInterface>(Modules.StripeProduct, dehydratedProduct) : undefined,\n );\n const [catalogueVersion, setCatalogueVersion] = useState<number>(0);\n\n const refreshCatalogue = useCallback(() => setCatalogueVersion((value) => value + 1), []);\n\n const reloadProduct = useCallback(async () => {\n if (!product?.id) return;\n setProduct(await StripeProductService.getProduct({ id: product.id }));\n }, [product?.id]);\n\n const createProduct = useCallback(\n async (input: StripeProductInput) => {\n const saved = await StripeProductService.createProduct(input);\n refreshCatalogue();\n return saved;\n },\n [refreshCatalogue],\n );\n\n const updateProduct = useCallback(\n async (input: StripeProductInput) => {\n const saved = await StripeProductService.updateProduct(input);\n setProduct(saved);\n refreshCatalogue();\n return saved;\n },\n [refreshCatalogue],\n );\n\n // The archive/reactivate routes answer 204 NO_CONTENT with an empty body\n // (stripe-product.controller.ts:99-115) even though the service signature\n // claims it returns a product — so the response is discarded and the entity\n // is re-read instead.\n const archiveProduct = useCallback(\n async (id?: string) => {\n const target = id ?? product?.id;\n if (!target) return;\n await StripeProductService.archiveProduct({ id: target });\n await reloadProduct();\n refreshCatalogue();\n },\n [product?.id, reloadProduct, refreshCatalogue],\n );\n\n const restoreProduct = useCallback(\n async (id?: string) => {\n const target = id ?? product?.id;\n if (!target) return;\n await StripeProductService.reactivateProduct({ id: target });\n await reloadProduct();\n refreshCatalogue();\n },\n [product?.id, reloadProduct, refreshCatalogue],\n );\n\n const value = useMemo<ProductContextType>(\n () => ({\n product,\n setProduct,\n reloadProduct,\n createProduct,\n updateProduct,\n archiveProduct,\n restoreProduct,\n catalogueVersion,\n refreshCatalogue,\n }),\n [\n product,\n reloadProduct,\n createProduct,\n updateProduct,\n archiveProduct,\n restoreProduct,\n catalogueVersion,\n refreshCatalogue,\n ],\n );\n\n if (!publishChrome) return <ProductContext.Provider value={value}>{children}</ProductContext.Provider>;\n\n const breadcrumb = (): BreadcrumbItemData[] => {\n const items: BreadcrumbItemData[] = [\n {\n name: t(\"billing.admin.products.title\"),\n // Linked only when it is NOT the current page.\n ...(product ? { href: generateUrl({ page: Modules.StripeProduct }) } : {}),\n },\n ];\n if (product) items.push({ name: product.name });\n return items;\n };\n\n const title = () => {\n const response: any = { type: t(\"billing.admin.products.title\") };\n if (!product) return response;\n\n response.element = product.name;\n\n // narr8 note: gate on hasRole ONLY. hasPermissionToModule always returns\n // false in this app, so gating an editor on it hides the editor outright.\n if (hasRole(getRoleId().Administrator)) {\n response.functions = [\n <ProductArchiver key=\"productArchiver\" product={product} />,\n <ProductEditor key=\"productEditor\" product={product} propagateChanges={setProduct} />,\n ];\n }\n\n return response;\n };\n\n return (\n <SharedProvider value={{ breadcrumbs: breadcrumb(), title: title() }}>\n <ProductContext.Provider value={value}>{children}</ProductContext.Provider>\n </SharedProvider>\n );\n};\n\nexport const useProductContext = (): ProductContextType => useContext(ProductContext) ?? defaultContextValue;\n","\"use client\";\n\nimport { ArchiveIcon, RotateCcwIcon } from \"lucide-react\";\nimport { useTranslations } from \"next-intl\";\nimport { useState } from \"react\";\nimport { errorToast } from \"../../../../../components/errors\";\nimport { Button, ConfirmDialog } from \"../../../../../shadcnui\";\nimport { useProductContext } from \"../../../contexts/ProductContext\";\nimport { StripeProductInterface } from \"../../data/stripe-product.interface\";\n\ntype ProductArchiverProps = {\n product: StripeProductInterface;\n};\n\n/**\n * Archive / restore control for a product. Built on ConfirmDialog, which is\n * controlled and trigger-less by design — the caller owns the trigger button,\n * so none of Base UI's trigger-composition pitfalls apply (no asChild, no\n * nested <button>).\n */\nexport function ProductArchiver({ product }: ProductArchiverProps) {\n const t = useTranslations();\n const { archiveProduct, restoreProduct } = useProductContext();\n const [open, setOpen] = useState<boolean>(false);\n\n const isArchived = !product.active;\n const scope = isArchived ? \"restore\" : \"archive\";\n\n return (\n <>\n <Button variant=\"outline\" size=\"sm\" onClick={() => setOpen(true)}>\n {isArchived ? <RotateCcwIcon /> : <ArchiveIcon />}\n {t(`billing.admin.products.${scope}.confirm`)}\n </Button>\n <ConfirmDialog\n open={open}\n onOpenChange={setOpen}\n title={t(`billing.admin.products.${scope}.title`)}\n description={t(`billing.admin.products.${scope}.description`, { name: product.name })}\n confirmLabel={t(`billing.admin.products.${scope}.confirm`)}\n cancelLabel={t(\"ui.buttons.cancel\")}\n destructive={!isArchived}\n onConfirm={async () => {\n try {\n if (isArchived) await restoreProduct(product.id);\n else await archiveProduct(product.id);\n } catch (error) {\n errorToast({ title: t(`billing.admin.products.${scope}.title`), error });\n throw error; // keeps the dialog open — ConfirmDialog swallows rejections\n }\n }}\n />\n </>\n );\n}\n","\"use client\";\n\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { useTranslations } from \"next-intl\";\nimport { ReactNode, useCallback, useMemo } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { v4 } from \"uuid\";\nimport { z } from \"zod\";\nimport { EditorSheet, FormInput, FormTextarea } from \"../../../../../components\";\nimport { Modules } from \"../../../../../core\";\nimport { useI18nRouter } from \"../../../../../i18n\";\nimport { useProductContext } from \"../../../contexts/ProductContext\";\nimport { StripeProductInterface } from \"../../data/stripe-product.interface\";\n\nexport type ProductEditorProps = {\n product?: StripeProductInterface;\n propagateChanges?: (product: StripeProductInterface) => void;\n onSuccess?: () => void | Promise<void>;\n trigger?: ReactNode;\n forceShow?: boolean;\n onClose?: () => void;\n dialogOpen?: boolean;\n onDialogOpenChange?: (open: boolean) => void;\n};\n\nfunction ProductEditorInternal({\n product,\n propagateChanges,\n onSuccess,\n trigger,\n forceShow,\n onClose,\n dialogOpen,\n onDialogOpenChange,\n}: ProductEditorProps) {\n const t = useTranslations();\n const router = useI18nRouter();\n const { createProduct, updateProduct } = useProductContext();\n const isEdit = !!product;\n\n const formSchema = useMemo(\n () =>\n z.object({\n id: z.uuidv4(),\n name: z.string().min(1, { message: t(\"billing.admin.products.errors.name\") }),\n description: z.string().optional(),\n }),\n [t],\n );\n\n // `active` is deliberately absent: archiving is the single deactivation path\n // (ProductArchiver), and a second toggle here would let the two disagree.\n const getDefaultValues = useCallback(\n () => ({\n id: product?.id ?? v4(),\n name: product?.name ?? \"\",\n description: product?.description ?? \"\",\n }),\n [product],\n );\n\n const form = useForm<z.infer<typeof formSchema>>({\n resolver: zodResolver(formSchema),\n defaultValues: getDefaultValues(),\n });\n\n return (\n <EditorSheet\n form={form}\n entityType={t(\"billing.admin.products.entity\")}\n entityName={product?.name}\n isEdit={isEdit}\n module={Modules.StripeProduct}\n size=\"md\"\n propagateChanges={propagateChanges}\n onSuccess={onSuccess}\n onNavigate={(url) => router.push(url)}\n onSubmit={async (values) => {\n const payload = { id: values.id, name: values.name, description: values.description };\n return isEdit ? await updateProduct(payload) : await createProduct(payload);\n }}\n onReset={getDefaultValues}\n trigger={trigger}\n forceShow={forceShow}\n onClose={onClose}\n dialogOpen={dialogOpen}\n onDialogOpenChange={onDialogOpenChange}\n >\n <div className=\"flex w-full flex-col gap-y-4\">\n <FormInput\n form={form}\n id=\"name\"\n name={t(\"billing.admin.products.fields.name\")}\n placeholder={t(\"billing.admin.products.placeholders.name\")}\n isRequired\n />\n <FormTextarea\n form={form}\n id=\"description\"\n name={t(\"billing.admin.products.fields.description\")}\n placeholder={t(\"billing.admin.products.placeholders.description\")}\n className=\"min-h-32\"\n />\n </div>\n </EditorSheet>\n );\n}\n\nexport default function ProductEditor(props: ProductEditorProps) {\n return <ProductEditorInternal {...props} />;\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { DetailField, SectionHeader } from \"../../../../../components\";\nimport { Modules } from \"../../../../../core\";\nimport { usePageUrlGenerator } from \"../../../../../hooks\";\nimport { Badge, Link } from \"../../../../../shadcnui\";\nimport { formatCurrency, formatInterval, useBillingFormatOptions } from \"../../../components/utils\";\nimport { usePriceContext } from \"../../../contexts/PriceContext\";\n\nexport function PriceDetails() {\n const t = useTranslations();\n const fmt = useBillingFormatOptions();\n const generateUrl = usePageUrlGenerator();\n const { price } = usePriceContext();\n\n if (!price) return null;\n\n const isRecurring = price.priceType === \"recurring\";\n\n return (\n <div className=\"flex w-full flex-col gap-y-8\">\n <div className=\"flex w-full flex-col gap-y-4\">\n <SectionHeader>{t(\"billing.admin.prices.title\")}</SectionHeader>\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2\">\n <DetailField\n label={t(\"billing.admin.prices.fields.product\")}\n value={\n price.product ? (\n <Link href={generateUrl({ page: Modules.StripeProduct, id: price.product.id })}>\n {price.product.name}\n </Link>\n ) : (\n \"—\"\n )\n }\n />\n <DetailField\n label={t(\"billing.admin.prices.fields.status\")}\n value={\n <span className=\"flex flex-wrap items-center gap-1\">\n {price.active ? (\n <Badge variant=\"softGreen\">{t(\"billing.admin.prices.status.active\")}</Badge>\n ) : (\n <Badge variant=\"softGray\">{t(\"billing.admin.prices.status.archived\")}</Badge>\n )}\n {price.isTrial && <Badge variant=\"softBlue\">{t(\"billing.admin.prices.badge.trial\")}</Badge>}\n </span>\n }\n />\n <DetailField\n label={t(\"billing.admin.prices.fields.amount\")}\n value={<span className=\"tabular-nums\">{formatCurrency(price.unitAmount, price.currency, fmt)}</span>}\n />\n <DetailField label={t(\"billing.admin.prices.fields.currency\")} value={price.currency.toUpperCase()} />\n <DetailField\n label={t(\"billing.admin.prices.fields.type\")}\n value={isRecurring ? t(\"billing.admin.prices.type.recurring\") : t(\"billing.admin.prices.type.oneTime\")}\n />\n <DetailField label={t(\"billing.admin.prices.fields.billing\")} value={formatInterval(price, fmt)} />\n {isRecurring && (\n <DetailField\n label={t(\"billing.admin.prices.fields.intervalCount\")}\n value={<span className=\"tabular-nums\">{price.recurring?.intervalCount ?? 1}</span>}\n />\n )}\n {isRecurring && (\n <DetailField\n label={t(\"billing.admin.prices.fields.usageType\")}\n value={\n price.recurring?.usageType === \"metered\"\n ? t(\"billing.admin.prices.usage.metered\")\n : t(\"billing.admin.prices.usage.licensed\")\n }\n />\n )}\n </div>\n </div>\n\n <div className=\"flex w-full flex-col gap-y-4\">\n <SectionHeader>{t(\"billing.admin.prices.fields.platformFeatures\")}</SectionHeader>\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2\">\n <DetailField\n label={t(\"billing.admin.prices.fields.token\")}\n value={<span className=\"tabular-nums\">{price.token ?? \"—\"}</span>}\n />\n <DetailField\n label={t(\"billing.admin.prices.fields.stripeId\")}\n value={<span className=\"tabular-nums\">{price.stripePriceId}</span>}\n />\n <DetailField\n label={t(\"billing.admin.prices.fields.description\")}\n value={price.description ?? \"—\"}\n className=\"md:col-span-2\"\n />\n <DetailField\n label={t(\"billing.admin.prices.fields.features\")}\n className=\"md:col-span-2\"\n value={\n price.features && price.features.length > 0 ? (\n <ul className=\"flex flex-col gap-1 text-sm\">\n {price.features.map((feature) => (\n <li key={feature}>{feature}</li>\n ))}\n </ul>\n ) : (\n \"—\"\n )\n }\n />\n <DetailField\n label={t(\"billing.admin.prices.fields.platformFeatures\")}\n className=\"md:col-span-2\"\n value={\n price.priceFeatures.length > 0 ? (\n <span className=\"flex flex-wrap items-center gap-1\">\n {price.priceFeatures.map((feature) => (\n <Badge key={feature.id} variant=\"softGray\">\n {feature.name}\n </Badge>\n ))}\n </span>\n ) : (\n \"—\"\n )\n }\n />\n </div>\n </div>\n </div>\n );\n}\n","\"use client\";\n\nimport { RoundPageContainer } from \"../../../../../components\";\nimport { Modules } from \"../../../../../core\";\nimport { usePriceContext } from \"../../../contexts/PriceContext\";\nimport { PriceDetails } from \"../details/PriceDetails\";\n\nexport function PriceContainer() {\n const { price } = usePriceContext();\n\n if (!price) return null;\n\n // No tabs: a price is a single flat record. RoundPageContainer without\n // `tabs` and without `fullWidth` gives the centred max-w-6xl column and the\n // title bar that PriceContext's chrome fills.\n return (\n <RoundPageContainer module={Modules.StripePrice} id={price.id}>\n <PriceDetails />\n </RoundPageContainer>\n );\n}\n","\"use client\";\n\nimport { DollarSignIcon, UploadIcon } from \"lucide-react\";\nimport { useTranslations } from \"next-intl\";\nimport { ChangeEvent, Fragment, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { ContentListTable, errorToast } from \"../../../../../components\";\nimport { Modules } from \"../../../../../core\";\nimport { DataListRetriever, useDataListRetriever } from \"../../../../../hooks\";\nimport { Button, EmptyState } from \"../../../../../shadcnui\";\nimport { usePriceContext } from \"../../../contexts/PriceContext\";\nimport { StripePriceFields } from \"../../data/stripe-price.fields\";\nimport { StripePriceInterface } from \"../../data/stripe-price.interface\";\nimport { StripePriceService } from \"../../data/stripe-price.service\";\nimport {\n buildStripePriceTransferDocument,\n parseStripePriceTransferDocument,\n priceToSeed,\n StripePriceSeed,\n stripePriceTransferFilename,\n} from \"../../data/stripe-price.transfer\";\nimport PriceEditor from \"../forms/PriceEditor\";\n\ntype PricesListProps = {\n productId: string;\n /**\n * Only when the list IS the page's own content inside a `RoundPageContainer\n * fullWidth`. Inside a tab panel leave it unset, or ContentListTable drops its\n * own `rounded-md border` and the table floats borderless in the panel.\n * Mirrors ThreatList, which is rendered both ways.\n */\n fullWidth?: boolean;\n};\n\nexport function PricesList({ productId, fullWidth }: PricesListProps) {\n const t = useTranslations();\n const { priceVersion } = usePriceContext();\n const importInputRef = useRef<HTMLInputElement>(null);\n\n // ONE seeded editor serves both clone and import. It is mounted on demand\n // (`seed &&`) rather than kept mounted and toggled, because PriceEditor builds\n // its react-hook-form defaults at construction time — a permanently mounted\n // editor would be constructed with no seed and ignore every later one.\n const [seed, setSeed] = useState<StripePriceSeed | undefined>(undefined);\n // Remount key. Cloning a second row while the first clone's editor has just\n // closed would otherwise reuse the same component instance and keep the first\n // seed's defaults.\n const [seedNonce, setSeedNonce] = useState(0);\n\n const data: DataListRetriever<StripePriceInterface> = useDataListRetriever({\n retriever: (params) => StripePriceService.listPrices({ ...params, productId }),\n retrieverParams: {},\n module: Modules.StripePrice,\n });\n\n useEffect(() => {\n if (priceVersion > 0) void data.refresh();\n }, [priceVersion]);\n\n const openSeededEditor = useCallback((next: StripePriceSeed) => {\n setSeed(next);\n setSeedNonce((nonce) => nonce + 1);\n }, []);\n\n const onClone = useCallback(\n (price: StripePriceInterface) => openSeededEditor(priceToSeed(price)),\n [openSeededEditor],\n );\n\n /**\n * Purely client-side: the document is built from the price already in memory,\n * so there is no API round trip. Download mechanics copied verbatim from\n * BackupCodesDialog.handleDownload (Blob -> createObjectURL -> anchor ->\n * click -> revokeObjectURL).\n */\n const onExport = useCallback((price: StripePriceInterface) => {\n const text = JSON.stringify(buildStripePriceTransferDocument(price), null, 2);\n const blob = new Blob([text], { type: \"application/json\" });\n const url = URL.createObjectURL(blob);\n const a = document.createElement(\"a\");\n a.href = url;\n a.download = stripePriceTransferFilename(price);\n a.click();\n URL.revokeObjectURL(url);\n }, []);\n\n // Memoised: ContentListTable is `memo`-wrapped and feeds this straight into\n // the table generator, so a fresh object every render would rebuild every\n // column on every render.\n const tableContext = useMemo(() => ({ onClone, onExport }), [onClone, onExport]);\n\n const onImportFileChange = useCallback(\n async (event: ChangeEvent<HTMLInputElement>) => {\n const input = event.target;\n const file = input.files?.[0];\n // Reset immediately (EditableAvatar.tsx:119-126 does the same) so picking\n // the SAME file twice still fires `change`. Held in `input` rather than\n // read off the event after the await, since the handler is async.\n input.value = \"\";\n if (!file) return;\n\n try {\n // NOT named `document`: that shadows the global the export path uses for\n // `document.createElement`, and a later edit that moved code between the\n // two handlers would break silently.\n const transfer = parseStripePriceTransferDocument(await file.text());\n openSeededEditor(transfer.price);\n } catch (error) {\n // The parser's own messages are hardcoded English (it is a pure,\n // i18n-free module), so they are logged for debugging and the user sees\n // the translated line instead.\n console.error(\"[PricesList] price import failed\", error);\n errorToast({\n title: t(\"billing.admin.prices.import.errors.title\"),\n error: t(\"billing.admin.prices.import.errors.invalid\"),\n });\n }\n },\n [openSeededEditor, t],\n );\n\n const functions: ReactNode[] = [\n <PriceEditor\n key=\"create-price\"\n productId={productId}\n // Present ONLY to suppress navigation: without an `onSuccess`,\n // EditorSheet.wrappedOnSubmit falls through to `onNavigate` and pushes the\n // brand-new price's page, yanking the admin off the product they were\n // editing. Deliberately does NOT call `data.refresh()` — PriceContext's\n // `createPrice` already bumps `priceVersion`, and the effect above\n // refreshes on that, so refreshing here too would fire two identical list\n // requests for one create.\n onSuccess={() => {}}\n />,\n <Fragment key=\"import-price\">\n <Button variant=\"outline\" size=\"sm\" type=\"button\" onClick={() => importInputRef.current?.click()}>\n <UploadIcon />\n {t(\"billing.admin.prices.actions.import\")}\n </Button>\n <input\n ref={importInputRef}\n type=\"file\"\n accept=\"application/json\"\n className=\"hidden\"\n onChange={onImportFileChange}\n />\n </Fragment>,\n ];\n\n return (\n <>\n <ContentListTable\n data={data}\n fields={[\n StripePriceFields.nickname,\n StripePriceFields.amount,\n StripePriceFields.interval,\n StripePriceFields.token,\n StripePriceFields.status,\n StripePriceFields.actions,\n ]}\n tableGeneratorType={Modules.StripePrice}\n functions={functions}\n context={tableContext}\n fullWidth={fullWidth}\n title={t(\"billing.admin.prices.title\")}\n emptyState={\n <EmptyState\n icon={DollarSignIcon}\n title={t(\"billing.admin.prices.empty.title\")}\n description={t(\"billing.admin.prices.empty.description\")}\n />\n }\n />\n {/* Passing `dialogOpen` makes EditorSheet render no trigger at all\n (EditorSheet.tsx:265-267), so this is a headless, caller-driven sheet. */}\n {seed && (\n <PriceEditor\n key={seedNonce}\n productId={productId}\n seed={seed}\n dialogOpen\n onDialogOpenChange={(open) => {\n if (!open) setSeed(undefined);\n }}\n onSuccess={async () => {\n setSeed(undefined);\n await data.refresh();\n }}\n />\n )}\n </>\n );\n}\n","import { z } from \"zod\";\nimport { StripePriceInterface } from \"./stripe-price.interface\";\n\n// ============================================================================\n// Stripe Price Transfer Document\n// ----------------------------------------------------------------------------\n// Pure, DOM-free: no React, no window, no Blob, no fetch. Everything here is a\n// plain data transformation so it can be unit-tested without a renderer, and so\n// both the export (download) and the clone (prefill an editor) paths can share\n// one definition of \"the editable shape of a price\".\n// ============================================================================\n\nexport const STRIPE_PRICE_TRANSFER_KIND = \"stripe-price\";\nexport const STRIPE_PRICE_TRANSFER_VERSION = 1;\n\n/** Versions this build can still read. Older documents stay parseable here. */\nconst SUPPORTED_VERSIONS: number[] = [STRIPE_PRICE_TRANSFER_VERSION];\n\n/** The editable shape of a price, portable between environments. */\nexport type StripePriceSeed = {\n unitAmount: number; // MINOR units (cents), exactly as Stripe stores it\n currency: string;\n interval: \"one_time\" | \"day\" | \"week\" | \"month\" | \"year\";\n intervalCount?: number;\n usageType?: \"licensed\" | \"metered\";\n nickname?: string;\n description?: string;\n features?: string[]; // free-text marketing bullets\n token?: number;\n isTrial?: boolean;\n platformFeatures?: string[]; // Feature entity NAMES (never ids — ids differ per environment)\n};\n\nexport type StripePriceTransferDocument = {\n kind: typeof STRIPE_PRICE_TRANSFER_KIND;\n version: typeof STRIPE_PRICE_TRANSFER_VERSION;\n price: StripePriceSeed;\n};\n\nconst seedSchema = z.object({\n unitAmount: z.number().min(0),\n currency: z.string().min(1),\n interval: z.enum([\"one_time\", \"day\", \"week\", \"month\", \"year\"]),\n intervalCount: z.number().min(1).optional(),\n usageType: z.enum([\"licensed\", \"metered\"]).optional(),\n nickname: z.string().optional(),\n description: z.string().optional(),\n features: z.array(z.string()).optional(),\n token: z.number().optional(),\n isTrial: z.boolean().optional(),\n platformFeatures: z.array(z.string()).optional(),\n});\n\n/**\n * `currency` and `priceType` are getters that THROW when the attribute is\n * absent (stripe-price.ts:42-58), and `productId` demonstrably is absent on a\n * hydrated price (see the note in PriceEditor). Read anything model-backed\n * through here so a partially-hydrated price degrades to a default instead of\n * exploding mid-export.\n */\nfunction readOptional<T>(read: () => T): T | undefined {\n try {\n return read();\n } catch {\n return undefined;\n }\n}\n\nfunction nonEmptyStrings(values: string[] | undefined): string[] {\n if (!Array.isArray(values)) return [];\n return values.map((value) => value.trim()).filter((value) => value.length > 0);\n}\n\n/**\n * Clone path: a live price -> the editable seed.\n *\n * Carries NO identity: no id, no stripePriceId, no productId, no active, no\n * lookupKey, no metadata. A clone is a brand-new Stripe price and Stripe mints\n * its own id on create; copying any of those across would either collide or\n * silently re-point the new price at the source environment's objects.\n */\nexport function priceToSeed(price: StripePriceInterface): StripePriceSeed {\n const priceType = readOptional(() => price.priceType);\n const currency = readOptional(() => price.currency);\n const recurring = price.recurring;\n\n // Same derivation as PriceEditor's getDefaultValues, so a cloned seed and the\n // form it prefills never disagree about what interval a price has.\n const interval: StripePriceSeed[\"interval\"] =\n priceType === \"one_time\" ? \"one_time\" : (recurring?.interval ?? \"month\");\n\n // MINOR units on purpose. PriceEditor divides by 100 for display; the\n // document stays in the unit Stripe itself stores.\n const seed: StripePriceSeed = {\n unitAmount: price.unitAmount ?? 0,\n currency: currency ?? \"usd\",\n interval,\n };\n\n if (interval !== \"one_time\") {\n if (typeof recurring?.intervalCount === \"number\") seed.intervalCount = recurring.intervalCount;\n if (recurring?.usageType) seed.usageType = recurring.usageType;\n }\n\n const nickname = price.nickname?.trim();\n if (nickname) seed.nickname = nickname;\n\n const description = price.description?.trim();\n if (description) seed.description = description;\n\n const features = nonEmptyStrings(price.features);\n if (features.length > 0) seed.features = features;\n\n // `0` is a meaningful token value (\"no AI\"), so test for the type, never for\n // truthiness.\n if (typeof price.token === \"number\" && !Number.isNaN(price.token)) seed.token = price.token;\n\n if (typeof price.isTrial === \"boolean\") seed.isTrial = price.isTrial;\n\n // NAMES, never ids: Feature ids differ per environment, names are the stable\n // handle an importing environment can resolve against its own Feature rows.\n const platformFeatures = nonEmptyStrings(price.priceFeatures?.map((feature) => feature.name));\n if (platformFeatures.length > 0) seed.platformFeatures = platformFeatures;\n\n return seed;\n}\n\n/** Export path. */\nexport function buildStripePriceTransferDocument(price: StripePriceInterface): StripePriceTransferDocument {\n return {\n kind: STRIPE_PRICE_TRANSFER_KIND,\n version: STRIPE_PRICE_TRANSFER_VERSION,\n price: priceToSeed(price),\n };\n}\n\nfunction slugify(value: string): string {\n return value\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n}\n\n/** Export path: a safe download filename, e.g. \"price-standard.json\". */\nexport function stripePriceTransferFilename(price: StripePriceInterface): string {\n const seed = priceToSeed(price);\n const fromNickname = seed.nickname ? slugify(seed.nickname) : \"\";\n // Fallback keeps two exports of different prices from colliding when neither\n // is nicknamed. Both halves are slugified, so no separator can survive.\n const slug = fromNickname || slugify(`${seed.unitAmount}-${seed.currency}`);\n\n return `price-${slug || \"export\"}.json`;\n}\n\n/** Import path. Throws an Error when the text is not a valid document. */\nexport function parseStripePriceTransferDocument(raw: string): StripePriceTransferDocument {\n let candidate: unknown;\n try {\n candidate = JSON.parse(raw);\n } catch {\n throw new Error(\"The file is not valid JSON.\");\n }\n\n if (typeof candidate !== \"object\" || candidate === null || Array.isArray(candidate))\n throw new Error(\"The file is not a price transfer document.\");\n\n const document = candidate as Record<string, unknown>;\n\n if (document.kind !== STRIPE_PRICE_TRANSFER_KIND)\n throw new Error(\n `The file is not a price transfer document: expected kind \"${STRIPE_PRICE_TRANSFER_KIND}\", found ${JSON.stringify(document.kind)}.`,\n );\n\n if (typeof document.version !== \"number\" || !SUPPORTED_VERSIONS.includes(document.version))\n throw new Error(\n `Unsupported price transfer version ${JSON.stringify(document.version)}. Supported: ${SUPPORTED_VERSIONS.join(\", \")}.`,\n );\n\n // Unknown keys are stripped rather than rejected, so a document written by a\n // newer build of the same version still imports.\n const parsed = seedSchema.safeParse(document.price);\n if (!parsed.success) {\n const details = parsed.error.issues\n .map((issue) => `${issue.path.join(\".\") || \"price\"}: ${issue.message}`)\n .join(\"; \");\n throw new Error(`The price in the file is not valid. ${details}`);\n }\n\n return {\n kind: STRIPE_PRICE_TRANSFER_KIND,\n version: STRIPE_PRICE_TRANSFER_VERSION,\n price: parsed.data,\n };\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { RoundPageContainer } from \"../../../../../components\";\nimport { Modules } from \"../../../../../core\";\nimport { PriceProvider } from \"../../../contexts/PriceContext\";\nimport { useProductContext } from \"../../../contexts/ProductContext\";\nimport { PricesList } from \"../../../stripe-price/components/lists/PricesList\";\nimport { ProductDetails } from \"../details/ProductDetails\";\n\nexport function ProductContainer() {\n const t = useTranslations();\n const { product } = useProductContext();\n\n if (!product) return null;\n\n return (\n <RoundPageContainer\n module={Modules.StripeProduct}\n id={product.id}\n tabs={[\n {\n sectionKey: \"details\",\n label: t(\"billing.admin.products.tabs.details\"),\n content: <ProductDetails />,\n },\n {\n key: Modules.StripePrice,\n label: t(\"billing.admin.prices.title\"),\n // publishChrome={false}: SharedProvider REPLACES its value for the\n // subtree (SharedContext.tsx:35-37), so a nested provider publishing\n // chrome here would blank this page's own title.\n // No `fillHeight` and no `fullWidth` on the list: a ContentListTable in\n // a tab keeps its own bordered card and flows with the panel, exactly\n // as ThreatList does inside GoalContainer.\n content: (\n <PriceProvider productId={product.id} publishChrome={false}>\n <PricesList productId={product.id} />\n </PriceProvider>\n ),\n },\n ]}\n />\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { DetailField, SectionHeader } from \"../../../../../components\";\nimport { Badge } from \"../../../../../shadcnui\";\nimport { useProductContext } from \"../../../contexts/ProductContext\";\n\nexport function ProductDetails() {\n const t = useTranslations();\n const { product } = useProductContext();\n\n if (!product) return null;\n\n return (\n <div className=\"flex w-full flex-col gap-y-4\">\n <SectionHeader>{t(\"billing.admin.products.tabs.details\")}</SectionHeader>\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2\">\n <DetailField label={t(\"billing.admin.products.fields.name\")} value={product.name} />\n <DetailField\n label={t(\"billing.admin.products.fields.status\")}\n value={\n product.active ? (\n <Badge variant=\"softGreen\">{t(\"billing.admin.products.status.active\")}</Badge>\n ) : (\n <Badge variant=\"softGray\">{t(\"billing.admin.products.status.archived\")}</Badge>\n )\n }\n />\n <DetailField\n label={t(\"billing.admin.products.fields.description\")}\n value={product.description ?? \"—\"}\n className=\"md:col-span-2\"\n />\n <DetailField\n label={t(\"billing.admin.products.fields.prices\")}\n value={<span className=\"tabular-nums\">{product.stripePrices.length}</span>}\n />\n <DetailField\n label={t(\"billing.admin.products.fields.stripeId\")}\n value={<span className=\"tabular-nums\">{product.stripeProductId}</span>}\n />\n </div>\n </div>\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { RoundPageContainer } from \"../../../../../components\";\nimport { Modules } from \"../../../../../core\";\nimport { CouponProvider } from \"../../../contexts/CouponContext\";\nimport { ProductProvider } from \"../../../contexts/ProductContext\";\nimport { CouponsList } from \"../../../stripe-coupon/components/lists/CouponsList\";\nimport { ProductsList } from \"../lists/ProductsList\";\n\n/**\n * Page container for the administrative products page: two tabs, Products and\n * Codes, with `sectionKey`s so the URL keeps the tab (`?section=codes`).\n *\n * Client component by necessity: `module={Modules.StripeProduct}` is a registry\n * entry carrying an icon component and methods, which a Server Component cannot\n * pass across the boundary. Routes mount this container instead — the same\n * shape as every other list page.\n *\n * No `fullWidth`, on the page or the lists: the page keeps its title bar and\n * each ContentListTable keeps its own bordered card in the tab (see ProductContainer). CouponProvider\n * is nested with `publishChrome={false}`: SharedProvider REPLACES its value for\n * the subtree (SharedContext.tsx:35-37), so a nested provider publishing chrome\n * would blank the products page's own title.\n */\nexport function ProductsListContainer() {\n const t = useTranslations();\n\n return (\n <ProductProvider>\n <CouponProvider publishChrome={false}>\n <RoundPageContainer\n module={Modules.StripeProduct}\n tabs={[\n { sectionKey: \"products\", label: t(\"billing.admin.products.tabs.products\"), content: <ProductsList /> },\n { sectionKey: \"codes\", label: t(\"billing.admin.products.tabs.codes\"), content: <CouponsList /> },\n ]}\n />\n </CouponProvider>\n </ProductProvider>\n );\n}\n","\"use client\";\n\nimport { TicketPercentIcon } from \"lucide-react\";\nimport { useTranslations } from \"next-intl\";\nimport { ReactNode, useEffect } from \"react\";\nimport { ContentListTable } from \"../../../../../components\";\nimport { Modules } from \"../../../../../core\";\nimport { DataListRetriever, useDataListRetriever } from \"../../../../../hooks\";\nimport { EmptyState } from \"../../../../../shadcnui\";\nimport { useCouponContext } from \"../../../contexts/CouponContext\";\nimport { StripeCouponFields } from \"../../data/stripe-coupon.fields\";\nimport { StripeCouponInterface } from \"../../data/stripe-coupon.interface\";\nimport { StripeCouponService } from \"../../data/stripe-coupon.service\";\nimport CouponEditor from \"../forms/CouponEditor\";\n\ntype CouponsListProps = {\n /**\n * Pass this whenever the list is the page's own content inside a\n * `RoundPageContainer fullWidth`. Without it ContentListTable wraps itself in\n * `rounded-md border`, drawing a second bordered card inside the page's\n * rounded shell.\n */\n fullWidth?: boolean;\n};\n\nexport function CouponsList({ fullWidth }: CouponsListProps = {}) {\n const t = useTranslations();\n const { catalogueVersion } = useCouponContext();\n\n const data: DataListRetriever<StripeCouponInterface> = useDataListRetriever({\n retriever: (params) => StripeCouponService.listCoupons(params),\n retrieverParams: {},\n module: Modules.StripeCoupon,\n });\n\n // Renames and code changes happen on the detail page, so the list has to\n // learn about them through the provider's counter rather than a direct callback.\n useEffect(() => {\n if (catalogueVersion > 0) void data.refresh();\n }, [catalogueVersion]);\n\n const functions: ReactNode[] = [<CouponEditor key=\"create-coupon\" />];\n\n return (\n <ContentListTable\n data={data}\n fields={[\n StripeCouponFields.name,\n StripeCouponFields.discount,\n StripeCouponFields.places,\n StripeCouponFields.codes,\n StripeCouponFields.status,\n ]}\n tableGeneratorType={Modules.StripeCoupon}\n functions={functions}\n fullWidth={fullWidth}\n title={t(\"billing.admin.coupons.title\")}\n emptyState={\n <EmptyState\n icon={TicketPercentIcon}\n title={t(\"billing.admin.coupons.empty.title\")}\n description={t(\"billing.admin.coupons.empty.description\")}\n />\n }\n />\n );\n}\n","\"use client\";\n\nimport { PackageIcon } from \"lucide-react\";\nimport { useTranslations } from \"next-intl\";\nimport { ReactNode, useEffect } from \"react\";\nimport { ContentListTable } from \"../../../../../components\";\nimport { Modules } from \"../../../../../core\";\nimport { DataListRetriever, useDataListRetriever } from \"../../../../../hooks\";\nimport { EmptyState } from \"../../../../../shadcnui\";\nimport { useProductContext } from \"../../../contexts/ProductContext\";\nimport { StripeProductFields } from \"../../data/stripe-product.fields\";\nimport { StripeProductInterface } from \"../../data/stripe-product.interface\";\nimport { StripeProductService } from \"../../data/stripe-product.service\";\nimport ProductEditor from \"../forms/ProductEditor\";\n\ntype ProductsListProps = {\n /**\n * Pass this whenever the list is the page's own content inside a\n * `RoundPageContainer fullWidth`. Without it ContentListTable wraps itself in\n * `rounded-md border`, drawing a second bordered card inside the page's\n * rounded shell.\n */\n fullWidth?: boolean;\n};\n\nexport function ProductsList({ fullWidth }: ProductsListProps = {}) {\n const t = useTranslations();\n const { catalogueVersion } = useProductContext();\n\n const data: DataListRetriever<StripeProductInterface> = useDataListRetriever({\n retriever: (params) => StripeProductService.listProducts(params),\n retrieverParams: {},\n module: Modules.StripeProduct,\n });\n\n // Archiving and restoring happen on the detail page, so the list has to learn\n // about them through the provider's counter rather than a direct callback.\n useEffect(() => {\n if (catalogueVersion > 0) void data.refresh();\n }, [catalogueVersion]);\n\n const functions: ReactNode[] = [<ProductEditor key=\"create-product\" />];\n\n return (\n <ContentListTable\n data={data}\n fields={[StripeProductFields.name, StripeProductFields.status, StripeProductFields.prices]}\n tableGeneratorType={Modules.StripeProduct}\n functions={functions}\n fullWidth={fullWidth}\n title={t(\"billing.admin.products.title\")}\n emptyState={\n <EmptyState\n icon={PackageIcon}\n title={t(\"billing.admin.products.empty.title\")}\n description={t(\"billing.admin.products.empty.description\")}\n />\n }\n />\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { RoundPageContainer } from \"../../../../../components\";\nimport { useCouponContext } from \"../../../contexts/CouponContext\";\nimport { CouponDetails } from \"../details/CouponDetails\";\nimport { CouponCodesList } from \"../lists/CouponCodesList\";\nimport { couponPageModule } from \"../../utils/coupon-page-module\";\n\nexport function CouponContainer() {\n const t = useTranslations();\n const { coupon } = useCouponContext();\n\n if (!coupon) return null;\n\n return (\n <RoundPageContainer\n module={couponPageModule()}\n id={coupon.id}\n tabs={[\n {\n sectionKey: \"details\",\n label: t(\"billing.admin.products.tabs.details\"),\n content: <CouponDetails />,\n },\n {\n sectionKey: \"codes\",\n label: t(\"billing.admin.coupons.codes.title\"),\n // No `fillHeight` and no `fullWidth` on the list: a ContentListTable in\n // a tab keeps its own bordered card and flows with the panel, exactly\n // as PricesList does inside ProductContainer. The codes list reads the\n // page's own CouponProvider, so no nested provider is needed.\n content: <CouponCodesList couponId={coupon.id} />,\n },\n ]}\n />\n );\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { DetailField, SectionHeader } from \"../../../../../components\";\nimport { Badge } from \"../../../../../shadcnui\";\nimport { formatDate, useBillingFormatOptions } from \"../../../components/utils\";\nimport { useCouponContext } from \"../../../contexts/CouponContext\";\nimport { formatCouponDiscount } from \"../../utils/format-coupon-discount\";\n\nexport function CouponDetails() {\n const t = useTranslations();\n const fmt = useBillingFormatOptions();\n const { coupon } = useCouponContext();\n\n if (!coupon) return null;\n\n return (\n <div className=\"flex w-full flex-col gap-y-4\">\n <SectionHeader>{t(\"billing.admin.products.tabs.details\")}</SectionHeader>\n {/* Typography role 10 (helper text). */}\n <p className=\"text-muted-foreground text-xs/relaxed\">{t(\"billing.admin.coupons.fixed_notice\")}</p>\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2\">\n <DetailField label={t(\"billing.admin.coupons.fields.name\")} value={coupon.name} />\n <DetailField\n label={t(\"billing.admin.coupons.fields.status\")}\n value={\n coupon.valid ? (\n <Badge variant=\"softGreen\">{t(\"billing.admin.coupons.status.active\")}</Badge>\n ) : (\n <Badge variant=\"softGray\">{t(\"billing.admin.coupons.status.ended\")}</Badge>\n )\n }\n />\n <DetailField\n label={t(\"billing.admin.coupons.fields.discount\")}\n value={<span className=\"tabular-nums\">{formatCouponDiscount(coupon, t, fmt)}</span>}\n />\n <DetailField\n label={t(\"billing.admin.coupons.fields.places\")}\n value={\n <span className=\"tabular-nums\">\n {coupon.maxRedemptions !== undefined\n ? t(\"billing.admin.coupons.places.limited\", {\n used: String(coupon.timesRedeemed),\n limit: String(coupon.maxRedemptions),\n })\n : t(\"billing.admin.coupons.places.unlimited\", { used: String(coupon.timesRedeemed) })}\n </span>\n }\n />\n <DetailField\n label={t(\"billing.admin.coupons.fields.products\")}\n value={\n coupon.stripeProductIds.length > 0 ? (\n <span className=\"tabular-nums\">{coupon.stripeProductIds.join(\", \")}</span>\n ) : (\n t(\"billing.admin.coupons.all_products\")\n )\n }\n />\n <DetailField\n label={t(\"billing.admin.coupons.fields.redeemBy\")}\n value={<span className=\"tabular-nums\">{coupon.redeemBy ? formatDate(coupon.redeemBy, fmt) : \"—\"}</span>}\n />\n <DetailField\n label={t(\"billing.admin.coupons.fields.stripeId\")}\n value={<span className=\"tabular-nums\">{coupon.stripeCouponId}</span>}\n />\n </div>\n </div>\n );\n}\n","\"use client\";\n\nimport { TicketIcon } from \"lucide-react\";\nimport { useTranslations } from \"next-intl\";\nimport { ReactNode, useEffect } from \"react\";\nimport { ContentListTable } from \"../../../../../components\";\nimport { Modules } from \"../../../../../core\";\nimport { DataListRetriever, useDataListRetriever } from \"../../../../../hooks\";\nimport { EmptyState } from \"../../../../../shadcnui\";\nimport { useCouponContext } from \"../../../contexts/CouponContext\";\nimport { StripeCouponCodeFields } from \"../../data/stripe-coupon-code.fields\";\nimport { StripeCouponCodeInterface } from \"../../data/stripe-coupon-code.interface\";\nimport { StripeCouponCodeService } from \"../../data/stripe-coupon-code.service\";\nimport CouponCodeEditor from \"../forms/CouponCodeEditor\";\n\ntype CouponCodesListProps = {\n couponId: string;\n};\n\nexport function CouponCodesList({ couponId }: CouponCodesListProps) {\n const t = useTranslations();\n const { catalogueVersion } = useCouponContext();\n\n const data: DataListRetriever<StripeCouponCodeInterface> = useDataListRetriever({\n retriever: (params) => StripeCouponCodeService.listByCoupon({ ...params, couponId }),\n retrieverParams: {},\n module: Modules.StripeCouponCode,\n });\n\n // Switching \"Apply automatically\" on one code clears it on the others in the\n // same server write, so every flag change re-reads the whole list.\n useEffect(() => {\n if (catalogueVersion > 0) void data.refresh();\n }, [catalogueVersion]);\n\n const functions: ReactNode[] = [<CouponCodeEditor key=\"create-code\" couponId={couponId} />];\n\n return (\n <ContentListTable\n data={data}\n fields={[\n StripeCouponCodeFields.code,\n StripeCouponCodeFields.places,\n StripeCouponCodeFields.expiresAt,\n StripeCouponCodeFields.automatic,\n StripeCouponCodeFields.status,\n StripeCouponCodeFields.actions,\n ]}\n tableGeneratorType={Modules.StripeCouponCode}\n functions={functions}\n title={t(\"billing.admin.coupons.codes.title\")}\n emptyState={\n <EmptyState\n icon={TicketIcon}\n title={t(\"billing.admin.coupons.codes.empty.title\")}\n description={t(\"billing.admin.coupons.codes.empty.description\")}\n />\n }\n />\n );\n}\n","\"use client\";\n\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { useTranslations } from \"next-intl\";\nimport { ReactNode, useCallback, useMemo } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { v4 } from \"uuid\";\nimport { z } from \"zod\";\nimport { EditorSheet } from \"../../../../../components\";\nimport { Modules } from \"../../../../../core\";\nimport { useI18nRouter } from \"../../../../../i18n\";\nimport { useCouponContext } from \"../../../contexts/CouponContext\";\nimport { CouponCodeFields, couponCodeDefaults, couponCodeSchema, toCouponCodeInput } from \"./CouponCodeFields\";\n\nexport type CouponCodeEditorProps = {\n couponId: string;\n onSuccess?: () => void | Promise<void>;\n trigger?: ReactNode;\n forceShow?: boolean;\n onClose?: () => void;\n dialogOpen?: boolean;\n onDialogOpenChange?: (open: boolean) => void;\n};\n\n/**\n * Create-only: Stripe allows changing a promotion code's `active` flag and\n * little else, so a code is never edited here. \"Apply automatically\" and\n * deactivate / reactivate live on the row (CouponCodeActions).\n */\nfunction CouponCodeEditorInternal({\n couponId,\n onSuccess,\n trigger,\n forceShow,\n onClose,\n dialogOpen,\n onDialogOpenChange,\n}: CouponCodeEditorProps) {\n const t = useTranslations();\n const router = useI18nRouter();\n const { createCode } = useCouponContext();\n\n const formSchema = useMemo(() => z.object({ id: z.uuidv4() }).and(couponCodeSchema(t)), [t]);\n type CouponCodeEditorValues = z.infer<typeof formSchema>;\n\n const getDefaultValues = useCallback((): CouponCodeEditorValues => ({ id: v4(), ...couponCodeDefaults() }), []);\n\n const form = useForm<CouponCodeEditorValues>({\n resolver: zodResolver(formSchema),\n defaultValues: getDefaultValues(),\n });\n\n return (\n <EditorSheet\n form={form}\n entityType={t(\"billing.admin.coupons.codes.entity\")}\n isEdit={false}\n module={Modules.StripeCouponCode}\n size=\"md\"\n // A code has no page of its own: without onSuccess EditorSheet would\n // navigate to `<module pageUrl>/<codeId>`, which does not exist.\n // The context's createCode already reloads the coupon and the codes list.\n onSuccess={onSuccess ?? (() => {})}\n onNavigate={(url) => router.push(url)}\n onSubmit={async (values) => await createCode({ id: values.id, couponId, ...toCouponCodeInput(values) })}\n onReset={getDefaultValues}\n trigger={trigger}\n forceShow={forceShow}\n onClose={onClose}\n dialogOpen={dialogOpen}\n onDialogOpenChange={onDialogOpenChange}\n >\n <CouponCodeFields form={form} />\n </EditorSheet>\n );\n}\n\nexport default function CouponCodeEditor(props: CouponCodeEditorProps) {\n return <CouponCodeEditorInternal {...props} />;\n}\n"]}