@burdenoff/microfe-billing 2026.821.1 → 2026.825.2

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 (145) hide show
  1. package/dist/billing/BillingAdminRoutes.js.map +1 -1
  2. package/dist/billing/BillingRoot.js.map +1 -1
  3. package/dist/billing/BillingUserRoutes.js.map +1 -1
  4. package/dist/billing/hooks/useBillingAccountSelection.js.map +1 -1
  5. package/dist/billing/hooks/useBillingEventEmitter.js.map +1 -1
  6. package/dist/billing/hooks/useBillingNavigate.js.map +1 -1
  7. package/dist/billing/hooks/useBillingPermissions.js.map +1 -1
  8. package/dist/billing/hooks/useDefaultBillingCurrency.js.map +1 -1
  9. package/dist/billing/modules/addons/components/FloatingCartDrawer.js.map +1 -1
  10. package/dist/billing/modules/addons/hooks/useAddons.js.map +1 -1
  11. package/dist/billing/modules/addons/pages/AddonBrowseDetailPage.js.map +1 -1
  12. package/dist/billing/modules/addons/pages/AddonCreatePage.js.map +1 -1
  13. package/dist/billing/modules/addons/pages/AddonDetailPage.js.map +1 -1
  14. package/dist/billing/modules/addons/pages/AddonEditPage.js.map +1 -1
  15. package/dist/billing/modules/addons/pages/AddonsBrowsePage.js.map +1 -1
  16. package/dist/billing/modules/addons/pages/AddonsListPage.js.map +1 -1
  17. package/dist/billing/modules/addons/store/addonCartStore.js.map +1 -1
  18. package/dist/billing/modules/affiliate/api.js.map +1 -1
  19. package/dist/billing/modules/affiliate/pages/AffiliateDashboardPage.js +107 -107
  20. package/dist/billing/modules/affiliate/pages/AffiliateDashboardPage.js.map +1 -1
  21. package/dist/billing/modules/analytics/pages/AnalyticsPage.js +80 -81
  22. package/dist/billing/modules/analytics/pages/AnalyticsPage.js.map +1 -1
  23. package/dist/billing/modules/billing/hooks/useInvoices.js.map +1 -1
  24. package/dist/billing/modules/billing/hooks/useRefunds.js.map +1 -1
  25. package/dist/billing/modules/billing/hooks/useTransactions.js.map +1 -1
  26. package/dist/billing/modules/billing/pages/InvoiceDetailPage.js.map +1 -1
  27. package/dist/billing/modules/billing/pages/InvoicesListPage.js +1 -1
  28. package/dist/billing/modules/billing/pages/InvoicesListPage.js.map +1 -1
  29. package/dist/billing/modules/billing/pages/PaymentMethodsPage.js.map +1 -1
  30. package/dist/billing/modules/billing/pages/RefundsPage.js.map +1 -1
  31. package/dist/billing/modules/billing/pages/TransactionsPage.js.map +1 -1
  32. package/dist/billing/modules/billing/pages/UserRefundsPage.js +127 -127
  33. package/dist/billing/modules/billing/pages/UserRefundsPage.js.map +1 -1
  34. package/dist/billing/modules/checkout/components/BillingAccountSelector.js.map +1 -1
  35. package/dist/billing/modules/checkout/components/CompleteBillingAccountModal.js.map +1 -1
  36. package/dist/billing/modules/checkout/components/CreditsAmountInput.js.map +1 -1
  37. package/dist/billing/modules/checkout/components/NativeCheckoutView.js.map +1 -1
  38. package/dist/billing/modules/checkout/components/OrderSummary.js.map +1 -1
  39. package/dist/billing/modules/checkout/components/PromoCodeInput.js.map +1 -1
  40. package/dist/billing/modules/checkout/components/RazorpayPaymentButton.js.map +1 -1
  41. package/dist/billing/modules/checkout/components/SimpleAutoPayToggle.js.map +1 -1
  42. package/dist/billing/modules/checkout/components/StoreShippingSection.js.map +1 -1
  43. package/dist/billing/modules/checkout/components/StripePaymentForm.js.map +1 -1
  44. package/dist/billing/modules/checkout/components/SubscriptionSelector.js.map +1 -1
  45. package/dist/billing/modules/checkout/components/UnifiedPaymentMethodSelector.js.map +1 -1
  46. package/dist/billing/modules/checkout/components/WorkspaceQuotaAssignmentModal.js.map +1 -1
  47. package/dist/billing/modules/checkout/hooks/useCheckout.js +0 -1
  48. package/dist/billing/modules/checkout/hooks/useCheckout.js.map +1 -1
  49. package/dist/billing/modules/checkout/hooks/useNativeCheckout.js.map +1 -1
  50. package/dist/billing/modules/checkout/hooks/usePaymentProviders.js.map +1 -1
  51. package/dist/billing/modules/checkout/hooks/usePaymentStatusPolling.js.map +1 -1
  52. package/dist/billing/modules/checkout/hooks/useStoreShipping.js.map +1 -1
  53. package/dist/billing/modules/checkout/pages/CheckoutPage.js +412 -395
  54. package/dist/billing/modules/checkout/pages/CheckoutPage.js.map +1 -1
  55. package/dist/billing/modules/checkout/utils/storeOrder.js.map +1 -1
  56. package/dist/billing/modules/coupons/pages/CouponDetailPage.js.map +1 -1
  57. package/dist/billing/modules/coupons/pages/CouponsListPage.js.map +1 -1
  58. package/dist/billing/modules/coupons/pages/CreateCouponPage.js.map +1 -1
  59. package/dist/billing/modules/coupons/pages/EditCouponPage.js.map +1 -1
  60. package/dist/billing/modules/credits/hooks/useCredits.js.map +1 -1
  61. package/dist/billing/modules/credits/pages/AdminCreditsPage.js.map +1 -1
  62. package/dist/billing/modules/credits/pages/CreditTransactionsPage.js.map +1 -1
  63. package/dist/billing/modules/credits/pages/CreditWithdrawalPage.js.map +1 -1
  64. package/dist/billing/modules/credits/pages/CreditsPage.js.map +1 -1
  65. package/dist/billing/modules/dashboard/components/ActiveSubscriptionCard.js.map +1 -1
  66. package/dist/billing/modules/dashboard/components/AddPaymentMethodModal.js.map +1 -1
  67. package/dist/billing/modules/dashboard/components/BillingAccountCard.js.map +1 -1
  68. package/dist/billing/modules/dashboard/components/CreateBillingAccountModal.js.map +1 -1
  69. package/dist/billing/modules/dashboard/components/PaymentMethodsSection.js.map +1 -1
  70. package/dist/billing/modules/dashboard/components/RecommendedAddonsSection.js.map +1 -1
  71. package/dist/billing/modules/dashboard/hooks/useBillingOverviewData.js.map +1 -1
  72. package/dist/billing/modules/dashboard/hooks/useDashboard.js.map +1 -1
  73. package/dist/billing/modules/dashboard/pages/OverviewPage.js.map +1 -1
  74. package/dist/billing/modules/discounts/pages/DiscountsListPage.js.map +1 -1
  75. package/dist/billing/modules/earnings/api.js.map +1 -1
  76. package/dist/billing/modules/earnings/pages/DeveloperRevenueAdminPage.js +145 -145
  77. package/dist/billing/modules/earnings/pages/DeveloperRevenueAdminPage.js.map +1 -1
  78. package/dist/billing/modules/earnings/pages/EarningsPage.js +133 -133
  79. package/dist/billing/modules/earnings/pages/EarningsPage.js.map +1 -1
  80. package/dist/billing/modules/earnings/pages/PayoutAdminPage.js +7 -9
  81. package/dist/billing/modules/earnings/pages/PayoutAdminPage.js.map +1 -1
  82. package/dist/billing/modules/earnings/pages/PayoutsPage.js +95 -95
  83. package/dist/billing/modules/earnings/pages/PayoutsPage.js.map +1 -1
  84. package/dist/billing/modules/plans/constants/featuredQuotas.js.map +1 -1
  85. package/dist/billing/modules/plans/hooks/usePlans.js.map +1 -1
  86. package/dist/billing/modules/plans/hooks/useQuotas.js.map +1 -1
  87. package/dist/billing/modules/plans/pages/PlanAssignFreePage.js.map +1 -1
  88. package/dist/billing/modules/plans/pages/PlanBrowseDetailPage.js.map +1 -1
  89. package/dist/billing/modules/plans/pages/PlanCreatePage.js.map +1 -1
  90. package/dist/billing/modules/plans/pages/PlanDetailPage.js.map +1 -1
  91. package/dist/billing/modules/plans/pages/PlanEditPage.js.map +1 -1
  92. package/dist/billing/modules/plans/pages/PlansBrowsePage.js.map +1 -1
  93. package/dist/billing/modules/plans/pages/PlansListPage.js.map +1 -1
  94. package/dist/billing/modules/promotions/pages/CreatePromotionPage.js.map +1 -1
  95. package/dist/billing/modules/promotions/pages/PromotionsListPage.js.map +1 -1
  96. package/dist/billing/modules/quota/hooks/useQuotaTemplates.js.map +1 -1
  97. package/dist/billing/modules/quota/pages/QuotaAssignFreePage.js.map +1 -1
  98. package/dist/billing/modules/quota/pages/QuotaTemplateCreatePage.js.map +1 -1
  99. package/dist/billing/modules/quota/pages/QuotaTemplateDetailPage.js.map +1 -1
  100. package/dist/billing/modules/quota/pages/QuotaTemplateEditPage.js.map +1 -1
  101. package/dist/billing/modules/quota/pages/QuotaTemplatesListPage.js.map +1 -1
  102. package/dist/billing/modules/settings/pages/SettingsPage.js.map +1 -1
  103. package/dist/billing/modules/settings/pages/TeamPermissionsPage.js.map +1 -1
  104. package/dist/billing/modules/store/pages/StoreCheckoutPage.js +5 -6
  105. package/dist/billing/modules/store/pages/StoreCheckoutPage.js.map +1 -1
  106. package/dist/billing/modules/subscriptions/components/ManageSeatsModal.js.map +1 -1
  107. package/dist/billing/modules/subscriptions/hooks/useSubscriptions.js.map +1 -1
  108. package/dist/billing/modules/subscriptions/pages/SubscriptionDetailPage.js.map +1 -1
  109. package/dist/billing/modules/subscriptions/pages/SubscriptionsListPage.js.map +1 -1
  110. package/dist/billing/modules/subscriptions/pages/UpgradeSubscriptionPage.js.map +1 -1
  111. package/dist/billing/modules/usage/hooks/useUsage.js +14 -9
  112. package/dist/billing/modules/usage/hooks/useUsage.js.map +1 -1
  113. package/dist/billing/modules/usage/pages/UsagePage.js +481 -453
  114. package/dist/billing/modules/usage/pages/UsagePage.js.map +1 -1
  115. package/dist/billing/providers/BillingProvider.js.map +1 -1
  116. package/dist/billing/shared/components/AccessDenied.js.map +1 -1
  117. package/dist/billing/shared/components/ActorIdentity.js.map +1 -1
  118. package/dist/billing/shared/components/CurrencyPriceOverridesEditor.js.map +1 -1
  119. package/dist/billing/shared/components/EmptyState.js.map +1 -1
  120. package/dist/billing/shared/components/PageHeader.js.map +1 -1
  121. package/dist/billing/shared/components/ServerError.js.map +1 -1
  122. package/dist/billing/shared/components/StatCard.js.map +1 -1
  123. package/dist/billing/shared/hooks/useActorProfiles.js.map +1 -1
  124. package/dist/billing/shared/types/graphql.js.map +1 -1
  125. package/dist/billing/shared/ui/Card.js.map +1 -1
  126. package/dist/billing/shared/ui/MetricCard.js.map +1 -1
  127. package/dist/billing/shared/ui/ProgressBar.js.map +1 -1
  128. package/dist/billing/shared/ui/StatusPill.js.map +1 -1
  129. package/dist/billing/shared/utils/directBillingGraphql.js.map +1 -1
  130. package/dist/billing/shared/utils/error.js.map +1 -1
  131. package/dist/billing/shared/utils/format.js.map +1 -1
  132. package/dist/billing/shared/utils/gatewayGraphql.js.map +1 -1
  133. package/dist/billing/shared/utils/navigation.js.map +1 -1
  134. package/dist/billing/shared/utils/paymentTheme.js.map +1 -1
  135. package/dist/billing/shared/utils/planFeatureQuantity.js.map +1 -1
  136. package/dist/billing/shared/utils/status.js.map +1 -1
  137. package/dist/billing/shared/utils/tokens.js.map +1 -1
  138. package/dist/generated/global-operations.js +5 -1
  139. package/dist/generated/global-operations.js.map +1 -1
  140. package/dist/generated/global-types.js.map +1 -1
  141. package/dist/generated/wspace-operations.js +1 -1
  142. package/dist/generated/wspace-operations.js.map +1 -1
  143. package/dist/node_modules/@capacitor/core/dist/index.js +1 -1
  144. package/dist/node_modules/@capacitor/core/dist/index.js.map +1 -1
  145. package/package.json +13 -10
@@ -1 +1 @@
1
- {"version":3,"file":"OrderSummary.js","names":[],"sources":["../../../../../src/billing/modules/checkout/components/OrderSummary.tsx"],"sourcesContent":["/**\n * Order Summary Component\n * Displays checkout items, discounts, taxes, and total\n */\n\nimport { type FC, useMemo } from 'react';\nimport {\n Package,\n Blocks,\n Coins,\n Minus,\n Plus,\n Trash2,\n Receipt,\n Check,\n ShoppingCart,\n} from 'lucide-react';\nimport {\n Accordion,\n AccordionItem,\n AccordionTrigger,\n AccordionContent,\n} from '@burdenoff/fe-libs/ui';\nimport type { CheckoutItem, Currency, CheckoutFeature } from '../types';\nimport { formatCurrency } from '../../../shared/utils/format';\nimport { categoryTokens, statusTokens } from '../../../shared/utils/tokens';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\n\n/**\n * Credit conversion rate: How many credits equal 1 USD\n * This should match the backend creditRate.rateUSD (default 0.01 means 100 credits = $1)\n */\nconst CREDITS_PER_USD = 100;\n\ninterface OrderSummaryProps {\n items: CheckoutItem[];\n subtotal: number;\n discountAmount: number;\n taxAmount: number;\n taxRate: number;\n taxType?: string;\n total: number;\n currency: Currency;\n showInCredits?: boolean;\n /** Whether tax is being calculated */\n isCalculatingTax?: boolean;\n onUpdateQuantity?: (itemId: string, quantity: number) => void;\n onRemoveItem?: (itemId: string) => void;\n isEditable?: boolean;\n}\n\nexport const OrderSummary: FC<OrderSummaryProps> = ({\n items,\n subtotal,\n discountAmount,\n taxAmount,\n taxRate,\n taxType,\n total,\n currency,\n showInCredits = false,\n isCalculatingTax = false,\n onUpdateQuantity,\n onRemoveItem,\n isEditable = true,\n}) => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n const getItemIcon = (type: CheckoutItem['type']) => {\n switch (type) {\n case 'PLAN':\n return <Package className=\"size-5\" />;\n case 'ADDON':\n return <Blocks className=\"size-5\" />;\n case 'CREDITS':\n return <Coins className=\"size-5\" />;\n case 'ORDER':\n return <ShoppingCart className=\"size-5\" />;\n default:\n return <Package className=\"size-5\" />;\n }\n };\n\n const getItemTypeBadge = (type: CheckoutItem['type']) => {\n const badges = {\n PLAN: {\n label: 'Plan',\n className: `${categoryTokens.plan.bg} ${categoryTokens.plan.text}`,\n },\n ADDON: {\n label: 'Add-on',\n className: `${categoryTokens.addon.bg} ${categoryTokens.addon.text}`,\n },\n CREDITS: {\n label: 'Credits',\n className: `${categoryTokens.credits.bg} ${categoryTokens.credits.text}`,\n },\n ORDER: {\n label: 'Store Order',\n className: 'bg-status-success-bg-subtle text-status-success-text',\n },\n };\n return badges[type];\n };\n\n // Convert USD amount to credits\n const usdToCredits = (usdAmount: number) => usdAmount * CREDITS_PER_USD;\n\n // Subtotal in USD — used for credit conversion (items may be priced in local currency)\n const subtotalUSD = useMemo(\n () => items.reduce((sum, item) => sum + (item.priceUSD ?? item.price) * item.quantity, 0),\n [items]\n );\n\n // Format amount - show in credits if paying with credits, otherwise in selected currency\n // Items now have their prices stored in the selected currency (from currencyPrices)\n // For credit conversion always use priceUSD (base USD price) — not the localized price.\n const formatAmount = (amount: number, item?: CheckoutItem) => {\n if (showInCredits) {\n const usdAmount = item?.priceUSD ?? amount;\n const creditsAmount = usdToCredits(usdAmount);\n return `${creditsAmount.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 0 })} Credits`;\n }\n // Format in selected currency (items now have localized prices)\n return formatCurrency(amount, currency);\n };\n\n // Format limit value for display\n const formatLimitValue = (_key: string, value: unknown): string => {\n if (typeof value === 'boolean') {\n return value ? 'Included' : 'Not included';\n }\n if (typeof value === 'number') {\n if (value === -1 || value === Infinity) return 'Unlimited';\n return value.toLocaleString();\n }\n return String(value);\n };\n\n // Merge duplicate features by name, combining numeric limits\n const mergeFeatures = (features: CheckoutFeature[]): CheckoutFeature[] => {\n const merged = new Map<string, CheckoutFeature>();\n\n for (const feature of features) {\n const existing = merged.get(feature.name);\n if (existing) {\n // Merge limits - add numeric values together\n const mergedLimits: Record<string, number | string | boolean> = { ...existing.limits };\n if (feature.limits) {\n for (const [key, value] of Object.entries(feature.limits)) {\n const existingValue = mergedLimits[key];\n if (typeof value === 'number' && typeof existingValue === 'number') {\n // Both are numbers - add them (unless unlimited)\n if (existingValue === -1 || value === -1) {\n mergedLimits[key] = -1; // Unlimited stays unlimited\n } else {\n mergedLimits[key] = existingValue + value;\n }\n } else {\n // Non-numeric or first occurrence - just set\n mergedLimits[key] = value;\n }\n }\n }\n existing.limits = mergedLimits;\n } else {\n // Clone the feature to avoid mutating original\n merged.set(feature.name, {\n ...feature,\n limits: feature.limits ? { ...feature.limits } : undefined,\n });\n }\n }\n\n return Array.from(merged.values());\n };\n\n // Render features list\n const renderFeatures = (features: CheckoutFeature[]) => {\n if (!features || features.length === 0) return null;\n\n const mergedFeatures = mergeFeatures(features);\n\n return (\n <ul className=\"space-y-2\">\n {mergedFeatures.map((feature) => (\n <li key={feature.id} className=\"flex items-start gap-2\">\n <Check className={`size-4 ${statusTokens.success.icon} shrink-0 mt-0.5`} />\n <div className=\"flex-1 min-w-0\">\n <span className=\"text-sm text-text-primary\">{feature.name}</span>\n {feature.limits && Object.keys(feature.limits).length > 0 && (\n <div className=\"mt-1 flex flex-wrap gap-1.5\">\n {Object.entries(feature.limits).map(([key, value]) => (\n <span\n key={key}\n className=\"inline-flex items-center text-xs px-1.5 py-0.5 rounded bg-bg-sunken text-text-secondary\"\n >\n {key}: {formatLimitValue(key, value)}\n </span>\n ))}\n </div>\n )}\n </div>\n </li>\n ))}\n </ul>\n );\n };\n\n return (\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface overflow-hidden\">\n {/* Header */}\n <div className=\"px-4 py-3 border-b border-border-subtle bg-bg-sunken\">\n <div className=\"flex items-center gap-2\">\n <Receipt className=\"size-4 text-text-secondary\" />\n <h3 className=\"font-semibold text-text-primary\">\n {tr('billing.checkout.orderSummary', 'Order Summary')}\n </h3>\n </div>\n </div>\n\n {/* Items */}\n <div className=\"divide-y divide-border-subtle\">\n {items.length === 0 ? (\n <div className=\"p-6 text-center\">\n <Package className=\"mx-auto size-8 text-text-secondary\" />\n <p className=\"mt-2 text-sm text-text-secondary\">No items in checkout</p>\n </div>\n ) : (\n items.map((item) => {\n const badge = getItemTypeBadge(item.type);\n const hasFeatures = item.features && item.features.length > 0;\n\n return (\n <div key={item.id} className=\"p-4\">\n <div className=\"flex items-start gap-3\">\n {/* Icon */}\n <div className=\"p-2 rounded-lg bg-bg-sunken text-text-secondary shrink-0\">\n {getItemIcon(item.type)}\n </div>\n\n {/* Item details */}\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2 flex-wrap\">\n <span className=\"font-medium text-text-primary\">{item.name}</span>\n <span className={`text-xs px-1.5 py-0.5 rounded ${badge.className}`}>\n {badge.label}\n </span>\n </div>\n {item.description && (\n <p className=\"text-sm text-text-secondary mt-0.5 line-clamp-2\">\n {item.description}\n </p>\n )}\n\n {/* Quantity controls */}\n <div className=\"flex items-center justify-between mt-3\">\n {isEditable && item.type !== 'PLAN' ? (\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => onUpdateQuantity?.(item.id, item.quantity - 1)}\n disabled={item.quantity <= 1}\n className=\"p-2 rounded border border-border-subtle hover:bg-bg-sunken disabled:opacity-50 disabled:cursor-not-allowed min-w-[36px] min-h-[36px] flex items-center justify-center\"\n aria-label={`Decrease ${item.name} quantity`}\n >\n <Minus className=\"size-3.5\" />\n </button>\n <span className=\"w-8 text-center text-sm font-medium\">\n {item.quantity}\n </span>\n <button\n type=\"button\"\n onClick={() => onUpdateQuantity?.(item.id, item.quantity + 1)}\n className=\"p-2 rounded border border-border-subtle hover:bg-bg-sunken min-w-[36px] min-h-[36px] flex items-center justify-center\"\n aria-label={`Increase ${item.name} quantity`}\n >\n <Plus className=\"size-3.5\" />\n </button>\n </div>\n ) : (\n <span className=\"text-sm text-text-secondary\">Qty: {item.quantity}</span>\n )}\n\n <div className=\"flex items-center gap-3\">\n <span className=\"font-medium text-text-primary\">\n {formatAmount(item.price * item.quantity, item)}\n </span>\n {isEditable && onRemoveItem && (\n <button\n type=\"button\"\n onClick={() => onRemoveItem(item.id)}\n className=\"p-1.5 rounded text-text-secondary hover:text-status-error-text hover:bg-status-error-bg-subtle\"\n aria-label={`Remove ${item.name}`}\n >\n <Trash2 className=\"size-4\" />\n </button>\n )}\n </div>\n </div>\n\n {/* Features - Collapsible */}\n {hasFeatures &&\n (() => {\n const mergedCount = mergeFeatures(item.features!).length;\n return (\n <Accordion type=\"single\" collapsible className=\"mt-3\">\n <AccordionItem value=\"features\" className=\"border-0\">\n <AccordionTrigger className=\"py-2 px-0 text-sm text-text-secondary hover:text-text-primary hover:no-underline\">\n <span className=\"flex items-center gap-1.5\">\n <Check className=\"size-3.5\" />\n {mergedCount} {mergedCount === 1 ? 'feature' : 'features'}{' '}\n included\n </span>\n </AccordionTrigger>\n <AccordionContent className=\"pt-2 pb-0\">\n {renderFeatures(item.features!)}\n </AccordionContent>\n </AccordionItem>\n </Accordion>\n );\n })()}\n </div>\n </div>\n </div>\n );\n })\n )}\n </div>\n\n {/* Totals */}\n {items.length > 0 && (\n <div className=\"border-t border-border-subtle bg-bg-sunken\">\n <div className=\"p-4 space-y-2\">\n {/* Subtotal - already in selected currency (from currencyPrices) */}\n <div className=\"flex items-center justify-between text-sm\">\n <span className=\"text-text-secondary\">\n {tr('billing.checkout.subtotal', 'Subtotal')}\n </span>\n <span className=\"text-text-primary\">\n {showInCredits ? formatAmount(subtotalUSD) : formatAmount(subtotal)}\n </span>\n </div>\n\n {/* Discount */}\n {discountAmount > 0 && (\n <div className=\"flex items-center justify-between text-sm\">\n <span className={statusTokens.success.text}>Discount</span>\n <span className={statusTokens.success.text}>-{formatAmount(discountAmount)}</span>\n </div>\n )}\n\n {/* Tax - calculated on the subtotal in selected currency */}\n {isCalculatingTax ? (\n <div className=\"flex items-center justify-between text-sm\">\n <span className=\"text-text-secondary\">Tax</span>\n <span className=\"text-text-secondary animate-pulse\">Calculating…</span>\n </div>\n ) : taxAmount > 0 && taxRate > 0 ? (\n <div className=\"flex items-center justify-between text-sm\">\n <span className=\"text-text-secondary\">\n {taxType || 'Tax'} ({(taxRate * 100).toFixed(0)}%)\n </span>\n <span className=\"text-text-primary\">{formatAmount(taxAmount)}</span>\n </div>\n ) : null}\n\n {/* Divider */}\n <div className=\"border-t border-border-subtle my-2\" />\n\n {/* Total - show in credits or selected currency */}\n {showInCredits ? (\n <>\n {/* Credit payment: Show total in credits */}\n <div className=\"flex items-center justify-between\">\n <span className=\"font-semibold text-text-primary\">\n {tr('billing.checkout.total', 'Total')}\n </span>\n <span className=\"text-xl font-bold text-text-primary\">\n {usdToCredits(subtotalUSD).toLocaleString(undefined, {\n minimumFractionDigits: 0,\n maximumFractionDigits: 0,\n })}{' '}\n Credits\n </span>\n </div>\n <p className=\"text-xs text-center text-text-secondary pt-2\">\n Paying with account credits\n </p>\n </>\n ) : (\n <>\n {/* Card payment: Show total in selected currency */}\n {/* Items already have localized prices from currencyPrices - no conversion needed */}\n <div className=\"flex items-center justify-between\">\n <span className=\"font-semibold text-text-primary\">Total in {currency}</span>\n <span className=\"text-xl font-bold text-text-primary\">\n {formatCurrency(total, currency)}\n </span>\n </div>\n </>\n )}\n </div>\n </div>\n )}\n </div>\n );\n};\n"],"mappings":";;;;;;;;AAgCA,IAAM,IAAkB,KAmBX,KAAuC,EAClD,UACA,aACA,mBACA,cACA,YACA,YACA,UACA,aACA,mBAAgB,IAChB,sBAAmB,IACnB,qBACA,iBACA,gBAAa,SACT;CACJ,IAAM,EAAE,SAAM,GAAS,EACjB,KAAM,GAAa,MAA6B;EACpD,IAAM,IAAa,EAAE,EAAI;AACzB,SAAO,MAAe,IAAM,IAAW;IAEnC,KAAe,MAA+B;AAClD,UAAQ,GAAR;GACE,KAAK,OACH,QAAO,kBAAC,GAAD,EAAS,WAAU,UAAW,CAAA;GACvC,KAAK,QACH,QAAO,kBAAC,GAAD,EAAQ,WAAU,UAAW,CAAA;GACtC,KAAK,UACH,QAAO,kBAAC,GAAD,EAAO,WAAU,UAAW,CAAA;GACrC,KAAK,QACH,QAAO,kBAAC,GAAD,EAAc,WAAU,UAAW,CAAA;GAC5C,QACE,QAAO,kBAAC,GAAD,EAAS,WAAU,UAAW,CAAA;;IAIrC,KAAoB,OACT;EACb,MAAM;GACJ,OAAO;GACP,WAAW,GAAG,EAAe,KAAK,GAAG,GAAG,EAAe,KAAK;GAC7D;EACD,OAAO;GACL,OAAO;GACP,WAAW,GAAG,EAAe,MAAM,GAAG,GAAG,EAAe,MAAM;GAC/D;EACD,SAAS;GACP,OAAO;GACP,WAAW,GAAG,EAAe,QAAQ,GAAG,GAAG,EAAe,QAAQ;GACnE;EACD,OAAO;GACL,OAAO;GACP,WAAW;GACZ;EACF,EACa,IAIV,KAAgB,MAAsB,IAAY,GAGlD,IAAc,QACZ,EAAM,QAAQ,GAAK,MAAS,KAAO,EAAK,YAAY,EAAK,SAAS,EAAK,UAAU,EAAE,EACzF,CAAC,EAAM,CACR,EAKK,KAAgB,GAAgB,MAChC,IAGK,GADe,EADJ,GAAM,YAAY,EACS,CACrB,eAAe,KAAA,GAAW;EAAE,uBAAuB;EAAG,uBAAuB;EAAG,CAAC,CAAC,YAGrG,EAAe,GAAQ,EAAS,EAInC,KAAoB,GAAc,MAClC,OAAO,KAAU,YACZ,IAAQ,aAAa,iBAE1B,OAAO,KAAU,WACf,MAAU,MAAM,MAAU,WAAiB,cACxC,EAAM,gBAAgB,GAExB,OAAO,EAAM,EAIhB,KAAiB,MAAmD;EACxE,IAAM,oBAAS,IAAI,KAA8B;AAEjD,OAAK,IAAM,KAAW,GAAU;GAC9B,IAAM,IAAW,EAAO,IAAI,EAAQ,KAAK;AACzC,OAAI,GAAU;IAEZ,IAAM,IAA0D,EAAE,GAAG,EAAS,QAAQ;AACtF,QAAI,EAAQ,OACV,MAAK,IAAM,CAAC,GAAK,MAAU,OAAO,QAAQ,EAAQ,OAAO,EAAE;KACzD,IAAM,IAAgB,EAAa;AACnC,KAAI,OAAO,KAAU,YAAY,OAAO,KAAkB,WAEpD,MAAkB,MAAM,MAAU,KACpC,EAAa,KAAO,KAEpB,EAAa,KAAO,IAAgB,IAItC,EAAa,KAAO;;AAI1B,MAAS,SAAS;SAGlB,GAAO,IAAI,EAAQ,MAAM;IACvB,GAAG;IACH,QAAQ,EAAQ,SAAS,EAAE,GAAG,EAAQ,QAAQ,GAAG,KAAA;IAClD,CAAC;;AAIN,SAAO,MAAM,KAAK,EAAO,QAAQ,CAAC;IAI9B,KAAkB,MAClB,CAAC,KAAY,EAAS,WAAW,IAAU,OAK7C,kBAAC,MAAD;EAAI,WAAU;YAHO,EAAc,EAAS,CAI1B,KAAK,MACnB,kBAAC,MAAD;GAAqB,WAAU;aAA/B,CACE,kBAAC,GAAD,EAAO,WAAW,UAAU,EAAa,QAAQ,KAAK,mBAAqB,CAAA,EAC3E,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,QAAD;KAAM,WAAU;eAA6B,EAAQ;KAAY,CAAA,EAChE,EAAQ,UAAU,OAAO,KAAK,EAAQ,OAAO,CAAC,SAAS,KACtD,kBAAC,OAAD;KAAK,WAAU;eACZ,OAAO,QAAQ,EAAQ,OAAO,CAAC,KAAK,CAAC,GAAK,OACzC,kBAAC,QAAD;MAEE,WAAU;gBAFZ;OAIG;OAAI;OAAG,EAAiB,GAAK,EAAM;OAC/B;QAJA,EAIA,CACP;KACE,CAAA,CAEJ;MACH;KAjBI,EAAQ,GAiBZ,CACL;EACC,CAAA;AAIT,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GAEE,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD,EAAS,WAAU,8BAA+B,CAAA,EAClD,kBAAC,MAAD;MAAI,WAAU;gBACX,EAAG,iCAAiC,gBAAgB;MAClD,CAAA,CACD;;IACF,CAAA;GAGN,kBAAC,OAAD;IAAK,WAAU;cACZ,EAAM,WAAW,IAChB,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD,EAAS,WAAU,sCAAuC,CAAA,EAC1D,kBAAC,KAAD;MAAG,WAAU;gBAAmC;MAAwB,CAAA,CACpE;SAEN,EAAM,KAAK,MAAS;KAClB,IAAM,IAAQ,EAAiB,EAAK,KAAK,EACnC,IAAc,EAAK,YAAY,EAAK,SAAS,SAAS;AAE5D,YACE,kBAAC,OAAD;MAAmB,WAAU;gBAC3B,kBAAC,OAAD;OAAK,WAAU;iBAAf,CAEE,kBAAC,OAAD;QAAK,WAAU;kBACZ,EAAY,EAAK,KAAK;QACnB,CAAA,EAGN,kBAAC,OAAD;QAAK,WAAU;kBAAf;SACE,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,QAAD;WAAM,WAAU;qBAAiC,EAAK;WAAY,CAAA,EAClE,kBAAC,QAAD;WAAM,WAAW,iCAAiC,EAAM;qBACrD,EAAM;WACF,CAAA,CACH;;SACL,EAAK,eACJ,kBAAC,KAAD;UAAG,WAAU;oBACV,EAAK;UACJ,CAAA;SAIN,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACG,KAAc,EAAK,SAAS,SAC3B,kBAAC,OAAD;WAAK,WAAU;qBAAf;YACE,kBAAC,UAAD;aACE,MAAK;aACL,eAAe,IAAmB,EAAK,IAAI,EAAK,WAAW,EAAE;aAC7D,UAAU,EAAK,YAAY;aAC3B,WAAU;aACV,cAAY,YAAY,EAAK,KAAK;uBAElC,kBAAC,GAAD,EAAO,WAAU,YAAa,CAAA;aACvB,CAAA;YACT,kBAAC,QAAD;aAAM,WAAU;uBACb,EAAK;aACD,CAAA;YACP,kBAAC,UAAD;aACE,MAAK;aACL,eAAe,IAAmB,EAAK,IAAI,EAAK,WAAW,EAAE;aAC7D,WAAU;aACV,cAAY,YAAY,EAAK,KAAK;uBAElC,kBAAC,GAAD,EAAM,WAAU,YAAa,CAAA;aACtB,CAAA;YACL;eAEN,kBAAC,QAAD;WAAM,WAAU;qBAAhB,CAA8C,SAAM,EAAK,SAAgB;cAG3E,kBAAC,OAAD;WAAK,WAAU;qBAAf,CACE,kBAAC,QAAD;YAAM,WAAU;sBACb,EAAa,EAAK,QAAQ,EAAK,UAAU,EAAK;YAC1C,CAAA,EACN,KAAc,KACb,kBAAC,UAAD;YACE,MAAK;YACL,eAAe,EAAa,EAAK,GAAG;YACpC,WAAU;YACV,cAAY,UAAU,EAAK;sBAE3B,kBAAC,GAAD,EAAQ,WAAU,UAAW,CAAA;YACtB,CAAA,CAEP;aACF;;SAGL,YACQ;UACL,IAAM,IAAc,EAAc,EAAK,SAAU,CAAC;AAClD,iBACE,kBAAC,GAAD;WAAW,MAAK;WAAS,aAAA;WAAY,WAAU;qBAC7C,kBAAC,GAAD;YAAe,OAAM;YAAW,WAAU;sBAA1C,CACE,kBAAC,GAAD;aAAkB,WAAU;uBAC1B,kBAAC,QAAD;cAAM,WAAU;wBAAhB;eACE,kBAAC,GAAD,EAAO,WAAU,YAAa,CAAA;eAC7B;eAAY;eAAE,MAAgB,IAAI,YAAY;eAAY;eAAI;eAE1D;;aACU,CAAA,EACnB,kBAAC,GAAD;aAAkB,WAAU;uBACzB,EAAe,EAAK,SAAU;aACd,CAAA,CACL;;WACN,CAAA;aAEZ;SACF;UACF;;MACF,EA1FI,EAAK,GA0FT;MAER;IAEA,CAAA;GAGL,EAAM,SAAS,KACd,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,OAAD;KAAK,WAAU;eAAf;MAEE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,QAAD;QAAM,WAAU;kBACb,EAAG,6BAA6B,WAAW;QACvC,CAAA,EACP,kBAAC,QAAD;QAAM,WAAU;kBACG,EAAhB,IAA6B,IAA4B,EAAS;QAC9D,CAAA,CACH;;MAGL,IAAiB,KAChB,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,QAAD;QAAM,WAAW,EAAa,QAAQ;kBAAM;QAAe,CAAA,EAC3D,kBAAC,QAAD;QAAM,WAAW,EAAa,QAAQ;kBAAtC,CAA4C,KAAE,EAAa,EAAe,CAAQ;UAC9E;;MAIP,IACC,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,QAAD;QAAM,WAAU;kBAAsB;QAAU,CAAA,EAChD,kBAAC,QAAD;QAAM,WAAU;kBAAoC;QAAmB,CAAA,CACnE;WACJ,IAAY,KAAK,IAAU,IAC7B,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,QAAD;QAAM,WAAU;kBAAhB;SACG,KAAW;SAAM;UAAI,IAAU,KAAK,QAAQ,EAAE;SAAC;SAC3C;WACP,kBAAC,QAAD;QAAM,WAAU;kBAAqB,EAAa,EAAU;QAAQ,CAAA,CAChE;WACJ;MAGJ,kBAAC,OAAD,EAAK,WAAU,sCAAuC,CAAA;MAGrD,IACC,kBAAA,GAAA,EAAA,UAAA,CAEE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,QAAD;QAAM,WAAU;kBACb,EAAG,0BAA0B,QAAQ;QACjC,CAAA,EACP,kBAAC,QAAD;QAAM,WAAU;kBAAhB;SACG,EAAa,EAAY,CAAC,eAAe,KAAA,GAAW;UACnD,uBAAuB;UACvB,uBAAuB;UACxB,CAAC;SAAE;SAAI;SAEH;UACH;UACN,kBAAC,KAAD;OAAG,WAAU;iBAA+C;OAExD,CAAA,CACH,EAAA,CAAA,GAEH,kBAAA,GAAA,EAAA,UAGE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,QAAD;QAAM,WAAU;kBAAhB,CAAkD,aAAU,EAAgB;WAC5E,kBAAC,QAAD;QAAM,WAAU;kBACb,EAAe,GAAO,EAAS;QAC3B,CAAA,CACH;UACL,CAAA;MAED;;IACF,CAAA;GAEJ"}
1
+ {"version":3,"file":"OrderSummary.js","names":[],"sources":["../../../../../src/billing/modules/checkout/components/OrderSummary.tsx"],"sourcesContent":["/**\n * Order Summary Component\n * Displays checkout items, discounts, taxes, and total\n */\n\nimport { type FC, useMemo } from 'react';\nimport {\n Package,\n Blocks,\n Coins,\n Minus,\n Plus,\n Trash2,\n Receipt,\n Check,\n ShoppingCart,\n} from 'lucide-react';\nimport {\n Accordion,\n AccordionItem,\n AccordionTrigger,\n AccordionContent,\n} from '@burdenoff/fe-libs/ui';\nimport type { CheckoutItem, Currency, CheckoutFeature } from '../types';\nimport { formatCurrency } from '../../../shared/utils/format';\nimport { categoryTokens, statusTokens } from '../../../shared/utils/tokens';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\n\n/**\n * Credit conversion rate: How many credits equal 1 USD\n * This should match the backend creditRate.rateUSD (default 0.01 means 100 credits = $1)\n */\nconst CREDITS_PER_USD = 100;\n\ninterface OrderSummaryProps {\n items: CheckoutItem[];\n subtotal: number;\n discountAmount: number;\n taxAmount: number;\n taxRate: number;\n taxType?: string;\n total: number;\n currency: Currency;\n showInCredits?: boolean;\n /** Whether tax is being calculated */\n isCalculatingTax?: boolean;\n onUpdateQuantity?: (itemId: string, quantity: number) => void;\n onRemoveItem?: (itemId: string) => void;\n isEditable?: boolean;\n}\n\nexport const OrderSummary: FC<OrderSummaryProps> = ({\n items,\n subtotal,\n discountAmount,\n taxAmount,\n taxRate,\n taxType,\n total,\n currency,\n showInCredits = false,\n isCalculatingTax = false,\n onUpdateQuantity,\n onRemoveItem,\n isEditable = true,\n}) => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n const getItemIcon = (type: CheckoutItem['type']) => {\n switch (type) {\n case 'PLAN':\n return <Package className=\"size-5\" />;\n case 'ADDON':\n return <Blocks className=\"size-5\" />;\n case 'CREDITS':\n return <Coins className=\"size-5\" />;\n case 'ORDER':\n return <ShoppingCart className=\"size-5\" />;\n default:\n return <Package className=\"size-5\" />;\n }\n };\n\n const getItemTypeBadge = (type: CheckoutItem['type']) => {\n const badges = {\n PLAN: {\n label: 'Plan',\n className: `${categoryTokens.plan.bg} ${categoryTokens.plan.text}`,\n },\n ADDON: {\n label: 'Add-on',\n className: `${categoryTokens.addon.bg} ${categoryTokens.addon.text}`,\n },\n CREDITS: {\n label: 'Credits',\n className: `${categoryTokens.credits.bg} ${categoryTokens.credits.text}`,\n },\n ORDER: {\n label: 'Store Order',\n className: 'bg-status-success-bg-subtle text-status-success-text',\n },\n };\n return badges[type];\n };\n\n // Convert USD amount to credits\n const usdToCredits = (usdAmount: number) => usdAmount * CREDITS_PER_USD;\n\n // Subtotal in USD — used for credit conversion (items may be priced in local currency)\n const subtotalUSD = useMemo(\n () => items.reduce((sum, item) => sum + (item.priceUSD ?? item.price) * item.quantity, 0),\n [items]\n );\n\n // Format amount - show in credits if paying with credits, otherwise in selected currency\n // Items now have their prices stored in the selected currency (from currencyPrices)\n // For credit conversion always use priceUSD (base USD price) — not the localized price.\n const formatAmount = (amount: number, item?: CheckoutItem) => {\n if (showInCredits) {\n const usdAmount = item?.priceUSD ?? amount;\n const creditsAmount = usdToCredits(usdAmount);\n return `${creditsAmount.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 0 })} Credits`;\n }\n // Format in selected currency (items now have localized prices)\n return formatCurrency(amount, currency);\n };\n\n // Format limit value for display\n const formatLimitValue = (_key: string, value: unknown): string => {\n if (typeof value === 'boolean') {\n return value ? 'Included' : 'Not included';\n }\n if (typeof value === 'number') {\n if (value === -1 || value === Infinity) return 'Unlimited';\n return value.toLocaleString();\n }\n return String(value);\n };\n\n // Merge duplicate features by name, combining numeric limits\n const mergeFeatures = (features: CheckoutFeature[]): CheckoutFeature[] => {\n const merged = new Map<string, CheckoutFeature>();\n\n for (const feature of features) {\n const existing = merged.get(feature.name);\n if (existing) {\n // Merge limits - add numeric values together\n const mergedLimits: Record<string, number | string | boolean> = { ...existing.limits };\n if (feature.limits) {\n for (const [key, value] of Object.entries(feature.limits)) {\n const existingValue = mergedLimits[key];\n if (typeof value === 'number' && typeof existingValue === 'number') {\n // Both are numbers - add them (unless unlimited)\n if (existingValue === -1 || value === -1) {\n mergedLimits[key] = -1; // Unlimited stays unlimited\n } else {\n mergedLimits[key] = existingValue + value;\n }\n } else {\n // Non-numeric or first occurrence - just set\n mergedLimits[key] = value;\n }\n }\n }\n existing.limits = mergedLimits;\n } else {\n // Clone the feature to avoid mutating original\n merged.set(feature.name, {\n ...feature,\n limits: feature.limits ? { ...feature.limits } : undefined,\n });\n }\n }\n\n return Array.from(merged.values());\n };\n\n // Render features list\n const renderFeatures = (features: CheckoutFeature[]) => {\n if (!features || features.length === 0) return null;\n\n const mergedFeatures = mergeFeatures(features);\n\n return (\n <ul className=\"space-y-2\">\n {mergedFeatures.map((feature) => (\n <li key={feature.id} className=\"flex items-start gap-2\">\n <Check className={`size-4 ${statusTokens.success.icon} shrink-0 mt-0.5`} />\n <div className=\"flex-1 min-w-0\">\n <span className=\"text-sm text-text-primary\">{feature.name}</span>\n {feature.limits && Object.keys(feature.limits).length > 0 && (\n <div className=\"mt-1 flex flex-wrap gap-1.5\">\n {Object.entries(feature.limits).map(([key, value]) => (\n <span\n key={key}\n className=\"inline-flex items-center text-xs px-1.5 py-0.5 rounded bg-bg-sunken text-text-secondary\"\n >\n {key}: {formatLimitValue(key, value)}\n </span>\n ))}\n </div>\n )}\n </div>\n </li>\n ))}\n </ul>\n );\n };\n\n return (\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface overflow-hidden\">\n {/* Header */}\n <div className=\"px-4 py-3 border-b border-border-subtle bg-bg-sunken\">\n <div className=\"flex items-center gap-2\">\n <Receipt className=\"size-4 text-text-secondary\" />\n <h3 className=\"font-semibold text-text-primary\">\n {tr('billing.checkout.orderSummary', 'Order Summary')}\n </h3>\n </div>\n </div>\n\n {/* Items */}\n <div className=\"divide-y divide-border-subtle\">\n {items.length === 0 ? (\n <div className=\"p-6 text-center\">\n <Package className=\"mx-auto size-8 text-text-secondary\" />\n <p className=\"mt-2 text-sm text-text-secondary\">No items in checkout</p>\n </div>\n ) : (\n items.map((item) => {\n const badge = getItemTypeBadge(item.type);\n const hasFeatures = item.features && item.features.length > 0;\n\n return (\n <div key={item.id} className=\"p-4\">\n <div className=\"flex items-start gap-3\">\n {/* Icon */}\n <div className=\"p-2 rounded-lg bg-bg-sunken text-text-secondary shrink-0\">\n {getItemIcon(item.type)}\n </div>\n\n {/* Item details */}\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2 flex-wrap\">\n <span className=\"font-medium text-text-primary\">{item.name}</span>\n <span className={`text-xs px-1.5 py-0.5 rounded ${badge.className}`}>\n {badge.label}\n </span>\n </div>\n {item.description && (\n <p className=\"text-sm text-text-secondary mt-0.5 line-clamp-2\">\n {item.description}\n </p>\n )}\n\n {/* Quantity controls */}\n <div className=\"flex items-center justify-between mt-3\">\n {isEditable && item.type !== 'PLAN' ? (\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => onUpdateQuantity?.(item.id, item.quantity - 1)}\n disabled={item.quantity <= 1}\n className=\"p-2 rounded border border-border-subtle hover:bg-bg-sunken disabled:opacity-50 disabled:cursor-not-allowed min-w-[36px] min-h-[36px] flex items-center justify-center\"\n aria-label={`Decrease ${item.name} quantity`}\n >\n <Minus className=\"size-3.5\" />\n </button>\n <span className=\"w-8 text-center text-sm font-medium\">\n {item.quantity}\n </span>\n <button\n type=\"button\"\n onClick={() => onUpdateQuantity?.(item.id, item.quantity + 1)}\n className=\"p-2 rounded border border-border-subtle hover:bg-bg-sunken min-w-[36px] min-h-[36px] flex items-center justify-center\"\n aria-label={`Increase ${item.name} quantity`}\n >\n <Plus className=\"size-3.5\" />\n </button>\n </div>\n ) : (\n <span className=\"text-sm text-text-secondary\">Qty: {item.quantity}</span>\n )}\n\n <div className=\"flex items-center gap-3\">\n <span className=\"font-medium text-text-primary\">\n {formatAmount(item.price * item.quantity, item)}\n </span>\n {isEditable && onRemoveItem && (\n <button\n type=\"button\"\n onClick={() => onRemoveItem(item.id)}\n className=\"p-1.5 rounded text-text-secondary hover:text-status-error-text hover:bg-status-error-bg-subtle\"\n aria-label={`Remove ${item.name}`}\n >\n <Trash2 className=\"size-4\" />\n </button>\n )}\n </div>\n </div>\n\n {/* Features - Collapsible */}\n {hasFeatures &&\n (() => {\n const mergedCount = mergeFeatures(item.features!).length;\n return (\n <Accordion type=\"single\" collapsible className=\"mt-3\">\n <AccordionItem value=\"features\" className=\"border-0\">\n <AccordionTrigger className=\"py-2 px-0 text-sm text-text-secondary hover:text-text-primary hover:no-underline\">\n <span className=\"flex items-center gap-1.5\">\n <Check className=\"size-3.5\" />\n {mergedCount} {mergedCount === 1 ? 'feature' : 'features'}{' '}\n included\n </span>\n </AccordionTrigger>\n <AccordionContent className=\"pt-2 pb-0\">\n {renderFeatures(item.features!)}\n </AccordionContent>\n </AccordionItem>\n </Accordion>\n );\n })()}\n </div>\n </div>\n </div>\n );\n })\n )}\n </div>\n\n {/* Totals */}\n {items.length > 0 && (\n <div className=\"border-t border-border-subtle bg-bg-sunken\">\n <div className=\"p-4 space-y-2\">\n {/* Subtotal - already in selected currency (from currencyPrices) */}\n <div className=\"flex items-center justify-between text-sm\">\n <span className=\"text-text-secondary\">\n {tr('billing.checkout.subtotal', 'Subtotal')}\n </span>\n <span className=\"text-text-primary\">\n {showInCredits ? formatAmount(subtotalUSD) : formatAmount(subtotal)}\n </span>\n </div>\n\n {/* Discount */}\n {discountAmount > 0 && (\n <div className=\"flex items-center justify-between text-sm\">\n <span className={statusTokens.success.text}>Discount</span>\n <span className={statusTokens.success.text}>-{formatAmount(discountAmount)}</span>\n </div>\n )}\n\n {/* Tax - calculated on the subtotal in selected currency */}\n {isCalculatingTax ? (\n <div className=\"flex items-center justify-between text-sm\">\n <span className=\"text-text-secondary\">Tax</span>\n <span className=\"text-text-secondary animate-pulse\">Calculating…</span>\n </div>\n ) : taxAmount > 0 && taxRate > 0 ? (\n <div className=\"flex items-center justify-between text-sm\">\n <span className=\"text-text-secondary\">\n {taxType || 'Tax'} ({(taxRate * 100).toFixed(0)}%)\n </span>\n <span className=\"text-text-primary\">{formatAmount(taxAmount)}</span>\n </div>\n ) : null}\n\n {/* Divider */}\n <div className=\"border-t border-border-subtle my-2\" />\n\n {/* Total - show in credits or selected currency */}\n {showInCredits ? (\n <>\n {/* Credit payment: Show total in credits */}\n <div className=\"flex items-center justify-between\">\n <span className=\"font-semibold text-text-primary\">\n {tr('billing.checkout.total', 'Total')}\n </span>\n <span className=\"text-xl font-bold text-text-primary\">\n {usdToCredits(subtotalUSD).toLocaleString(undefined, {\n minimumFractionDigits: 0,\n maximumFractionDigits: 0,\n })}{' '}\n Credits\n </span>\n </div>\n <p className=\"text-xs text-center text-text-secondary pt-2\">\n Paying with account credits\n </p>\n </>\n ) : (\n <>\n {/* Card payment: Show total in selected currency */}\n {/* Items already have localized prices from currencyPrices - no conversion needed */}\n <div className=\"flex items-center justify-between\">\n <span className=\"font-semibold text-text-primary\">Total in {currency}</span>\n <span className=\"text-xl font-bold text-text-primary\">\n {formatCurrency(total, currency)}\n </span>\n </div>\n </>\n )}\n </div>\n </div>\n )}\n </div>\n );\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"PromoCodeInput.js","names":[],"sources":["../../../../../src/billing/modules/checkout/components/PromoCodeInput.tsx"],"sourcesContent":["/**\n * Promo Code Input Component\n * Input field for entering and validating discount codes\n */\n\nimport { type FC, useState, useCallback } from 'react';\nimport { Tag, Check, X, Loader2 } from 'lucide-react';\nimport { statusTokens } from '../../../shared/utils/tokens';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\n\ninterface PromoCodeInputProps {\n value: string;\n onChange: (value: string) => void;\n onValidate: () => Promise<{ valid: boolean; error?: string }>;\n onClear: () => void;\n isApplied: boolean;\n discountValue?: number;\n discountType?: 'PERCENTAGE' | 'FIXED';\n isValidating?: boolean;\n error?: string;\n}\n\nexport const PromoCodeInput: FC<PromoCodeInputProps> = ({\n value,\n onChange,\n onValidate,\n onClear,\n isApplied,\n discountValue,\n discountType: _discountType,\n isValidating = false,\n error,\n}) => {\n void _discountType;\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n const [localError, setLocalError] = useState<string | null>(null);\n\n const handleValidate = useCallback(async () => {\n if (!value.trim()) {\n setLocalError('Please enter a promo code');\n return;\n }\n setLocalError(null);\n const result = await onValidate();\n if (!result.valid && result.error) {\n setLocalError(result.error);\n }\n }, [value, onValidate]);\n\n const handleClear = useCallback(() => {\n setLocalError(null);\n onClear();\n }, [onClear]);\n\n const handleKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (e.key === 'Enter') {\n e.preventDefault();\n handleValidate();\n }\n },\n [handleValidate]\n );\n\n const displayError = error || localError;\n\n if (isApplied && discountValue) {\n // Show applied discount\n return (\n <div\n className={`rounded-lg border ${statusTokens.success.border} ${statusTokens.success.bg} p-3`}\n >\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-2\">\n <div\n className={`p-1.5 rounded-full ${statusTokens.success.bg} ${statusTokens.success.icon}`}\n >\n <Check className=\"size-4\" />\n </div>\n <div>\n <div className=\"flex items-center gap-1.5\">\n <span className={`font-medium ${statusTokens.success.text}`}>\n {value.toUpperCase()}\n </span>\n <span className={`text-sm ${statusTokens.success.text}`}>applied</span>\n </div>\n <p className={`text-sm ${statusTokens.success.text}`}>\n {/* discountValue now contains the actual discount amount, not percentage */}\n <span className=\"flex items-center gap-0.5\">\n {discountValue.toFixed(2)} off your order\n </span>\n </p>\n </div>\n </div>\n <button\n type=\"button\"\n onClick={handleClear}\n className={`p-1.5 rounded-full hover:${statusTokens.success.bg} ${statusTokens.success.icon}`}\n >\n <X className=\"size-4\" />\n </button>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium text-text-primary\">\n {tr('billing.checkout.promoCode', 'Promo Code')}\n </label>\n <div className=\"flex gap-2\">\n <div className=\"relative flex-1\">\n <Tag className=\"absolute left-3 top-1/2 -translate-y-1/2 size-4 text-text-secondary\" />\n <input\n type=\"text\"\n value={value}\n onChange={(e) => {\n onChange(e.target.value.toUpperCase());\n setLocalError(null);\n }}\n onKeyDown={handleKeyDown}\n placeholder=\"Enter code\"\n disabled={isValidating}\n className={`\n w-full pl-[3.25rem] pr-4 py-2.5 rounded-lg border bg-bg-surface text-text-primary\n placeholder:text-text-placeholder\n focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] focus:border-border-strong\n disabled:opacity-50 disabled:cursor-not-allowed\n ${\n displayError\n ? 'border-status-error-border focus:ring-status-error-border focus:border-status-error-border'\n : 'border-border-subtle'\n }\n `}\n />\n </div>\n <button\n type=\"button\"\n onClick={handleValidate}\n disabled={!value.trim() || isValidating}\n className=\"px-4 py-2.5 rounded-lg border border-border-subtle bg-bg-sunken text-text-primary font-medium\n hover:bg-bg-sunken disabled:opacity-50 disabled:cursor-not-allowed\n transition-colors duration-200\"\n >\n {isValidating ? <Loader2 className=\"size-4 animate-spin\" /> : 'Apply'}\n </button>\n </div>\n\n {/* Error message */}\n {displayError && (\n <p className=\"text-sm text-status-error-text flex items-center gap-1.5\">\n <X className=\"size-3.5\" />\n {displayError}\n </p>\n )}\n </div>\n );\n};\n"],"mappings":";;;;;;AAsBA,IAAa,KAA2C,EACtD,UACA,aACA,eACA,YACA,cACA,kBACA,cAAc,GACd,kBAAe,IACf,eACI;CAEJ,IAAM,EAAE,SAAM,GAAS,EACjB,KAAM,GAAa,MAA6B;EACpD,IAAM,IAAa,EAAE,EAAI;AACzB,SAAO,MAAe,IAAM,IAAW;IAEnC,CAAC,GAAY,KAAiB,EAAwB,KAAK,EAE3D,IAAiB,EAAY,YAAY;AAC7C,MAAI,CAAC,EAAM,MAAM,EAAE;AACjB,KAAc,4BAA4B;AAC1C;;AAEF,IAAc,KAAK;EACnB,IAAM,IAAS,MAAM,GAAY;AACjC,EAAI,CAAC,EAAO,SAAS,EAAO,SAC1B,EAAc,EAAO,MAAM;IAE5B,CAAC,GAAO,EAAW,CAAC,EAEjB,IAAc,QAAkB;AAEpC,EADA,EAAc,KAAK,EACnB,GAAS;IACR,CAAC,EAAQ,CAAC,EAEP,IAAgB,GACnB,MAA2B;AAC1B,EAAI,EAAE,QAAQ,YACZ,EAAE,gBAAgB,EAClB,GAAgB;IAGpB,CAAC,EAAe,CACjB,EAEK,IAAe,KAAS;AA0C9B,QAxCI,KAAa,IAGb,kBAAC,OAAD;EACE,WAAW,qBAAqB,EAAa,QAAQ,OAAO,GAAG,EAAa,QAAQ,GAAG;YAEvF,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KACE,WAAW,sBAAsB,EAAa,QAAQ,GAAG,GAAG,EAAa,QAAQ;eAEjF,kBAAC,GAAD,EAAO,WAAU,UAAW,CAAA;KACxB,CAAA,EACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,QAAD;MAAM,WAAW,eAAe,EAAa,QAAQ;gBAClD,EAAM,aAAa;MACf,CAAA,EACP,kBAAC,QAAD;MAAM,WAAW,WAAW,EAAa,QAAQ;gBAAQ;MAAc,CAAA,CACnE;QACN,kBAAC,KAAD;KAAG,WAAW,WAAW,EAAa,QAAQ;eAE5C,kBAAC,QAAD;MAAM,WAAU;gBAAhB,CACG,EAAc,QAAQ,EAAE,EAAC,kBACrB;;KACL,CAAA,CACA,EAAA,CAAA,CACF;OACN,kBAAC,UAAD;IACE,MAAK;IACL,SAAS;IACT,WAAW,4BAA4B,EAAa,QAAQ,GAAG,GAAG,EAAa,QAAQ;cAEvF,kBAAC,GAAD,EAAG,WAAU,UAAW,CAAA;IACjB,CAAA,CACL;;EACF,CAAA,GAKR,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,SAAD;IAAO,WAAU;cACd,EAAG,8BAA8B,aAAa;IACzC,CAAA;GACR,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD,EAAK,WAAU,uEAAwE,CAAA,EACvF,kBAAC,SAAD;MACE,MAAK;MACE;MACP,WAAW,MAAM;AAEf,OADA,EAAS,EAAE,OAAO,MAAM,aAAa,CAAC,EACtC,EAAc,KAAK;;MAErB,WAAW;MACX,aAAY;MACZ,UAAU;MACV,WAAW;;;;;gBAMP,IACI,+FACA,uBACL;;MAEH,CAAA,CACE;QACN,kBAAC,UAAD;KACE,MAAK;KACL,SAAS;KACT,UAAU,CAAC,EAAM,MAAM,IAAI;KAC3B,WAAU;eAIT,IAAe,kBAAC,GAAD,EAAS,WAAU,uBAAwB,CAAA,GAAG;KACvD,CAAA,CACL;;GAGL,KACC,kBAAC,KAAD;IAAG,WAAU;cAAb,CACE,kBAAC,GAAD,EAAG,WAAU,YAAa,CAAA,EACzB,EACC;;GAEF"}
1
+ {"version":3,"file":"PromoCodeInput.js","names":[],"sources":["../../../../../src/billing/modules/checkout/components/PromoCodeInput.tsx"],"sourcesContent":["/**\n * Promo Code Input Component\n * Input field for entering and validating discount codes\n */\n\nimport { type FC, useState, useCallback } from 'react';\nimport { Tag, Check, X, Loader2 } from 'lucide-react';\nimport { statusTokens } from '../../../shared/utils/tokens';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\n\ninterface PromoCodeInputProps {\n value: string;\n onChange: (value: string) => void;\n onValidate: () => Promise<{ valid: boolean; error?: string }>;\n onClear: () => void;\n isApplied: boolean;\n discountValue?: number;\n discountType?: 'PERCENTAGE' | 'FIXED';\n isValidating?: boolean;\n error?: string;\n}\n\nexport const PromoCodeInput: FC<PromoCodeInputProps> = ({\n value,\n onChange,\n onValidate,\n onClear,\n isApplied,\n discountValue,\n discountType: _discountType,\n isValidating = false,\n error,\n}) => {\n void _discountType;\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n const [localError, setLocalError] = useState<string | null>(null);\n\n const handleValidate = useCallback(async () => {\n if (!value.trim()) {\n setLocalError('Please enter a promo code');\n return;\n }\n setLocalError(null);\n const result = await onValidate();\n if (!result.valid && result.error) {\n setLocalError(result.error);\n }\n }, [value, onValidate]);\n\n const handleClear = useCallback(() => {\n setLocalError(null);\n onClear();\n }, [onClear]);\n\n const handleKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (e.key === 'Enter') {\n e.preventDefault();\n handleValidate();\n }\n },\n [handleValidate]\n );\n\n const displayError = error || localError;\n\n if (isApplied && discountValue) {\n // Show applied discount\n return (\n <div\n className={`rounded-lg border ${statusTokens.success.border} ${statusTokens.success.bg} p-3`}\n >\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-2\">\n <div\n className={`p-1.5 rounded-full ${statusTokens.success.bg} ${statusTokens.success.icon}`}\n >\n <Check className=\"size-4\" />\n </div>\n <div>\n <div className=\"flex items-center gap-1.5\">\n <span className={`font-medium ${statusTokens.success.text}`}>\n {value.toUpperCase()}\n </span>\n <span className={`text-sm ${statusTokens.success.text}`}>applied</span>\n </div>\n <p className={`text-sm ${statusTokens.success.text}`}>\n {/* discountValue now contains the actual discount amount, not percentage */}\n <span className=\"flex items-center gap-0.5\">\n {discountValue.toFixed(2)} off your order\n </span>\n </p>\n </div>\n </div>\n <button\n type=\"button\"\n onClick={handleClear}\n className={`p-1.5 rounded-full hover:${statusTokens.success.bg} ${statusTokens.success.icon}`}\n >\n <X className=\"size-4\" />\n </button>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium text-text-primary\">\n {tr('billing.checkout.promoCode', 'Promo Code')}\n </label>\n <div className=\"flex gap-2\">\n <div className=\"relative flex-1\">\n <Tag className=\"absolute left-3 top-1/2 -translate-y-1/2 size-4 text-text-secondary\" />\n <input\n type=\"text\"\n value={value}\n onChange={(e) => {\n onChange(e.target.value.toUpperCase());\n setLocalError(null);\n }}\n onKeyDown={handleKeyDown}\n placeholder=\"Enter code\"\n disabled={isValidating}\n className={`\n w-full pl-[3.25rem] pr-4 py-2.5 rounded-lg border bg-bg-surface text-text-primary\n placeholder:text-text-placeholder\n focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] focus:border-border-strong\n disabled:opacity-50 disabled:cursor-not-allowed\n ${\n displayError\n ? 'border-status-error-border focus:ring-status-error-border focus:border-status-error-border'\n : 'border-border-subtle'\n }\n `}\n />\n </div>\n <button\n type=\"button\"\n onClick={handleValidate}\n disabled={!value.trim() || isValidating}\n className=\"px-4 py-2.5 rounded-lg border border-border-subtle bg-bg-sunken text-text-primary font-medium\n hover:bg-bg-sunken disabled:opacity-50 disabled:cursor-not-allowed\n transition-colors duration-200\"\n >\n {isValidating ? <Loader2 className=\"size-4 animate-spin\" /> : 'Apply'}\n </button>\n </div>\n\n {/* Error message */}\n {displayError && (\n <p className=\"text-sm text-status-error-text flex items-center gap-1.5\">\n <X className=\"size-3.5\" />\n {displayError}\n </p>\n )}\n </div>\n );\n};\n"],"mappings":";;;;;;AAsBA,IAAa,KAA2C,EACtD,UACA,aACA,eACA,YACA,cACA,kBACA,cAAc,GACd,kBAAe,IACf,eACI;CAEJ,IAAM,EAAE,SAAM,EAAQ,GAChB,KAAM,GAAa,MAA6B;EACpD,IAAM,IAAa,EAAE,CAAG;EACxB,OAAO,MAAe,IAAM,IAAW;CACzC,GACM,CAAC,GAAY,KAAiB,EAAwB,IAAI,GAE1D,IAAiB,EAAY,YAAY;EAC7C,IAAI,CAAC,EAAM,KAAK,GAAG;GACjB,EAAc,2BAA2B;GACzC;EACF;EACA,EAAc,IAAI;EAClB,IAAM,IAAS,MAAM,EAAW;EAChC,AAAI,CAAC,EAAO,SAAS,EAAO,SAC1B,EAAc,EAAO,KAAK;CAE9B,GAAG,CAAC,GAAO,CAAU,CAAC,GAEhB,IAAc,QAAkB;EAEpC,AADA,EAAc,IAAI,GAClB,EAAQ;CACV,GAAG,CAAC,CAAO,CAAC,GAEN,IAAgB,GACnB,MAA2B;EAC1B,AAAI,EAAE,QAAQ,YACZ,EAAE,eAAe,GACjB,EAAe;CAEnB,GACA,CAAC,CAAc,CACjB,GAEM,IAAe,KAAS;CA0C9B,OAxCI,KAAa,IAGb,kBAAC,OAAD;EACE,WAAW,qBAAqB,EAAa,QAAQ,OAAO,GAAG,EAAa,QAAQ,GAAG;YAEvF,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KACE,WAAW,sBAAsB,EAAa,QAAQ,GAAG,GAAG,EAAa,QAAQ;eAEjF,kBAAC,GAAD,EAAO,WAAU,SAAU,CAAA;IACxB,CAAA,GACL,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,QAAD;MAAM,WAAW,eAAe,EAAa,QAAQ;gBAClD,EAAM,YAAY;KACf,CAAA,GACN,kBAAC,QAAD;MAAM,WAAW,WAAW,EAAa,QAAQ;gBAAQ;KAAa,CAAA,CACnE;QACL,kBAAC,KAAD;KAAG,WAAW,WAAW,EAAa,QAAQ;eAE5C,kBAAC,QAAD;MAAM,WAAU;gBAAhB,CACG,EAAc,QAAQ,CAAC,GAAE,iBACtB;;IACL,CAAA,CACA,EAAA,CAAA,CACF;OACL,kBAAC,UAAD;IACE,MAAK;IACL,SAAS;IACT,WAAW,4BAA4B,EAAa,QAAQ,GAAG,GAAG,EAAa,QAAQ;cAEvF,kBAAC,GAAD,EAAG,WAAU,SAAU,CAAA;GACjB,CAAA,CACL;;CACF,CAAA,IAKP,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,SAAD;IAAO,WAAU;cACd,EAAG,8BAA8B,YAAY;GACzC,CAAA;GACP,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD,EAAK,WAAU,sEAAuE,CAAA,GACtF,kBAAC,SAAD;MACE,MAAK;MACE;MACP,WAAW,MAAM;OAEf,AADA,EAAS,EAAE,OAAO,MAAM,YAAY,CAAC,GACrC,EAAc,IAAI;MACpB;MACA,WAAW;MACX,aAAY;MACZ,UAAU;MACV,WAAW;;;;;gBAMP,IACI,+FACA,uBACL;;KAEJ,CAAA,CACE;QACL,kBAAC,UAAD;KACE,MAAK;KACL,SAAS;KACT,UAAU,CAAC,EAAM,KAAK,KAAK;KAC3B,WAAU;eAIT,IAAe,kBAAC,GAAD,EAAS,WAAU,sBAAuB,CAAA,IAAI;IACxD,CAAA,CACL;;GAGJ,KACC,kBAAC,KAAD;IAAG,WAAU;cAAb,CACE,kBAAC,GAAD,EAAG,WAAU,WAAY,CAAA,GACxB,CACA;;EAEF;;AAET"}
@@ -1 +1 @@
1
- {"version":3,"file":"RazorpayPaymentButton.js","names":[],"sources":["../../../../../src/billing/modules/checkout/components/RazorpayPaymentButton.tsx"],"sourcesContent":["/**\n * {tr('billing.checkout.razorpayPayment', 'Razorpay Payment')} Button Component\n * Integrates with Razorpay checkout for INR payments\n */\n\nimport { type FC, useState, useCallback, useEffect } from 'react';\nimport { Lock, Loader2, Check, AlertCircle } from 'lucide-react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport type {\n RazorpaySuccessResponse,\n CheckoutRazorpayOptions,\n CheckoutRazorpayInstance,\n} from '../types';\nimport { getPaymentProviderThemeColor } from '../../../shared/utils';\n\ntype RazorpayCheckoutOptions = CheckoutRazorpayOptions & {\n customer_id?: string;\n remember_customer?: boolean;\n handler: (response: RazorpaySuccessResponse) => void;\n};\n\ninterface RazorpayPaymentButtonProps {\n orderId: string;\n keyId: string;\n amount: number; // In paise (smallest currency unit)\n currency: string;\n name?: string;\n description?: string;\n customerId?: string; // Razorpay customer ID for saved cards\n prefill?: {\n name?: string;\n email?: string;\n contact?: string;\n };\n notes?: Record<string, string>;\n onSuccess: (response: RazorpaySuccessResponse) => void;\n onError: (error: string) => void;\n onCancel?: () => void;\n disabled?: boolean;\n}\n\ntype PaymentState = 'idle' | 'loading' | 'processing' | 'success' | 'error';\n\nexport const RazorpayPaymentButton: FC<RazorpayPaymentButtonProps> = ({\n orderId,\n keyId,\n amount,\n currency,\n name = 'Burdenoff',\n description = 'Purchase',\n customerId,\n prefill,\n notes,\n onSuccess,\n onError,\n onCancel,\n disabled = false,\n}) => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n const [paymentState, setPaymentState] = useState<PaymentState>('idle');\n const [error, setError] = useState<string | null>(null);\n const [isRazorpayLoaded, setIsRazorpayLoaded] = useState(false);\n\n // Load Razorpay SDK dynamically\n useEffect(() => {\n if (typeof window !== 'undefined' && !window.Razorpay) {\n const script = document.createElement('script');\n script.src = 'https://checkout.razorpay.com/v1/checkout.js';\n script.async = true;\n script.onload = () => {\n setIsRazorpayLoaded(true);\n };\n script.onerror = () => {\n setError('Failed to load payment gateway');\n };\n document.body.appendChild(script);\n } else if (window.Razorpay) {\n setIsRazorpayLoaded(true);\n }\n }, []);\n\n const handlePayment = useCallback(() => {\n if (!isRazorpayLoaded || !window.Razorpay) {\n setError('Payment gateway not ready');\n return;\n }\n\n setPaymentState('processing');\n setError(null);\n\n // Build Razorpay options - customer_id enables saved cards feature\n const options: RazorpayCheckoutOptions = {\n key: keyId,\n amount: amount, // Amount in paise\n currency: currency.toUpperCase(),\n name,\n description,\n order_id: orderId,\n prefill: prefill || {},\n notes: notes || {},\n handler: (response: RazorpaySuccessResponse) => {\n setPaymentState('success');\n onSuccess(response);\n },\n modal: {\n ondismiss: () => {\n setPaymentState('idle');\n onCancel?.();\n },\n escape: true,\n confirm_close: true,\n },\n };\n\n const providerThemeColor = getPaymentProviderThemeColor();\n if (providerThemeColor) {\n options.theme = { color: providerThemeColor };\n }\n\n // Add customer_id for saved cards feature\n if (customerId) {\n options.customer_id = customerId;\n options.remember_customer = true;\n }\n\n try {\n const RazorpayConstructor = (\n window as unknown as {\n Razorpay?: new (options: RazorpayCheckoutOptions) => CheckoutRazorpayInstance;\n }\n ).Razorpay;\n\n if (!RazorpayConstructor) {\n throw new Error('Payment gateway not ready');\n }\n\n const razorpay = new RazorpayConstructor(options);\n\n razorpay.on('payment.failed', (response: unknown) => {\n const errorResponse = response as {\n error?: {\n code?: string;\n description?: string;\n reason?: string;\n };\n };\n const errorMessage =\n errorResponse?.error?.description || errorResponse?.error?.reason || 'Payment failed';\n setError(errorMessage);\n setPaymentState('error');\n onError(errorMessage);\n });\n\n razorpay.open();\n } catch (err) {\n const errorMessage = err instanceof Error ? err.message : 'Failed to open payment gateway';\n setError(errorMessage);\n setPaymentState('error');\n onError(errorMessage);\n }\n }, [\n isRazorpayLoaded,\n keyId,\n amount,\n currency,\n name,\n description,\n customerId,\n orderId,\n prefill,\n notes,\n onSuccess,\n onError,\n onCancel,\n ]);\n\n // Format amount for display (convert from paise to rupees)\n const displayAmount = (amount / 100).toLocaleString('en-IN', {\n style: 'currency',\n currency: 'INR',\n });\n\n // Render success state\n if (paymentState === 'success') {\n return (\n <div className=\"p-6 rounded-lg border border-border-subtle bg-status-success-bg-subtle\">\n <div className=\"flex items-center gap-3\">\n <div className=\"p-2 rounded-full bg-status-success-bg-subtle text-status-success-text\">\n <Check className=\"size-6\" />\n </div>\n <div>\n <h3 className=\"font-semibold text-status-success-text\">Payment Successful!</h3>\n <p className=\"text-sm text-status-success-text\">\n Your payment has been processed successfully.\n </p>\n </div>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-4\">\n {/* Security badge */}\n <div className=\"flex items-center justify-center gap-1.5 text-xs text-text-secondary\">\n <Lock className=\"size-3\" />\n Secured by Razorpay\n </div>\n\n {/* Error message */}\n {error && (\n <div className=\"flex items-center gap-2 p-3 rounded-lg bg-status-error-bg-subtle text-status-error-text text-sm\">\n <AlertCircle className=\"size-4 shrink-0\" />\n {error}\n </div>\n )}\n\n {/* Loading state */}\n {!isRazorpayLoaded && (\n <div className=\"flex items-center justify-center gap-2 py-4 text-text-secondary\">\n <Loader2 className=\"size-4 animate-spin\" />\n <span className=\"text-sm\">\n {tr('billing.checkout.loadingGateway', 'Loading payment gateway...')}\n </span>\n </div>\n )}\n\n {/* Actions */}\n <div className=\"flex gap-3\">\n {onCancel && (\n <button\n type=\"button\"\n onClick={onCancel}\n disabled={paymentState === 'processing' || disabled}\n className=\"flex-1 px-4 py-3 rounded-lg border border-border-subtle text-text-primary font-medium\n hover:bg-bg-sunken disabled:opacity-50 disabled:cursor-not-allowed transition-colors\"\n >\n Cancel\n </button>\n )}\n <button\n type=\"button\"\n onClick={handlePayment}\n disabled={!isRazorpayLoaded || paymentState === 'processing' || disabled}\n className=\"flex-1 px-4 py-3 rounded-lg bg-action-primary-bg text-action-primary-text font-medium\n hover:bg-action-primary-bg/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors\n flex items-center justify-center gap-2\"\n >\n {paymentState === 'processing' ? (\n <>\n <Loader2 className=\"size-4 animate-spin\" />\n Processing…\n </>\n ) : (\n <>Pay {displayAmount}</>\n )}\n </button>\n </div>\n\n {/* Security note */}\n <p className=\"text-center text-xs text-text-secondary\">\n Your payment is secured with 256-bit encryption. Powered by Razorpay.\n </p>\n </div>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"RazorpayPaymentButton.js","names":[],"sources":["../../../../../src/billing/modules/checkout/components/RazorpayPaymentButton.tsx"],"sourcesContent":["/**\n * {tr('billing.checkout.razorpayPayment', 'Razorpay Payment')} Button Component\n * Integrates with Razorpay checkout for INR payments\n */\n\nimport { type FC, useState, useCallback, useEffect } from 'react';\nimport { Lock, Loader2, Check, AlertCircle } from 'lucide-react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport type {\n RazorpaySuccessResponse,\n CheckoutRazorpayOptions,\n CheckoutRazorpayInstance,\n} from '../types';\nimport { getPaymentProviderThemeColor } from '../../../shared/utils';\n\ntype RazorpayCheckoutOptions = CheckoutRazorpayOptions & {\n customer_id?: string;\n remember_customer?: boolean;\n handler: (response: RazorpaySuccessResponse) => void;\n};\n\ninterface RazorpayPaymentButtonProps {\n orderId: string;\n keyId: string;\n amount: number; // In paise (smallest currency unit)\n currency: string;\n name?: string;\n description?: string;\n customerId?: string; // Razorpay customer ID for saved cards\n prefill?: {\n name?: string;\n email?: string;\n contact?: string;\n };\n notes?: Record<string, string>;\n onSuccess: (response: RazorpaySuccessResponse) => void;\n onError: (error: string) => void;\n onCancel?: () => void;\n disabled?: boolean;\n}\n\ntype PaymentState = 'idle' | 'loading' | 'processing' | 'success' | 'error';\n\nexport const RazorpayPaymentButton: FC<RazorpayPaymentButtonProps> = ({\n orderId,\n keyId,\n amount,\n currency,\n name = 'Burdenoff',\n description = 'Purchase',\n customerId,\n prefill,\n notes,\n onSuccess,\n onError,\n onCancel,\n disabled = false,\n}) => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n const [paymentState, setPaymentState] = useState<PaymentState>('idle');\n const [error, setError] = useState<string | null>(null);\n const [isRazorpayLoaded, setIsRazorpayLoaded] = useState(false);\n\n // Load Razorpay SDK dynamically\n useEffect(() => {\n if (typeof window !== 'undefined' && !window.Razorpay) {\n const script = document.createElement('script');\n script.src = 'https://checkout.razorpay.com/v1/checkout.js';\n script.async = true;\n script.onload = () => {\n setIsRazorpayLoaded(true);\n };\n script.onerror = () => {\n setError('Failed to load payment gateway');\n };\n document.body.appendChild(script);\n } else if (window.Razorpay) {\n setIsRazorpayLoaded(true);\n }\n }, []);\n\n const handlePayment = useCallback(() => {\n if (!isRazorpayLoaded || !window.Razorpay) {\n setError('Payment gateway not ready');\n return;\n }\n\n setPaymentState('processing');\n setError(null);\n\n // Build Razorpay options - customer_id enables saved cards feature\n const options: RazorpayCheckoutOptions = {\n key: keyId,\n amount: amount, // Amount in paise\n currency: currency.toUpperCase(),\n name,\n description,\n order_id: orderId,\n prefill: prefill || {},\n notes: notes || {},\n handler: (response: RazorpaySuccessResponse) => {\n setPaymentState('success');\n onSuccess(response);\n },\n modal: {\n ondismiss: () => {\n setPaymentState('idle');\n onCancel?.();\n },\n escape: true,\n confirm_close: true,\n },\n };\n\n const providerThemeColor = getPaymentProviderThemeColor();\n if (providerThemeColor) {\n options.theme = { color: providerThemeColor };\n }\n\n // Add customer_id for saved cards feature\n if (customerId) {\n options.customer_id = customerId;\n options.remember_customer = true;\n }\n\n try {\n const RazorpayConstructor = (\n window as unknown as {\n Razorpay?: new (options: RazorpayCheckoutOptions) => CheckoutRazorpayInstance;\n }\n ).Razorpay;\n\n if (!RazorpayConstructor) {\n throw new Error('Payment gateway not ready');\n }\n\n const razorpay = new RazorpayConstructor(options);\n\n razorpay.on('payment.failed', (response: unknown) => {\n const errorResponse = response as {\n error?: {\n code?: string;\n description?: string;\n reason?: string;\n };\n };\n const errorMessage =\n errorResponse?.error?.description || errorResponse?.error?.reason || 'Payment failed';\n setError(errorMessage);\n setPaymentState('error');\n onError(errorMessage);\n });\n\n razorpay.open();\n } catch (err) {\n const errorMessage = err instanceof Error ? err.message : 'Failed to open payment gateway';\n setError(errorMessage);\n setPaymentState('error');\n onError(errorMessage);\n }\n }, [\n isRazorpayLoaded,\n keyId,\n amount,\n currency,\n name,\n description,\n customerId,\n orderId,\n prefill,\n notes,\n onSuccess,\n onError,\n onCancel,\n ]);\n\n // Format amount for display (convert from paise to rupees)\n const displayAmount = (amount / 100).toLocaleString('en-IN', {\n style: 'currency',\n currency: 'INR',\n });\n\n // Render success state\n if (paymentState === 'success') {\n return (\n <div className=\"p-6 rounded-lg border border-border-subtle bg-status-success-bg-subtle\">\n <div className=\"flex items-center gap-3\">\n <div className=\"p-2 rounded-full bg-status-success-bg-subtle text-status-success-text\">\n <Check className=\"size-6\" />\n </div>\n <div>\n <h3 className=\"font-semibold text-status-success-text\">Payment Successful!</h3>\n <p className=\"text-sm text-status-success-text\">\n Your payment has been processed successfully.\n </p>\n </div>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-4\">\n {/* Security badge */}\n <div className=\"flex items-center justify-center gap-1.5 text-xs text-text-secondary\">\n <Lock className=\"size-3\" />\n Secured by Razorpay\n </div>\n\n {/* Error message */}\n {error && (\n <div className=\"flex items-center gap-2 p-3 rounded-lg bg-status-error-bg-subtle text-status-error-text text-sm\">\n <AlertCircle className=\"size-4 shrink-0\" />\n {error}\n </div>\n )}\n\n {/* Loading state */}\n {!isRazorpayLoaded && (\n <div className=\"flex items-center justify-center gap-2 py-4 text-text-secondary\">\n <Loader2 className=\"size-4 animate-spin\" />\n <span className=\"text-sm\">\n {tr('billing.checkout.loadingGateway', 'Loading payment gateway...')}\n </span>\n </div>\n )}\n\n {/* Actions */}\n <div className=\"flex gap-3\">\n {onCancel && (\n <button\n type=\"button\"\n onClick={onCancel}\n disabled={paymentState === 'processing' || disabled}\n className=\"flex-1 px-4 py-3 rounded-lg border border-border-subtle text-text-primary font-medium\n hover:bg-bg-sunken disabled:opacity-50 disabled:cursor-not-allowed transition-colors\"\n >\n Cancel\n </button>\n )}\n <button\n type=\"button\"\n onClick={handlePayment}\n disabled={!isRazorpayLoaded || paymentState === 'processing' || disabled}\n className=\"flex-1 px-4 py-3 rounded-lg bg-action-primary-bg text-action-primary-text font-medium\n hover:bg-action-primary-bg/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors\n flex items-center justify-center gap-2\"\n >\n {paymentState === 'processing' ? (\n <>\n <Loader2 className=\"size-4 animate-spin\" />\n Processing…\n </>\n ) : (\n <>Pay {displayAmount}</>\n )}\n </button>\n </div>\n\n {/* Security note */}\n <p className=\"text-center text-xs text-text-secondary\">\n Your payment is secured with 256-bit encryption. Powered by Razorpay.\n </p>\n </div>\n );\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"SimpleAutoPayToggle.js","names":[],"sources":["../../../../../src/billing/modules/checkout/components/SimpleAutoPayToggle.tsx"],"sourcesContent":["/**\n * Simple Auto-Pay Toggle Component\n * Just enables/disables auto-renewal using the selected payment method\n */\n\nimport { type FC, useState } from 'react';\nimport { RefreshCw, Info } from 'lucide-react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\n\ninterface SimpleAutoPayToggleProps {\n enabled: boolean;\n onToggle: (enabled: boolean) => void;\n hasPaymentMethodSelected: boolean;\n selectedPaymentMethodDisplay?: string;\n isNewCard?: boolean; // Whether the selected payment method is a new card (not saved)\n disabled?: boolean;\n}\n\nexport const SimpleAutoPayToggle: FC<SimpleAutoPayToggleProps> = ({\n enabled,\n onToggle,\n hasPaymentMethodSelected,\n selectedPaymentMethodDisplay,\n isNewCard = false,\n disabled = false,\n}) => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n const [showInfo, setShowInfo] = useState(false);\n const canEnable = hasPaymentMethodSelected;\n\n return (\n <div className=\"pt-3 border-t border-border-subtle space-y-2\">\n {/* Toggle Row */}\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => {\n if (!canEnable) return;\n onToggle(!enabled);\n }}\n disabled={disabled || !canEnable}\n className={`\n relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent\n transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] focus:ring-offset-2\n ${enabled ? 'bg-action-primary-bg' : 'bg-bg-sunken'}\n ${(!canEnable || disabled) && 'opacity-50 cursor-not-allowed'}\n `}\n >\n <span\n className={`\n pointer-events-none inline-block size-4 transform rounded-full bg-bg-surface shadow ring-0\n transition duration-200 ease-in-out\n ${enabled ? 'translate-x-4' : 'translate-x-0'}\n `}\n />\n </button>\n\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-1.5\">\n <RefreshCw className=\"size-3.5 text-text-secondary shrink-0\" />\n <span className=\"text-xs font-medium text-text-primary\">\n {tr('billing.checkout.autoRenew', 'Auto-renew subscription')}\n </span>\n <button\n type=\"button\"\n onClick={() => setShowInfo(!showInfo)}\n className=\"p-0.5 hover:bg-bg-sunken rounded transition-colors\"\n >\n <Info className=\"size-3.5 text-text-secondary\" />\n </button>\n </div>\n\n {/* Status Message */}\n {enabled && selectedPaymentMethodDisplay && isNewCard ? (\n <div className=\"text-xs text-accent-blue mt-0.5\">\n ℹ️ This payment method will be saved for future renewals\n </div>\n ) : enabled && selectedPaymentMethodDisplay ? (\n <div className=\"text-xs text-success mt-0.5\">\n ✓ Will renew using {selectedPaymentMethodDisplay}\n </div>\n ) : !canEnable ? (\n <div className=\"text-xs text-warning mt-0.5\">\n {tr('billing.checkout.selectPaymentFirst', 'Select a payment method first')}\n </div>\n ) : null}\n </div>\n </div>\n\n {/* Info Panel */}\n {showInfo && (\n <div className=\"p-3 rounded-lg bg-bg-sunken border border-border-subtle\">\n <p className=\"text-xs text-text-secondary\">\n {canEnable\n ? 'Your subscription will automatically renew when it expires using the payment method selected on the left.'\n : 'Enable auto-renewal to automatically charge your payment method when the subscription expires. Select a payment method first.'}\n </p>\n\n {!canEnable && (\n <div className=\"flex items-start gap-2 p-2 rounded bg-warning/10 mt-2\">\n <Info className=\"size-3.5 text-warning shrink-0 mt-0.5\" />\n <p className=\"text-xs text-warning\">\n You can save a card during this purchase, then enable auto-renew for future\n subscriptions.\n </p>\n </div>\n )}\n </div>\n )}\n </div>\n );\n};\n"],"mappings":";;;;;AAkBA,IAAa,KAAqD,EAChE,YACA,aACA,6BACA,iCACA,eAAY,IACZ,cAAW,SACP;CACJ,IAAM,EAAE,SAAM,GAAS,EACjB,KAAM,GAAa,MAA6B;EACpD,IAAM,IAAa,EAAE,EAAI;AACzB,SAAO,MAAe,IAAM,IAAW;IAEnC,CAAC,GAAU,KAAe,EAAS,GAAM,EACzC,IAAY;AAElB,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CAEE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,UAAD;IACE,MAAK;IACL,eAAe;AACR,UACL,EAAS,CAAC,EAAQ;;IAEpB,UAAU,KAAY,CAAC;IACvB,WAAW;;;cAGP,IAAU,yBAAyB,eAAe;eACjD,CAAC,KAAa,MAAa,gCAAgC;;cAGhE,kBAAC,QAAD,EACE,WAAW;;;gBAGP,IAAU,kBAAkB,gBAAgB;eAEhD,CAAA;IACK,CAAA,EAET,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,GAAD,EAAW,WAAU,yCAA0C,CAAA;MAC/D,kBAAC,QAAD;OAAM,WAAU;iBACb,EAAG,8BAA8B,0BAA0B;OACvD,CAAA;MACP,kBAAC,UAAD;OACE,MAAK;OACL,eAAe,EAAY,CAAC,EAAS;OACrC,WAAU;iBAEV,kBAAC,GAAD,EAAM,WAAU,gCAAiC,CAAA;OAC1C,CAAA;MACL;QAGL,KAAW,KAAgC,IAC1C,kBAAC,OAAD;KAAK,WAAU;eAAkC;KAE3C,CAAA,GACJ,KAAW,IACb,kBAAC,OAAD;KAAK,WAAU;eAAf,CAA6C,uBACvB,EAChB;SACH,IAID,OAHF,kBAAC,OAAD;KAAK,WAAU;eACZ,EAAG,uCAAuC,gCAAgC;KACvE,CAAA,CAEJ;MACF;MAGL,KACC,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,KAAD;IAAG,WAAU;cACV,IACG,8GACA;IACF,CAAA,EAEH,CAAC,KACA,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD,EAAM,WAAU,yCAA0C,CAAA,EAC1D,kBAAC,KAAD;KAAG,WAAU;eAAuB;KAGhC,CAAA,CACA;MAEJ;KAEJ"}
1
+ {"version":3,"file":"SimpleAutoPayToggle.js","names":[],"sources":["../../../../../src/billing/modules/checkout/components/SimpleAutoPayToggle.tsx"],"sourcesContent":["/**\n * Simple Auto-Pay Toggle Component\n * Just enables/disables auto-renewal using the selected payment method\n */\n\nimport { type FC, useState } from 'react';\nimport { RefreshCw, Info } from 'lucide-react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\n\ninterface SimpleAutoPayToggleProps {\n enabled: boolean;\n onToggle: (enabled: boolean) => void;\n hasPaymentMethodSelected: boolean;\n selectedPaymentMethodDisplay?: string;\n isNewCard?: boolean; // Whether the selected payment method is a new card (not saved)\n disabled?: boolean;\n}\n\nexport const SimpleAutoPayToggle: FC<SimpleAutoPayToggleProps> = ({\n enabled,\n onToggle,\n hasPaymentMethodSelected,\n selectedPaymentMethodDisplay,\n isNewCard = false,\n disabled = false,\n}) => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n const [showInfo, setShowInfo] = useState(false);\n const canEnable = hasPaymentMethodSelected;\n\n return (\n <div className=\"pt-3 border-t border-border-subtle space-y-2\">\n {/* Toggle Row */}\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => {\n if (!canEnable) return;\n onToggle(!enabled);\n }}\n disabled={disabled || !canEnable}\n className={`\n relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent\n transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] focus:ring-offset-2\n ${enabled ? 'bg-action-primary-bg' : 'bg-bg-sunken'}\n ${(!canEnable || disabled) && 'opacity-50 cursor-not-allowed'}\n `}\n >\n <span\n className={`\n pointer-events-none inline-block size-4 transform rounded-full bg-bg-surface shadow ring-0\n transition duration-200 ease-in-out\n ${enabled ? 'translate-x-4' : 'translate-x-0'}\n `}\n />\n </button>\n\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-1.5\">\n <RefreshCw className=\"size-3.5 text-text-secondary shrink-0\" />\n <span className=\"text-xs font-medium text-text-primary\">\n {tr('billing.checkout.autoRenew', 'Auto-renew subscription')}\n </span>\n <button\n type=\"button\"\n onClick={() => setShowInfo(!showInfo)}\n className=\"p-0.5 hover:bg-bg-sunken rounded transition-colors\"\n >\n <Info className=\"size-3.5 text-text-secondary\" />\n </button>\n </div>\n\n {/* Status Message */}\n {enabled && selectedPaymentMethodDisplay && isNewCard ? (\n <div className=\"text-xs text-accent-blue mt-0.5\">\n ℹ️ This payment method will be saved for future renewals\n </div>\n ) : enabled && selectedPaymentMethodDisplay ? (\n <div className=\"text-xs text-success mt-0.5\">\n ✓ Will renew using {selectedPaymentMethodDisplay}\n </div>\n ) : !canEnable ? (\n <div className=\"text-xs text-warning mt-0.5\">\n {tr('billing.checkout.selectPaymentFirst', 'Select a payment method first')}\n </div>\n ) : null}\n </div>\n </div>\n\n {/* Info Panel */}\n {showInfo && (\n <div className=\"p-3 rounded-lg bg-bg-sunken border border-border-subtle\">\n <p className=\"text-xs text-text-secondary\">\n {canEnable\n ? 'Your subscription will automatically renew when it expires using the payment method selected on the left.'\n : 'Enable auto-renewal to automatically charge your payment method when the subscription expires. Select a payment method first.'}\n </p>\n\n {!canEnable && (\n <div className=\"flex items-start gap-2 p-2 rounded bg-warning/10 mt-2\">\n <Info className=\"size-3.5 text-warning shrink-0 mt-0.5\" />\n <p className=\"text-xs text-warning\">\n You can save a card during this purchase, then enable auto-renew for future\n subscriptions.\n </p>\n </div>\n )}\n </div>\n )}\n </div>\n );\n};\n"],"mappings":";;;;;AAkBA,IAAa,KAAqD,EAChE,YACA,aACA,6BACA,iCACA,eAAY,IACZ,cAAW,SACP;CACJ,IAAM,EAAE,SAAM,EAAQ,GAChB,KAAM,GAAa,MAA6B;EACpD,IAAM,IAAa,EAAE,CAAG;EACxB,OAAO,MAAe,IAAM,IAAW;CACzC,GACM,CAAC,GAAU,KAAe,EAAS,EAAK,GACxC,IAAY;CAElB,OACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CAEE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,UAAD;IACE,MAAK;IACL,eAAe;KACR,KACL,EAAS,CAAC,CAAO;IACnB;IACA,UAAU,KAAY,CAAC;IACvB,WAAW;;;cAGP,IAAU,yBAAyB,eAAe;eACjD,CAAC,KAAa,MAAa,gCAAgC;;cAGhE,kBAAC,QAAD,EACE,WAAW;;;gBAGP,IAAU,kBAAkB,gBAAgB;cAEjD,CAAA;GACK,CAAA,GAER,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,GAAD,EAAW,WAAU,wCAAyC,CAAA;MAC9D,kBAAC,QAAD;OAAM,WAAU;iBACb,EAAG,8BAA8B,yBAAyB;MACvD,CAAA;MACN,kBAAC,UAAD;OACE,MAAK;OACL,eAAe,EAAY,CAAC,CAAQ;OACpC,WAAU;iBAEV,kBAAC,GAAD,EAAM,WAAU,+BAAgC,CAAA;MAC1C,CAAA;KACL;QAGJ,KAAW,KAAgC,IAC1C,kBAAC,OAAD;KAAK,WAAU;eAAkC;IAE5C,CAAA,IACH,KAAW,IACb,kBAAC,OAAD;KAAK,WAAU;eAAf,CAA6C,uBACvB,CACjB;SACF,IAID,OAHF,kBAAC,OAAD;KAAK,WAAU;eACZ,EAAG,uCAAuC,+BAA+B;IACvE,CAAA,CAEJ;KACF;MAGJ,KACC,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,KAAD;IAAG,WAAU;cACV,IACG,8GACA;GACH,CAAA,GAEF,CAAC,KACA,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD,EAAM,WAAU,wCAAyC,CAAA,GACzD,kBAAC,KAAD;KAAG,WAAU;eAAuB;IAGjC,CAAA,CACA;KAEJ;IAEJ;;AAET"}
@@ -1 +1 @@
1
- {"version":3,"file":"StoreShippingSection.js","names":[],"sources":["../../../../../src/billing/modules/checkout/components/StoreShippingSection.tsx"],"sourcesContent":["/**\n * StoreShippingSection\n * ============================================================================\n * Physical-store-order-only shipping picker rendered inside the checkout\n * left column. Lets the buyer pick (or add) a delivery address and a shipping\n * method, then fetches a DISPLAY-ONLY cost estimate. The authoritative cost is\n * recomputed and persisted server-side via `setStoreOrderShipping` at\n * proceed-to-payment time (see CheckoutPage.handleProceedToPayment).\n *\n * This component is only ever mounted for store orders that contain a PHYSICAL\n * line item, so nothing here runs on the digital/plan/addon/credits checkout\n * path.\n *\n * Field set / validation / country list mirror\n * `components/CompleteBillingAccountModal.tsx` for visual + behavioural\n * consistency (shipping adds a required recipient `fullName`).\n */\n\nimport { type FC, useEffect, useMemo, useRef, useState } from 'react';\nimport { MapPin, Truck, Plus, Loader2, Check, X } from 'lucide-react';\nimport {\n useMyShippingAddresses,\n useCreateShippingAddress,\n useShippingMethods,\n useEstimateOrderShipping,\n type ShippingAddress,\n} from '../hooks/useStoreShipping';\n\n// Mirror of CompleteBillingAccountModal COUNTRY_OPTIONS.\nconst COUNTRY_OPTIONS = [\n { value: 'IN', label: 'India' },\n { value: 'US', label: 'United States' },\n { value: 'GB', label: 'United Kingdom' },\n { value: 'AU', label: 'Australia' },\n { value: 'CA', label: 'Canada' },\n { value: 'SG', label: 'Singapore' },\n { value: 'DE', label: 'Germany' },\n { value: 'FR', label: 'France' },\n { value: 'AE', label: 'UAE' },\n];\n\nexport interface StoreShippingSelection {\n shippingAddressId: string | null;\n shippingMethodId: string | null;\n /** DISPLAY-ONLY estimate in the order currency; backend recomputes on submit. */\n shippingCost: number;\n destinationCountry: string | null;\n}\n\ninterface Props {\n storeOrderId: string;\n /** Order/checkout currency, used only for formatting the estimate. */\n currency: string;\n onChange: (selection: StoreShippingSelection) => void;\n}\n\ninterface AddressFormState {\n fullName: string;\n phone: string;\n addressLine1: string;\n addressLine2: string;\n city: string;\n state: string;\n postalCode: string;\n country: string;\n}\n\ninterface AddressFormErrors {\n fullName?: string;\n addressLine1?: string;\n city?: string;\n postalCode?: string;\n country?: string;\n}\n\nconst EMPTY_FORM: AddressFormState = {\n fullName: '',\n phone: '',\n addressLine1: '',\n addressLine2: '',\n city: '',\n state: '',\n postalCode: '',\n country: 'IN',\n};\n\nfunction formatCost(amount: number, currency: string): string {\n try {\n return amount.toLocaleString('en', { style: 'currency', currency: currency || 'USD' });\n } catch {\n return `${currency || 'USD'} ${amount.toFixed(2)}`;\n }\n}\n\nfunction formatEta(minDays?: number | null, maxDays?: number | null): string | null {\n if (minDays == null && maxDays == null) return null;\n if (minDays != null && maxDays != null) {\n return minDays === maxDays ? `${minDays} days` : `${minDays}–${maxDays} days`;\n }\n const single = minDays ?? maxDays;\n return single != null ? `${single} days` : null;\n}\n\nexport const StoreShippingSection: FC<Props> = ({ storeOrderId, currency, onChange }) => {\n const {\n addresses,\n loading: addressesLoading,\n refetch: refetchAddresses,\n } = useMyShippingAddresses();\n const { createShippingAddress, loading: creatingAddress } = useCreateShippingAddress();\n const { methods, loading: methodsLoading } = useShippingMethods();\n const [runEstimate, { data: estData, loading: estimateLoading }] = useEstimateOrderShipping();\n\n const [selectedAddressId, setSelectedAddressId] = useState<string | null>(null);\n const [selectedMethodId, setSelectedMethodId] = useState<string | null>(null);\n\n const [showAddForm, setShowAddForm] = useState(false);\n const [form, setForm] = useState<AddressFormState>(EMPTY_FORM);\n const [formErrors, setFormErrors] = useState<AddressFormErrors>({});\n const [formServerError, setFormServerError] = useState<string | null>(null);\n\n // Keep onChange in a ref so the emit effect never depends on its identity.\n const onChangeRef = useRef(onChange);\n useEffect(() => {\n onChangeRef.current = onChange;\n });\n\n const selectedAddress = useMemo<ShippingAddress | undefined>(\n () => addresses.find((a) => a.id === selectedAddressId),\n [addresses, selectedAddressId]\n );\n\n // Methods available for the selected address' destination country.\n // Empty availableCountries is treated as \"available everywhere\".\n const availableMethods = useMemo(() => {\n if (!selectedAddress) return [];\n const country = selectedAddress.country;\n return methods\n .filter(\n (m) =>\n m.isActive &&\n (m.availableCountries.length === 0 || m.availableCountries.includes(country)) &&\n !m.excludedCountries.includes(country)\n )\n .sort((a, b) => a.displayOrder - b.displayOrder);\n }, [methods, selectedAddress]);\n\n // Default the address selection to the user's default (or first) address.\n useEffect(() => {\n if (!selectedAddressId && addresses.length > 0) {\n const preferred = addresses.find((a) => a.isDefault) ?? addresses[0];\n setSelectedAddressId(preferred.id);\n }\n }, [addresses, selectedAddressId]);\n\n // Drop a selected method that is no longer valid for the chosen destination.\n useEffect(() => {\n if (selectedMethodId && !availableMethods.some((m) => m.id === selectedMethodId)) {\n setSelectedMethodId(null);\n }\n }, [availableMethods, selectedMethodId]);\n\n // Fetch a fresh estimate whenever address + method are both chosen.\n useEffect(() => {\n if (selectedAddress && selectedMethodId) {\n void runEstimate({\n variables: {\n input: {\n orderId: storeOrderId,\n shippingMethodId: selectedMethodId,\n destinationCountry: selectedAddress.country,\n destinationPostalCode: selectedAddress.postalCode,\n },\n },\n });\n }\n }, [\n selectedAddress?.id,\n selectedAddress?.country,\n selectedAddress?.postalCode,\n selectedMethodId,\n storeOrderId,\n runEstimate,\n selectedAddress,\n ]);\n\n const estimate = estData?.estimateOrderShipping;\n const estimateReady = Boolean(\n selectedAddressId &&\n selectedMethodId &&\n !estimateLoading &&\n estimate &&\n estimate.shippingMethodId === selectedMethodId\n );\n\n // Emit the current selection upward. Method id + cost are only surfaced once a\n // matching estimate has resolved, so the parent's proceed gate stays disabled\n // while an estimate is in flight (free shipping resolves with cost 0, which is\n // still \"ready\").\n useEffect(() => {\n onChangeRef.current({\n shippingAddressId: selectedAddressId,\n shippingMethodId: estimateReady ? selectedMethodId : null,\n shippingCost: estimateReady && estimate ? estimate.cost : 0,\n destinationCountry: selectedAddress?.country ?? null,\n });\n // eslint-disable-next-line react-hooks/exhaustive-deps -- emit derived selection; estimate object identity excluded intentionally\n }, [\n selectedAddressId,\n selectedMethodId,\n estimateReady,\n estimate?.cost,\n selectedAddress?.country,\n ]);\n\n const setField = (field: keyof AddressFormState, value: string) =>\n setForm((prev) => ({ ...prev, [field]: value }));\n\n const validateForm = (): boolean => {\n const e: AddressFormErrors = {};\n if (!form.fullName.trim()) e.fullName = 'Recipient name is required';\n if (!form.addressLine1.trim()) e.addressLine1 = 'Street address is required';\n if (!form.city.trim()) e.city = 'City is required';\n if (!form.postalCode.trim()) e.postalCode = 'Postal code is required';\n if (!form.country.trim()) e.country = 'Country is required';\n setFormErrors(e);\n return Object.keys(e).length === 0;\n };\n\n const handleCreateAddress = async () => {\n if (!validateForm()) return;\n setFormServerError(null);\n try {\n const created = await createShippingAddress({\n fullName: form.fullName.trim(),\n ...(form.phone.trim() ? { phone: form.phone.trim() } : {}),\n addressLine1: form.addressLine1.trim(),\n ...(form.addressLine2.trim() ? { addressLine2: form.addressLine2.trim() } : {}),\n city: form.city.trim(),\n ...(form.state.trim() ? { state: form.state.trim() } : {}),\n postalCode: form.postalCode.trim(),\n country: form.country,\n isDefault: addresses.length === 0,\n });\n if (!created) {\n setFormServerError('Failed to save address. Please try again.');\n return;\n }\n await refetchAddresses();\n setSelectedAddressId(created.id);\n setSelectedMethodId(null);\n setForm(EMPTY_FORM);\n setFormErrors({});\n setShowAddForm(false);\n } catch {\n setFormServerError('Failed to save address. Please try again.');\n }\n };\n\n const inputClass = (hasError: boolean) =>\n `w-full px-3 py-2 text-sm border rounded-input bg-bg-surface text-text-primary placeholder:text-text-placeholder focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] ${\n hasError ? 'border-status-error-border' : 'border-border-subtle'\n }`;\n\n return (\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface p-4 sm:p-6 space-y-5\">\n <div className=\"flex items-center gap-2 text-sm font-semibold text-text-primary\">\n <Truck className=\"size-4 text-text-secondary\" />\n Shipping\n </div>\n\n {/* Delivery address */}\n <section className=\"space-y-3\">\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-2 text-sm font-medium text-text-primary\">\n <MapPin className=\"size-4 text-text-secondary\" />\n Delivery address\n </div>\n {!showAddForm && (\n <button\n type=\"button\"\n onClick={() => {\n setShowAddForm(true);\n setFormServerError(null);\n }}\n className=\"inline-flex items-center gap-1 text-xs font-medium text-text-link hover:underline\"\n >\n <Plus className=\"size-3.5\" />\n Add address\n </button>\n )}\n </div>\n\n {addressesLoading && addresses.length === 0 ? (\n <div className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <Loader2 className=\"size-4 animate-spin\" />\n Loading addresses…\n </div>\n ) : (\n <>\n {addresses.length === 0 && !showAddForm && (\n <p className=\"text-sm text-text-secondary\">\n No saved delivery addresses yet. Add one to continue.\n </p>\n )}\n {addresses.length > 0 && (\n <div className=\"space-y-2\">\n {addresses.map((addr) => {\n const selected = addr.id === selectedAddressId;\n return (\n <button\n key={addr.id}\n type=\"button\"\n onClick={() => {\n setSelectedAddressId(addr.id);\n setSelectedMethodId(null);\n }}\n className={`w-full text-left p-3 rounded-input border transition-colors ${\n selected\n ? 'border-action-primary-bg bg-bg-sunken'\n : 'border-border-subtle hover:bg-bg-sunken'\n }`}\n >\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"text-sm\">\n <p className=\"font-medium text-text-primary\">\n {addr.fullName}\n {addr.label ? (\n <span className=\"ml-2 text-xs text-text-muted\">({addr.label})</span>\n ) : null}\n </p>\n <p className=\"text-text-secondary text-xs mt-0.5\">\n {[\n addr.addressLine1,\n addr.addressLine2,\n addr.city,\n addr.state,\n addr.postalCode,\n addr.country,\n ]\n .filter(Boolean)\n .join(', ')}\n </p>\n {addr.phone && (\n <p className=\"text-text-muted text-xs mt-0.5\">{addr.phone}</p>\n )}\n </div>\n {selected && (\n <Check className=\"size-4 text-action-primary-bg shrink-0 mt-0.5\" />\n )}\n </div>\n </button>\n );\n })}\n </div>\n )}\n </>\n )}\n\n {/* Inline add-address form */}\n {showAddForm && (\n <div className=\"rounded-input border border-border-subtle p-3 space-y-3 bg-bg-sunken\">\n <div className=\"flex items-center justify-between\">\n <span className=\"text-sm font-medium text-text-primary\">New delivery address</span>\n <button\n type=\"button\"\n onClick={() => {\n setShowAddForm(false);\n setFormErrors({});\n setFormServerError(null);\n }}\n className=\"p-1 text-text-secondary hover:text-text-primary rounded-button\"\n >\n <X className=\"size-4\" />\n </button>\n </div>\n\n {formServerError && (\n <div className=\"p-2 rounded-lg bg-status-error-bg-subtle border border-status-error-border text-xs text-status-error-text\">\n {formServerError}\n </div>\n )}\n\n <div>\n <label className=\"block text-xs text-text-secondary mb-1\">Recipient name</label>\n <input\n type=\"text\"\n value={form.fullName}\n onChange={(e) => setField('fullName', e.target.value)}\n placeholder=\"Jane Doe\"\n className={inputClass(Boolean(formErrors.fullName))}\n />\n {formErrors.fullName && (\n <p className=\"mt-1 text-xs text-status-error-text\">{formErrors.fullName}</p>\n )}\n </div>\n\n <div>\n <label className=\"block text-xs text-text-secondary mb-1\">\n Phone <span className=\"text-text-muted\">(optional)</span>\n </label>\n <input\n type=\"tel\"\n value={form.phone}\n onChange={(e) => setField('phone', e.target.value)}\n placeholder=\"+91 98765 43210\"\n className={inputClass(false)}\n />\n </div>\n\n <div>\n <label className=\"block text-xs text-text-secondary mb-1\">Country</label>\n <select\n value={form.country}\n onChange={(e) => setField('country', e.target.value)}\n className={inputClass(Boolean(formErrors.country))}\n >\n {COUNTRY_OPTIONS.map((c) => (\n <option key={c.value} value={c.value}>\n {c.label}\n </option>\n ))}\n </select>\n </div>\n\n <div>\n <label className=\"block text-xs text-text-secondary mb-1\">Street address</label>\n <input\n type=\"text\"\n value={form.addressLine1}\n onChange={(e) => setField('addressLine1', e.target.value)}\n placeholder=\"123 Main St\"\n className={inputClass(Boolean(formErrors.addressLine1))}\n />\n {formErrors.addressLine1 && (\n <p className=\"mt-1 text-xs text-status-error-text\">{formErrors.addressLine1}</p>\n )}\n </div>\n\n <div>\n <label className=\"block text-xs text-text-secondary mb-1\">\n Apartment, suite, etc. <span className=\"text-text-muted\">(optional)</span>\n </label>\n <input\n type=\"text\"\n value={form.addressLine2}\n onChange={(e) => setField('addressLine2', e.target.value)}\n placeholder=\"Apt 4B\"\n className={inputClass(false)}\n />\n </div>\n\n <div className=\"grid grid-cols-2 gap-3\">\n <div>\n <label className=\"block text-xs text-text-secondary mb-1\">City</label>\n <input\n type=\"text\"\n value={form.city}\n onChange={(e) => setField('city', e.target.value)}\n placeholder=\"Mumbai\"\n className={inputClass(Boolean(formErrors.city))}\n />\n {formErrors.city && (\n <p className=\"mt-1 text-xs text-status-error-text\">{formErrors.city}</p>\n )}\n </div>\n <div>\n <label className=\"block text-xs text-text-secondary mb-1\">\n State <span className=\"text-text-muted\">(optional)</span>\n </label>\n <input\n type=\"text\"\n value={form.state}\n onChange={(e) => setField('state', e.target.value)}\n placeholder=\"Maharashtra\"\n className={inputClass(false)}\n />\n </div>\n </div>\n\n <div>\n <label className=\"block text-xs text-text-secondary mb-1\">Postal code</label>\n <input\n type=\"text\"\n value={form.postalCode}\n onChange={(e) => setField('postalCode', e.target.value)}\n placeholder=\"400001\"\n className={inputClass(Boolean(formErrors.postalCode))}\n />\n {formErrors.postalCode && (\n <p className=\"mt-1 text-xs text-status-error-text\">{formErrors.postalCode}</p>\n )}\n </div>\n\n <div className=\"flex justify-end\">\n <button\n type=\"button\"\n onClick={handleCreateAddress}\n disabled={creatingAddress}\n className=\"inline-flex items-center gap-2 px-3 py-2 text-sm font-medium text-action-primary-text bg-action-primary-bg hover:bg-action-primary-bgHover rounded-lg transition-colors disabled:opacity-50\"\n >\n {creatingAddress && <Loader2 className=\"size-4 animate-spin\" />}\n Save address\n </button>\n </div>\n </div>\n )}\n </section>\n\n {/* Shipping method */}\n <section className=\"space-y-3\">\n <div className=\"flex items-center gap-2 text-sm font-medium text-text-primary\">\n <Truck className=\"size-4 text-text-secondary\" />\n Shipping method\n </div>\n\n {!selectedAddress ? (\n <p className=\"text-sm text-text-secondary\">Select a delivery address to see options.</p>\n ) : methodsLoading && methods.length === 0 ? (\n <div className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <Loader2 className=\"size-4 animate-spin\" />\n Loading options…\n </div>\n ) : availableMethods.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">\n No shipping methods available for {selectedAddress.country}. Try a different address.\n </p>\n ) : (\n <div className=\"space-y-2\">\n {availableMethods.map((method) => {\n const selected = method.id === selectedMethodId;\n const eta = formatEta(method.estimatedMinDays, method.estimatedMaxDays);\n const showEstimate = selected && estimateReady && estimate;\n return (\n <button\n key={method.id}\n type=\"button\"\n onClick={() => setSelectedMethodId(method.id)}\n className={`w-full text-left p-3 rounded-input border transition-colors ${\n selected\n ? 'border-action-primary-bg bg-bg-sunken'\n : 'border-border-subtle hover:bg-bg-sunken'\n }`}\n >\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"text-sm\">\n <p className=\"font-medium text-text-primary\">{method.name}</p>\n <p className=\"text-text-secondary text-xs mt-0.5\">\n {method.carrier ? `${method.carrier} · ` : ''}\n {eta ?? 'Delivery time varies'}\n </p>\n </div>\n <div className=\"text-right shrink-0\">\n {selected && estimateLoading ? (\n <Loader2 className=\"size-4 animate-spin text-text-secondary\" />\n ) : (\n <span className=\"text-sm font-semibold text-text-primary\">\n {showEstimate\n ? formatCost(estimate.cost, estimate.currency || currency)\n : formatCost(method.baseCost, method.currency || currency)}\n </span>\n )}\n </div>\n </div>\n </button>\n );\n })}\n </div>\n )}\n\n {estimateReady && estimate && (\n <p className=\"text-xs text-text-secondary\">\n Estimated shipping: {formatCost(estimate.cost, estimate.currency || currency)}\n {estimate.estimatedDeliveryDate\n ? ` · arrives around ${new Date(estimate.estimatedDeliveryDate).toLocaleDateString()}`\n : ''}\n </p>\n )}\n </section>\n </div>\n );\n};\n"],"mappings":";;;;;AA6BA,IAAM,IAAkB;CACtB;EAAE,OAAO;EAAM,OAAO;EAAS;CAC/B;EAAE,OAAO;EAAM,OAAO;EAAiB;CACvC;EAAE,OAAO;EAAM,OAAO;EAAkB;CACxC;EAAE,OAAO;EAAM,OAAO;EAAa;CACnC;EAAE,OAAO;EAAM,OAAO;EAAU;CAChC;EAAE,OAAO;EAAM,OAAO;EAAa;CACnC;EAAE,OAAO;EAAM,OAAO;EAAW;CACjC;EAAE,OAAO;EAAM,OAAO;EAAU;CAChC;EAAE,OAAO;EAAM,OAAO;EAAO;CAC9B,EAoCK,IAA+B;CACnC,UAAU;CACV,OAAO;CACP,cAAc;CACd,cAAc;CACd,MAAM;CACN,OAAO;CACP,YAAY;CACZ,SAAS;CACV;AAED,SAAS,EAAW,GAAgB,GAA0B;AAC5D,KAAI;AACF,SAAO,EAAO,eAAe,MAAM;GAAE,OAAO;GAAY,UAAU,KAAY;GAAO,CAAC;SAChF;AACN,SAAO,GAAG,KAAY,MAAM,GAAG,EAAO,QAAQ,EAAE;;;AAIpD,SAAS,EAAU,GAAyB,GAAwC;AAClF,KAAI,KAAW,QAAQ,KAAW,KAAM,QAAO;AAC/C,KAAI,KAAW,QAAQ,KAAW,KAChC,QAAO,MAAY,IAAU,GAAG,EAAQ,SAAS,GAAG,EAAQ,GAAG,EAAQ;CAEzE,IAAM,IAAS,KAAW;AAC1B,QAAO,KAAU,OAA0B,OAAnB,GAAG,EAAO;;AAGpC,IAAa,KAAmC,EAAE,iBAAc,aAAU,kBAAe;CACvF,IAAM,EACJ,cACA,SAAS,GACT,SAAS,MACP,GAAwB,EACtB,EAAE,0BAAuB,SAAS,MAAoB,GAA0B,EAChF,EAAE,YAAS,SAAS,MAAmB,GAAoB,EAC3D,CAAC,GAAa,EAAE,MAAM,GAAS,SAAS,OAAqB,GAA0B,EAEvF,CAAC,GAAmB,KAAwB,EAAwB,KAAK,EACzE,CAAC,GAAkB,KAAuB,EAAwB,KAAK,EAEvE,CAAC,GAAa,KAAkB,EAAS,GAAM,EAC/C,CAAC,GAAM,KAAW,EAA2B,EAAW,EACxD,CAAC,GAAY,KAAiB,EAA4B,EAAE,CAAC,EAC7D,CAAC,GAAiB,KAAsB,EAAwB,KAAK,EAGrE,IAAc,EAAO,EAAS;AACpC,SAAgB;AACd,IAAY,UAAU;GACtB;CAEF,IAAM,IAAkB,QAChB,EAAU,MAAM,MAAM,EAAE,OAAO,EAAkB,EACvD,CAAC,GAAW,EAAkB,CAC/B,EAIK,IAAmB,QAAc;AACrC,MAAI,CAAC,EAAiB,QAAO,EAAE;EAC/B,IAAM,IAAU,EAAgB;AAChC,SAAO,EACJ,QACE,MACC,EAAE,aACD,EAAE,mBAAmB,WAAW,KAAK,EAAE,mBAAmB,SAAS,EAAQ,KAC5E,CAAC,EAAE,kBAAkB,SAAS,EAAQ,CACzC,CACA,MAAM,GAAG,MAAM,EAAE,eAAe,EAAE,aAAa;IACjD,CAAC,GAAS,EAAgB,CAAC;AAkB9B,CAfA,QAAgB;AACd,EAAI,CAAC,KAAqB,EAAU,SAAS,KAE3C,GADkB,EAAU,MAAM,MAAM,EAAE,UAAU,IAAI,EAAU,IACnC,GAAG;IAEnC,CAAC,GAAW,EAAkB,CAAC,EAGlC,QAAgB;AACd,EAAI,KAAoB,CAAC,EAAiB,MAAM,MAAM,EAAE,OAAO,EAAiB,IAC9E,EAAoB,KAAK;IAE1B,CAAC,GAAkB,EAAiB,CAAC,EAGxC,QAAgB;AACd,EAAI,KAAmB,KAChB,EAAY,EACf,WAAW,EACT,OAAO;GACL,SAAS;GACT,kBAAkB;GAClB,oBAAoB,EAAgB;GACpC,uBAAuB,EAAgB;GACxC,EACF,EACF,CAAC;IAEH;EACD,GAAiB;EACjB,GAAiB;EACjB,GAAiB;EACjB;EACA;EACA;EACA;EACD,CAAC;CAEF,IAAM,IAAW,GAAS,uBACpB,IAAgB,GACpB,KACA,KACA,CAAC,KACD,KACA,EAAS,qBAAqB;AAOhC,SAAgB;AACd,IAAY,QAAQ;GAClB,mBAAmB;GACnB,kBAAkB,IAAgB,IAAmB;GACrD,cAAc,KAAiB,IAAW,EAAS,OAAO;GAC1D,oBAAoB,GAAiB,WAAW;GACjD,CAAC;IAED;EACD;EACA;EACA;EACA,GAAU;EACV,GAAiB;EAClB,CAAC;CAEF,IAAM,KAAY,GAA+B,MAC/C,GAAS,OAAU;EAAE,GAAG;GAAO,IAAQ;EAAO,EAAE,EAE5C,UAA8B;EAClC,IAAM,IAAuB,EAAE;AAO/B,SANK,EAAK,SAAS,MAAM,KAAE,EAAE,WAAW,+BACnC,EAAK,aAAa,MAAM,KAAE,EAAE,eAAe,+BAC3C,EAAK,KAAK,MAAM,KAAE,EAAE,OAAO,qBAC3B,EAAK,WAAW,MAAM,KAAE,EAAE,aAAa,4BACvC,EAAK,QAAQ,MAAM,KAAE,EAAE,UAAU,wBACtC,EAAc,EAAE,EACT,OAAO,KAAK,EAAE,CAAC,WAAW;IAG7B,IAAsB,YAAY;AACjC,SAAc,EACnB;KAAmB,KAAK;AACxB,OAAI;IACF,IAAM,IAAU,MAAM,EAAsB;KAC1C,UAAU,EAAK,SAAS,MAAM;KAC9B,GAAI,EAAK,MAAM,MAAM,GAAG,EAAE,OAAO,EAAK,MAAM,MAAM,EAAE,GAAG,EAAE;KACzD,cAAc,EAAK,aAAa,MAAM;KACtC,GAAI,EAAK,aAAa,MAAM,GAAG,EAAE,cAAc,EAAK,aAAa,MAAM,EAAE,GAAG,EAAE;KAC9E,MAAM,EAAK,KAAK,MAAM;KACtB,GAAI,EAAK,MAAM,MAAM,GAAG,EAAE,OAAO,EAAK,MAAM,MAAM,EAAE,GAAG,EAAE;KACzD,YAAY,EAAK,WAAW,MAAM;KAClC,SAAS,EAAK;KACd,WAAW,EAAU,WAAW;KACjC,CAAC;AACF,QAAI,CAAC,GAAS;AACZ,OAAmB,4CAA4C;AAC/D;;AAOF,IALA,MAAM,GAAkB,EACxB,EAAqB,EAAQ,GAAG,EAChC,EAAoB,KAAK,EACzB,EAAQ,EAAW,EACnB,EAAc,EAAE,CAAC,EACjB,EAAe,GAAM;WACf;AACN,MAAmB,4CAA4C;;;IAI7D,KAAc,MAClB,wLACE,IAAW,+BAA+B;AAG9C,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD,EAAO,WAAU,8BAA+B,CAAA,EAAA,WAE5C;;GAGN,kBAAC,WAAD;IAAS,WAAU;cAAnB;KACE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD,EAAQ,WAAU,8BAA+B,CAAA,EAAA,mBAE7C;UACL,CAAC,KACA,kBAAC,UAAD;OACE,MAAK;OACL,eAAe;AAEb,QADA,EAAe,GAAK,EACpB,EAAmB,KAAK;;OAE1B,WAAU;iBANZ,CAQE,kBAAC,GAAD,EAAM,WAAU,YAAa,CAAA,EAAA,cAEtB;SAEP;;KAEL,KAAoB,EAAU,WAAW,IACxC,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,GAAD,EAAS,WAAU,uBAAwB,CAAA,EAAA,qBAEvC;UAEN,kBAAA,IAAA,EAAA,UAAA,CACG,EAAU,WAAW,KAAK,CAAC,KAC1B,kBAAC,KAAD;MAAG,WAAU;gBAA8B;MAEvC,CAAA,EAEL,EAAU,SAAS,KAClB,kBAAC,OAAD;MAAK,WAAU;gBACZ,EAAU,KAAK,MAAS;OACvB,IAAM,IAAW,EAAK,OAAO;AAC7B,cACE,kBAAC,UAAD;QAEE,MAAK;QACL,eAAe;AAEb,SADA,EAAqB,EAAK,GAAG,EAC7B,EAAoB,KAAK;;QAE3B,WAAW,+DACT,IACI,0CACA;kBAGN,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,OAAD;UAAK,WAAU;oBAAf;WACE,kBAAC,KAAD;YAAG,WAAU;sBAAb,CACG,EAAK,UACL,EAAK,QACJ,kBAAC,QAAD;aAAM,WAAU;uBAAhB;cAA+C;cAAE,EAAK;cAAM;cAAQ;iBAClE,KACF;;WACJ,kBAAC,KAAD;YAAG,WAAU;sBACV;aACC,EAAK;aACL,EAAK;aACL,EAAK;aACL,EAAK;aACL,EAAK;aACL,EAAK;aACN,CACE,OAAO,QAAQ,CACf,KAAK,KAAK;YACX,CAAA;WACH,EAAK,SACJ,kBAAC,KAAD;YAAG,WAAU;sBAAkC,EAAK;YAAU,CAAA;WAE5D;aACL,KACC,kBAAC,GAAD,EAAO,WAAU,iDAAkD,CAAA,CAEjE;;QACC,EAxCF,EAAK,GAwCH;QAEX;MACE,CAAA,CAEP,EAAA,CAAA;KAIJ,KACC,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACE,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,QAAD;SAAM,WAAU;mBAAwC;SAA2B,CAAA,EACnF,kBAAC,UAAD;SACE,MAAK;SACL,eAAe;AAGb,UAFA,EAAe,GAAM,EACrB,EAAc,EAAE,CAAC,EACjB,EAAmB,KAAK;;SAE1B,WAAU;mBAEV,kBAAC,GAAD,EAAG,WAAU,UAAW,CAAA;SACjB,CAAA,CACL;;OAEL,KACC,kBAAC,OAAD;QAAK,WAAU;kBACZ;QACG,CAAA;OAGR,kBAAC,OAAD,EAAA,UAAA;QACE,kBAAC,SAAD;SAAO,WAAU;mBAAyC;SAAsB,CAAA;QAChF,kBAAC,SAAD;SACE,MAAK;SACL,OAAO,EAAK;SACZ,WAAW,MAAM,EAAS,YAAY,EAAE,OAAO,MAAM;SACrD,aAAY;SACZ,WAAW,EAAW,EAAQ,EAAW,SAAU;SACnD,CAAA;QACD,EAAW,YACV,kBAAC,KAAD;SAAG,WAAU;mBAAuC,EAAW;SAAa,CAAA;QAE1E,EAAA,CAAA;OAEN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;QAAO,WAAU;kBAAjB,CAA0D,UAClD,kBAAC,QAAD;SAAM,WAAU;mBAAkB;SAAiB,CAAA,CACnD;WACR,kBAAC,SAAD;QACE,MAAK;QACL,OAAO,EAAK;QACZ,WAAW,MAAM,EAAS,SAAS,EAAE,OAAO,MAAM;QAClD,aAAY;QACZ,WAAW,EAAW,GAAM;QAC5B,CAAA,CACE,EAAA,CAAA;OAEN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;QAAO,WAAU;kBAAyC;QAAe,CAAA,EACzE,kBAAC,UAAD;QACE,OAAO,EAAK;QACZ,WAAW,MAAM,EAAS,WAAW,EAAE,OAAO,MAAM;QACpD,WAAW,EAAW,EAAQ,EAAW,QAAS;kBAEjD,EAAgB,KAAK,MACpB,kBAAC,UAAD;SAAsB,OAAO,EAAE;mBAC5B,EAAE;SACI,EAFI,EAAE,MAEN,CACT;QACK,CAAA,CACL,EAAA,CAAA;OAEN,kBAAC,OAAD,EAAA,UAAA;QACE,kBAAC,SAAD;SAAO,WAAU;mBAAyC;SAAsB,CAAA;QAChF,kBAAC,SAAD;SACE,MAAK;SACL,OAAO,EAAK;SACZ,WAAW,MAAM,EAAS,gBAAgB,EAAE,OAAO,MAAM;SACzD,aAAY;SACZ,WAAW,EAAW,EAAQ,EAAW,aAAc;SACvD,CAAA;QACD,EAAW,gBACV,kBAAC,KAAD;SAAG,WAAU;mBAAuC,EAAW;SAAiB,CAAA;QAE9E,EAAA,CAAA;OAEN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;QAAO,WAAU;kBAAjB,CAA0D,2BACjC,kBAAC,QAAD;SAAM,WAAU;mBAAkB;SAAiB,CAAA,CACpE;WACR,kBAAC,SAAD;QACE,MAAK;QACL,OAAO,EAAK;QACZ,WAAW,MAAM,EAAS,gBAAgB,EAAE,OAAO,MAAM;QACzD,aAAY;QACZ,WAAW,EAAW,GAAM;QAC5B,CAAA,CACE,EAAA,CAAA;OAEN,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,OAAD,EAAA,UAAA;SACE,kBAAC,SAAD;UAAO,WAAU;oBAAyC;UAAY,CAAA;SACtE,kBAAC,SAAD;UACE,MAAK;UACL,OAAO,EAAK;UACZ,WAAW,MAAM,EAAS,QAAQ,EAAE,OAAO,MAAM;UACjD,aAAY;UACZ,WAAW,EAAW,EAAQ,EAAW,KAAM;UAC/C,CAAA;SACD,EAAW,QACV,kBAAC,KAAD;UAAG,WAAU;oBAAuC,EAAW;UAAS,CAAA;SAEtE,EAAA,CAAA,EACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;SAAO,WAAU;mBAAjB,CAA0D,UAClD,kBAAC,QAAD;UAAM,WAAU;oBAAkB;UAAiB,CAAA,CACnD;YACR,kBAAC,SAAD;SACE,MAAK;SACL,OAAO,EAAK;SACZ,WAAW,MAAM,EAAS,SAAS,EAAE,OAAO,MAAM;SAClD,aAAY;SACZ,WAAW,EAAW,GAAM;SAC5B,CAAA,CACE,EAAA,CAAA,CACF;;OAEN,kBAAC,OAAD,EAAA,UAAA;QACE,kBAAC,SAAD;SAAO,WAAU;mBAAyC;SAAmB,CAAA;QAC7E,kBAAC,SAAD;SACE,MAAK;SACL,OAAO,EAAK;SACZ,WAAW,MAAM,EAAS,cAAc,EAAE,OAAO,MAAM;SACvD,aAAY;SACZ,WAAW,EAAW,EAAQ,EAAW,WAAY;SACrD,CAAA;QACD,EAAW,cACV,kBAAC,KAAD;SAAG,WAAU;mBAAuC,EAAW;SAAe,CAAA;QAE5E,EAAA,CAAA;OAEN,kBAAC,OAAD;QAAK,WAAU;kBACb,kBAAC,UAAD;SACE,MAAK;SACL,SAAS;SACT,UAAU;SACV,WAAU;mBAJZ,CAMG,KAAmB,kBAAC,GAAD,EAAS,WAAU,uBAAwB,CAAA,EAAC,eAEzD;;QACL,CAAA;OACF;;KAEA;;GAGV,kBAAC,WAAD;IAAS,WAAU;cAAnB;KACE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,GAAD,EAAO,WAAU,8BAA+B,CAAA,EAAA,kBAE5C;;KAEJ,IAEE,KAAkB,EAAQ,WAAW,IACvC,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,GAAD,EAAS,WAAU,uBAAwB,CAAA,EAAA,mBAEvC;UACJ,EAAiB,WAAW,IAC9B,kBAAC,KAAD;MAAG,WAAU;gBAAb;OAA2C;OACN,EAAgB;OAAQ;OACzD;UAEJ,kBAAC,OAAD;MAAK,WAAU;gBACZ,EAAiB,KAAK,MAAW;OAChC,IAAM,IAAW,EAAO,OAAO,GACzB,IAAM,EAAU,EAAO,kBAAkB,EAAO,iBAAiB,EACjE,IAAe,KAAY,KAAiB;AAClD,cACE,kBAAC,UAAD;QAEE,MAAK;QACL,eAAe,EAAoB,EAAO,GAAG;QAC7C,WAAW,+DACT,IACI,0CACA;kBAGN,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,KAAD;WAAG,WAAU;qBAAiC,EAAO;WAAS,CAAA,EAC9D,kBAAC,KAAD;WAAG,WAAU;qBAAb,CACG,EAAO,UAAU,GAAG,EAAO,QAAQ,OAAO,IAC1C,KAAO,uBACN;aACA;aACN,kBAAC,OAAD;UAAK,WAAU;oBACZ,KAAY,IACX,kBAAC,GAAD,EAAS,WAAU,2CAA4C,CAAA,GAE/D,kBAAC,QAAD;WAAM,WAAU;qBACb,IACG,EAAW,EAAS,MAAM,EAAS,YAAY,EAAS,GACxD,EAAW,EAAO,UAAU,EAAO,YAAY,EAAS;WACvD,CAAA;UAEL,CAAA,CACF;;QACC,EA7BF,EAAO,GA6BL;QAEX;MACE,CAAA,GAlDN,kBAAC,KAAD;MAAG,WAAU;gBAA8B;MAA6C,CAAA;KAqDzF,KAAiB,KAChB,kBAAC,KAAD;MAAG,WAAU;gBAAb;OAA2C;OACpB,EAAW,EAAS,MAAM,EAAS,YAAY,EAAS;OAC5E,EAAS,wBACN,qBAAqB,IAAI,KAAK,EAAS,sBAAsB,CAAC,oBAAoB,KAClF;OACF;;KAEE;;GACN"}
1
+ {"version":3,"file":"StoreShippingSection.js","names":[],"sources":["../../../../../src/billing/modules/checkout/components/StoreShippingSection.tsx"],"sourcesContent":["/**\n * StoreShippingSection\n * ============================================================================\n * Physical-store-order-only shipping picker rendered inside the checkout\n * left column. Lets the buyer pick (or add) a delivery address and a shipping\n * method, then fetches a DISPLAY-ONLY cost estimate. The authoritative cost is\n * recomputed and persisted server-side via `setStoreOrderShipping` at\n * proceed-to-payment time (see CheckoutPage.handleProceedToPayment).\n *\n * This component is only ever mounted for store orders that contain a PHYSICAL\n * line item, so nothing here runs on the digital/plan/addon/credits checkout\n * path.\n *\n * Field set / validation / country list mirror\n * `components/CompleteBillingAccountModal.tsx` for visual + behavioural\n * consistency (shipping adds a required recipient `fullName`).\n */\n\nimport { type FC, useEffect, useMemo, useRef, useState } from 'react';\nimport { MapPin, Truck, Plus, Loader2, Check, X } from 'lucide-react';\nimport {\n useMyShippingAddresses,\n useCreateShippingAddress,\n useShippingMethods,\n useEstimateOrderShipping,\n type ShippingAddress,\n} from '../hooks/useStoreShipping';\n\n// Mirror of CompleteBillingAccountModal COUNTRY_OPTIONS.\nconst COUNTRY_OPTIONS = [\n { value: 'IN', label: 'India' },\n { value: 'US', label: 'United States' },\n { value: 'GB', label: 'United Kingdom' },\n { value: 'AU', label: 'Australia' },\n { value: 'CA', label: 'Canada' },\n { value: 'SG', label: 'Singapore' },\n { value: 'DE', label: 'Germany' },\n { value: 'FR', label: 'France' },\n { value: 'AE', label: 'UAE' },\n];\n\nexport interface StoreShippingSelection {\n shippingAddressId: string | null;\n shippingMethodId: string | null;\n /** DISPLAY-ONLY estimate in the order currency; backend recomputes on submit. */\n shippingCost: number;\n destinationCountry: string | null;\n}\n\ninterface Props {\n storeOrderId: string;\n /** Order/checkout currency, used only for formatting the estimate. */\n currency: string;\n onChange: (selection: StoreShippingSelection) => void;\n}\n\ninterface AddressFormState {\n fullName: string;\n phone: string;\n addressLine1: string;\n addressLine2: string;\n city: string;\n state: string;\n postalCode: string;\n country: string;\n}\n\ninterface AddressFormErrors {\n fullName?: string;\n addressLine1?: string;\n city?: string;\n postalCode?: string;\n country?: string;\n}\n\nconst EMPTY_FORM: AddressFormState = {\n fullName: '',\n phone: '',\n addressLine1: '',\n addressLine2: '',\n city: '',\n state: '',\n postalCode: '',\n country: 'IN',\n};\n\nfunction formatCost(amount: number, currency: string): string {\n try {\n return amount.toLocaleString('en', { style: 'currency', currency: currency || 'USD' });\n } catch {\n return `${currency || 'USD'} ${amount.toFixed(2)}`;\n }\n}\n\nfunction formatEta(minDays?: number | null, maxDays?: number | null): string | null {\n if (minDays == null && maxDays == null) return null;\n if (minDays != null && maxDays != null) {\n return minDays === maxDays ? `${minDays} days` : `${minDays}–${maxDays} days`;\n }\n const single = minDays ?? maxDays;\n return single != null ? `${single} days` : null;\n}\n\nexport const StoreShippingSection: FC<Props> = ({ storeOrderId, currency, onChange }) => {\n const {\n addresses,\n loading: addressesLoading,\n refetch: refetchAddresses,\n } = useMyShippingAddresses();\n const { createShippingAddress, loading: creatingAddress } = useCreateShippingAddress();\n const { methods, loading: methodsLoading } = useShippingMethods();\n const [runEstimate, { data: estData, loading: estimateLoading }] = useEstimateOrderShipping();\n\n const [selectedAddressId, setSelectedAddressId] = useState<string | null>(null);\n const [selectedMethodId, setSelectedMethodId] = useState<string | null>(null);\n\n const [showAddForm, setShowAddForm] = useState(false);\n const [form, setForm] = useState<AddressFormState>(EMPTY_FORM);\n const [formErrors, setFormErrors] = useState<AddressFormErrors>({});\n const [formServerError, setFormServerError] = useState<string | null>(null);\n\n // Keep onChange in a ref so the emit effect never depends on its identity.\n const onChangeRef = useRef(onChange);\n useEffect(() => {\n onChangeRef.current = onChange;\n });\n\n const selectedAddress = useMemo<ShippingAddress | undefined>(\n () => addresses.find((a) => a.id === selectedAddressId),\n [addresses, selectedAddressId]\n );\n\n // Methods available for the selected address' destination country.\n // Empty availableCountries is treated as \"available everywhere\".\n const availableMethods = useMemo(() => {\n if (!selectedAddress) return [];\n const country = selectedAddress.country;\n return methods\n .filter(\n (m) =>\n m.isActive &&\n (m.availableCountries.length === 0 || m.availableCountries.includes(country)) &&\n !m.excludedCountries.includes(country)\n )\n .sort((a, b) => a.displayOrder - b.displayOrder);\n }, [methods, selectedAddress]);\n\n // Default the address selection to the user's default (or first) address.\n useEffect(() => {\n if (!selectedAddressId && addresses.length > 0) {\n const preferred = addresses.find((a) => a.isDefault) ?? addresses[0];\n setSelectedAddressId(preferred.id);\n }\n }, [addresses, selectedAddressId]);\n\n // Drop a selected method that is no longer valid for the chosen destination.\n useEffect(() => {\n if (selectedMethodId && !availableMethods.some((m) => m.id === selectedMethodId)) {\n setSelectedMethodId(null);\n }\n }, [availableMethods, selectedMethodId]);\n\n // Fetch a fresh estimate whenever address + method are both chosen.\n useEffect(() => {\n if (selectedAddress && selectedMethodId) {\n void runEstimate({\n variables: {\n input: {\n orderId: storeOrderId,\n shippingMethodId: selectedMethodId,\n destinationCountry: selectedAddress.country,\n destinationPostalCode: selectedAddress.postalCode,\n },\n },\n });\n }\n }, [\n selectedAddress?.id,\n selectedAddress?.country,\n selectedAddress?.postalCode,\n selectedMethodId,\n storeOrderId,\n runEstimate,\n selectedAddress,\n ]);\n\n const estimate = estData?.estimateOrderShipping;\n const estimateReady = Boolean(\n selectedAddressId &&\n selectedMethodId &&\n !estimateLoading &&\n estimate &&\n estimate.shippingMethodId === selectedMethodId\n );\n\n // Emit the current selection upward. Method id + cost are only surfaced once a\n // matching estimate has resolved, so the parent's proceed gate stays disabled\n // while an estimate is in flight (free shipping resolves with cost 0, which is\n // still \"ready\").\n useEffect(() => {\n onChangeRef.current({\n shippingAddressId: selectedAddressId,\n shippingMethodId: estimateReady ? selectedMethodId : null,\n shippingCost: estimateReady && estimate ? estimate.cost : 0,\n destinationCountry: selectedAddress?.country ?? null,\n });\n // eslint-disable-next-line react-hooks/exhaustive-deps -- emit derived selection; estimate object identity excluded intentionally\n }, [\n selectedAddressId,\n selectedMethodId,\n estimateReady,\n estimate?.cost,\n selectedAddress?.country,\n ]);\n\n const setField = (field: keyof AddressFormState, value: string) =>\n setForm((prev) => ({ ...prev, [field]: value }));\n\n const validateForm = (): boolean => {\n const e: AddressFormErrors = {};\n if (!form.fullName.trim()) e.fullName = 'Recipient name is required';\n if (!form.addressLine1.trim()) e.addressLine1 = 'Street address is required';\n if (!form.city.trim()) e.city = 'City is required';\n if (!form.postalCode.trim()) e.postalCode = 'Postal code is required';\n if (!form.country.trim()) e.country = 'Country is required';\n setFormErrors(e);\n return Object.keys(e).length === 0;\n };\n\n const handleCreateAddress = async () => {\n if (!validateForm()) return;\n setFormServerError(null);\n try {\n const created = await createShippingAddress({\n fullName: form.fullName.trim(),\n ...(form.phone.trim() ? { phone: form.phone.trim() } : {}),\n addressLine1: form.addressLine1.trim(),\n ...(form.addressLine2.trim() ? { addressLine2: form.addressLine2.trim() } : {}),\n city: form.city.trim(),\n ...(form.state.trim() ? { state: form.state.trim() } : {}),\n postalCode: form.postalCode.trim(),\n country: form.country,\n isDefault: addresses.length === 0,\n });\n if (!created) {\n setFormServerError('Failed to save address. Please try again.');\n return;\n }\n await refetchAddresses();\n setSelectedAddressId(created.id);\n setSelectedMethodId(null);\n setForm(EMPTY_FORM);\n setFormErrors({});\n setShowAddForm(false);\n } catch {\n setFormServerError('Failed to save address. Please try again.');\n }\n };\n\n const inputClass = (hasError: boolean) =>\n `w-full px-3 py-2 text-sm border rounded-input bg-bg-surface text-text-primary placeholder:text-text-placeholder focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] ${\n hasError ? 'border-status-error-border' : 'border-border-subtle'\n }`;\n\n return (\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface p-4 sm:p-6 space-y-5\">\n <div className=\"flex items-center gap-2 text-sm font-semibold text-text-primary\">\n <Truck className=\"size-4 text-text-secondary\" />\n Shipping\n </div>\n\n {/* Delivery address */}\n <section className=\"space-y-3\">\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-2 text-sm font-medium text-text-primary\">\n <MapPin className=\"size-4 text-text-secondary\" />\n Delivery address\n </div>\n {!showAddForm && (\n <button\n type=\"button\"\n onClick={() => {\n setShowAddForm(true);\n setFormServerError(null);\n }}\n className=\"inline-flex items-center gap-1 text-xs font-medium text-text-link hover:underline\"\n >\n <Plus className=\"size-3.5\" />\n Add address\n </button>\n )}\n </div>\n\n {addressesLoading && addresses.length === 0 ? (\n <div className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <Loader2 className=\"size-4 animate-spin\" />\n Loading addresses…\n </div>\n ) : (\n <>\n {addresses.length === 0 && !showAddForm && (\n <p className=\"text-sm text-text-secondary\">\n No saved delivery addresses yet. Add one to continue.\n </p>\n )}\n {addresses.length > 0 && (\n <div className=\"space-y-2\">\n {addresses.map((addr) => {\n const selected = addr.id === selectedAddressId;\n return (\n <button\n key={addr.id}\n type=\"button\"\n onClick={() => {\n setSelectedAddressId(addr.id);\n setSelectedMethodId(null);\n }}\n className={`w-full text-left p-3 rounded-input border transition-colors ${\n selected\n ? 'border-action-primary-bg bg-bg-sunken'\n : 'border-border-subtle hover:bg-bg-sunken'\n }`}\n >\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"text-sm\">\n <p className=\"font-medium text-text-primary\">\n {addr.fullName}\n {addr.label ? (\n <span className=\"ml-2 text-xs text-text-muted\">({addr.label})</span>\n ) : null}\n </p>\n <p className=\"text-text-secondary text-xs mt-0.5\">\n {[\n addr.addressLine1,\n addr.addressLine2,\n addr.city,\n addr.state,\n addr.postalCode,\n addr.country,\n ]\n .filter(Boolean)\n .join(', ')}\n </p>\n {addr.phone && (\n <p className=\"text-text-muted text-xs mt-0.5\">{addr.phone}</p>\n )}\n </div>\n {selected && (\n <Check className=\"size-4 text-action-primary-bg shrink-0 mt-0.5\" />\n )}\n </div>\n </button>\n );\n })}\n </div>\n )}\n </>\n )}\n\n {/* Inline add-address form */}\n {showAddForm && (\n <div className=\"rounded-input border border-border-subtle p-3 space-y-3 bg-bg-sunken\">\n <div className=\"flex items-center justify-between\">\n <span className=\"text-sm font-medium text-text-primary\">New delivery address</span>\n <button\n type=\"button\"\n onClick={() => {\n setShowAddForm(false);\n setFormErrors({});\n setFormServerError(null);\n }}\n className=\"p-1 text-text-secondary hover:text-text-primary rounded-button\"\n >\n <X className=\"size-4\" />\n </button>\n </div>\n\n {formServerError && (\n <div className=\"p-2 rounded-lg bg-status-error-bg-subtle border border-status-error-border text-xs text-status-error-text\">\n {formServerError}\n </div>\n )}\n\n <div>\n <label className=\"block text-xs text-text-secondary mb-1\">Recipient name</label>\n <input\n type=\"text\"\n value={form.fullName}\n onChange={(e) => setField('fullName', e.target.value)}\n placeholder=\"Jane Doe\"\n className={inputClass(Boolean(formErrors.fullName))}\n />\n {formErrors.fullName && (\n <p className=\"mt-1 text-xs text-status-error-text\">{formErrors.fullName}</p>\n )}\n </div>\n\n <div>\n <label className=\"block text-xs text-text-secondary mb-1\">\n Phone <span className=\"text-text-muted\">(optional)</span>\n </label>\n <input\n type=\"tel\"\n value={form.phone}\n onChange={(e) => setField('phone', e.target.value)}\n placeholder=\"+91 98765 43210\"\n className={inputClass(false)}\n />\n </div>\n\n <div>\n <label className=\"block text-xs text-text-secondary mb-1\">Country</label>\n <select\n value={form.country}\n onChange={(e) => setField('country', e.target.value)}\n className={inputClass(Boolean(formErrors.country))}\n >\n {COUNTRY_OPTIONS.map((c) => (\n <option key={c.value} value={c.value}>\n {c.label}\n </option>\n ))}\n </select>\n </div>\n\n <div>\n <label className=\"block text-xs text-text-secondary mb-1\">Street address</label>\n <input\n type=\"text\"\n value={form.addressLine1}\n onChange={(e) => setField('addressLine1', e.target.value)}\n placeholder=\"123 Main St\"\n className={inputClass(Boolean(formErrors.addressLine1))}\n />\n {formErrors.addressLine1 && (\n <p className=\"mt-1 text-xs text-status-error-text\">{formErrors.addressLine1}</p>\n )}\n </div>\n\n <div>\n <label className=\"block text-xs text-text-secondary mb-1\">\n Apartment, suite, etc. <span className=\"text-text-muted\">(optional)</span>\n </label>\n <input\n type=\"text\"\n value={form.addressLine2}\n onChange={(e) => setField('addressLine2', e.target.value)}\n placeholder=\"Apt 4B\"\n className={inputClass(false)}\n />\n </div>\n\n <div className=\"grid grid-cols-2 gap-3\">\n <div>\n <label className=\"block text-xs text-text-secondary mb-1\">City</label>\n <input\n type=\"text\"\n value={form.city}\n onChange={(e) => setField('city', e.target.value)}\n placeholder=\"Mumbai\"\n className={inputClass(Boolean(formErrors.city))}\n />\n {formErrors.city && (\n <p className=\"mt-1 text-xs text-status-error-text\">{formErrors.city}</p>\n )}\n </div>\n <div>\n <label className=\"block text-xs text-text-secondary mb-1\">\n State <span className=\"text-text-muted\">(optional)</span>\n </label>\n <input\n type=\"text\"\n value={form.state}\n onChange={(e) => setField('state', e.target.value)}\n placeholder=\"Maharashtra\"\n className={inputClass(false)}\n />\n </div>\n </div>\n\n <div>\n <label className=\"block text-xs text-text-secondary mb-1\">Postal code</label>\n <input\n type=\"text\"\n value={form.postalCode}\n onChange={(e) => setField('postalCode', e.target.value)}\n placeholder=\"400001\"\n className={inputClass(Boolean(formErrors.postalCode))}\n />\n {formErrors.postalCode && (\n <p className=\"mt-1 text-xs text-status-error-text\">{formErrors.postalCode}</p>\n )}\n </div>\n\n <div className=\"flex justify-end\">\n <button\n type=\"button\"\n onClick={handleCreateAddress}\n disabled={creatingAddress}\n className=\"inline-flex items-center gap-2 px-3 py-2 text-sm font-medium text-action-primary-text bg-action-primary-bg hover:bg-action-primary-bgHover rounded-lg transition-colors disabled:opacity-50\"\n >\n {creatingAddress && <Loader2 className=\"size-4 animate-spin\" />}\n Save address\n </button>\n </div>\n </div>\n )}\n </section>\n\n {/* Shipping method */}\n <section className=\"space-y-3\">\n <div className=\"flex items-center gap-2 text-sm font-medium text-text-primary\">\n <Truck className=\"size-4 text-text-secondary\" />\n Shipping method\n </div>\n\n {!selectedAddress ? (\n <p className=\"text-sm text-text-secondary\">Select a delivery address to see options.</p>\n ) : methodsLoading && methods.length === 0 ? (\n <div className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <Loader2 className=\"size-4 animate-spin\" />\n Loading options…\n </div>\n ) : availableMethods.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">\n No shipping methods available for {selectedAddress.country}. Try a different address.\n </p>\n ) : (\n <div className=\"space-y-2\">\n {availableMethods.map((method) => {\n const selected = method.id === selectedMethodId;\n const eta = formatEta(method.estimatedMinDays, method.estimatedMaxDays);\n const showEstimate = selected && estimateReady && estimate;\n return (\n <button\n key={method.id}\n type=\"button\"\n onClick={() => setSelectedMethodId(method.id)}\n className={`w-full text-left p-3 rounded-input border transition-colors ${\n selected\n ? 'border-action-primary-bg bg-bg-sunken'\n : 'border-border-subtle hover:bg-bg-sunken'\n }`}\n >\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"text-sm\">\n <p className=\"font-medium text-text-primary\">{method.name}</p>\n <p className=\"text-text-secondary text-xs mt-0.5\">\n {method.carrier ? `${method.carrier} · ` : ''}\n {eta ?? 'Delivery time varies'}\n </p>\n </div>\n <div className=\"text-right shrink-0\">\n {selected && estimateLoading ? (\n <Loader2 className=\"size-4 animate-spin text-text-secondary\" />\n ) : (\n <span className=\"text-sm font-semibold text-text-primary\">\n {showEstimate\n ? formatCost(estimate.cost, estimate.currency || currency)\n : formatCost(method.baseCost, method.currency || currency)}\n </span>\n )}\n </div>\n </div>\n </button>\n );\n })}\n </div>\n )}\n\n {estimateReady && estimate && (\n <p className=\"text-xs text-text-secondary\">\n Estimated shipping: {formatCost(estimate.cost, estimate.currency || currency)}\n {estimate.estimatedDeliveryDate\n ? ` · arrives around ${new Date(estimate.estimatedDeliveryDate).toLocaleDateString()}`\n : ''}\n </p>\n )}\n </section>\n </div>\n );\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"StripePaymentForm.js","names":[],"sources":["../../../../../src/billing/modules/checkout/components/StripePaymentForm.tsx"],"sourcesContent":["/**\n * Stripe Payment Form Component\n * Integrates with Stripe Elements for card payment\n */\n\nimport { type FC, useState, useCallback, useEffect, useRef } from 'react';\nimport { CreditCard, Lock, Loader2, Check, AlertCircle } from 'lucide-react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { getStripeCardElementStyle, statusTokens } from '../../../shared/utils';\n\n// Stripe types - local definitions to avoid conflicts with external type declarations\ninterface LocalStripeCardElement {\n mount: (element: HTMLElement) => void;\n unmount: () => void;\n destroy: () => void;\n on: (event: string, handler: (event: LocalStripeCardChangeEvent) => void) => void;\n}\n\ninterface LocalStripeCardChangeEvent {\n complete: boolean;\n error?: { message: string };\n}\n\ninterface LocalStripeElements {\n create: (type: 'card', options?: Record<string, unknown>) => LocalStripeCardElement;\n}\n\ninterface LocalStripePaymentMethod {\n id: string;\n card?: {\n last4: string;\n brand: string;\n };\n}\n\ninterface LocalStripeInstance {\n elements: () => LocalStripeElements;\n createPaymentMethod: (options: {\n type: 'card';\n card: LocalStripeCardElement;\n }) => Promise<{ paymentMethod?: LocalStripePaymentMethod; error?: { message: string } }>;\n confirmCardPayment: (\n clientSecret: string,\n options?: { payment_method: string }\n ) => Promise<{\n paymentIntent?: { id: string; status: string };\n error?: { message: string };\n }>;\n}\n\n// Use type assertion when accessing window.Stripe\ntype StripeFactory = (key: string) => LocalStripeInstance;\n\ninterface StripePaymentFormProps {\n clientSecret: string;\n publishableKey: string;\n amount: number;\n currency: string;\n onSuccess: (paymentIntentId: string) => void;\n onError: (error: string) => void;\n onCancel?: () => void;\n disabled?: boolean;\n isCompletingExistingPayment?: boolean; // True for saved cards that need 3D Secure\n}\n\ntype PaymentState = 'idle' | 'loading' | 'processing' | 'success' | 'error';\n\n/**\n * Load Stripe.js SDK dynamically\n */\nconst loadStripeSDK = (): Promise<void> => {\n return new Promise((resolve, reject) => {\n if (window.Stripe) {\n resolve();\n return;\n }\n\n const existingScript = document.querySelector('script[src=\"https://js.stripe.com/v3/\"]');\n if (existingScript) {\n // Wait for existing script to load\n const checkStripe = setInterval(() => {\n if (window.Stripe) {\n clearInterval(checkStripe);\n resolve();\n }\n }, 100);\n return;\n }\n\n const script = document.createElement('script');\n script.src = 'https://js.stripe.com/v3/';\n script.async = true;\n script.onload = () => resolve();\n script.onerror = () => reject(new Error('Failed to load Stripe SDK'));\n document.body.appendChild(script);\n });\n};\n\nexport const StripePaymentForm: FC<StripePaymentFormProps> = ({\n clientSecret,\n publishableKey,\n amount,\n currency,\n onSuccess,\n onError,\n onCancel,\n disabled = false,\n isCompletingExistingPayment = false,\n}) => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n const [paymentState, setPaymentState] = useState<PaymentState>('idle');\n const [error, setError] = useState<string | null>(null);\n const [sdkLoaded, setSdkLoaded] = useState(false);\n const [cardComplete, setCardComplete] = useState(false);\n const [cardError, setCardError] = useState<string | null>(null);\n\n const cardElementRef = useRef<HTMLDivElement>(null);\n const stripeRef = useRef<LocalStripeInstance | null>(null);\n const cardElementInstanceRef = useRef<LocalStripeCardElement | null>(null);\n const isMountedRef = useRef(false);\n\n // Load Stripe SDK\n useEffect(() => {\n loadStripeSDK()\n .then(() => {\n setSdkLoaded(true);\n })\n .catch(() => {\n setError('Failed to load payment gateway');\n });\n }, []);\n\n // Initialize Stripe and mount card element (only for new card payments)\n useEffect(() => {\n // Skip card input for completing existing payments (saved cards with 3D Secure)\n if (isCompletingExistingPayment) {\n return;\n }\n\n const windowStripe = (window as unknown as { Stripe?: StripeFactory }).Stripe;\n if (!sdkLoaded || !publishableKey || !windowStripe || isMountedRef.current) {\n return;\n }\n\n // Initialize Stripe\n const stripeInstance = windowStripe(publishableKey);\n stripeRef.current = stripeInstance;\n\n // Create elements\n const elements = stripeInstance.elements();\n const cardElement = elements.create('card', {\n style: getStripeCardElementStyle(),\n });\n cardElementInstanceRef.current = cardElement;\n\n // Mount card element\n if (cardElementRef.current) {\n cardElement.mount(cardElementRef.current);\n isMountedRef.current = true;\n\n // Listen to card element changes\n cardElement.on('change', (event: LocalStripeCardChangeEvent) => {\n setCardError(event.error ? event.error.message : null);\n setCardComplete(event.complete);\n });\n }\n\n // Cleanup\n return () => {\n if (cardElementInstanceRef.current && isMountedRef.current) {\n try {\n cardElementInstanceRef.current.unmount();\n cardElementInstanceRef.current.destroy();\n } catch {\n // Element might already be destroyed\n }\n cardElementInstanceRef.current = null;\n isMountedRef.current = false;\n }\n };\n }, [sdkLoaded, publishableKey, isCompletingExistingPayment]);\n\n // Auto-confirm payment for saved cards (requires 3D Secure or similar action)\n useEffect(() => {\n if (!isCompletingExistingPayment || !sdkLoaded || !publishableKey || paymentState !== 'idle') {\n return;\n }\n\n const confirmExistingPayment = async () => {\n const windowStripe = (window as unknown as { Stripe?: StripeFactory }).Stripe;\n if (!windowStripe) {\n setError('Payment gateway not loaded');\n return;\n }\n\n // Initialize Stripe if not already done\n if (!stripeRef.current) {\n stripeRef.current = windowStripe(publishableKey);\n }\n\n setPaymentState('processing');\n setError(null);\n\n try {\n // Confirm the payment (payment method already attached by backend)\n // This will show 3D Secure popup if needed\n const { paymentIntent, error: confirmError } =\n await stripeRef.current.confirmCardPayment(clientSecret);\n\n if (confirmError) {\n throw new Error(confirmError.message || 'Payment confirmation failed');\n }\n\n if (paymentIntent?.status === 'succeeded') {\n setPaymentState('success');\n onSuccess(paymentIntent.id);\n } else if (paymentIntent?.status === 'processing') {\n setPaymentState('processing');\n // Keep in processing state - webhook will handle completion\n } else {\n throw new Error('Payment was not successful');\n }\n } catch (err) {\n const errorMessage = err instanceof Error ? err.message : 'Payment confirmation failed';\n setError(errorMessage);\n setPaymentState('error');\n onError(errorMessage);\n }\n };\n\n confirmExistingPayment();\n }, [\n isCompletingExistingPayment,\n sdkLoaded,\n publishableKey,\n clientSecret,\n paymentState,\n onSuccess,\n onError,\n ]);\n\n const handleSubmit = useCallback(\n async (e: React.FormEvent) => {\n e.preventDefault();\n\n const stripe = stripeRef.current;\n const cardElement = cardElementInstanceRef.current;\n\n if (!stripe || !cardElement) {\n setError('Payment form not ready');\n return;\n }\n\n if (!cardComplete) {\n setCardError('Please enter complete card details');\n return;\n }\n\n setPaymentState('processing');\n setError(null);\n setCardError(null);\n\n try {\n // Step 1: Create PaymentMethod\n const { paymentMethod, error: pmError } = await stripe.createPaymentMethod({\n type: 'card',\n card: cardElement,\n });\n\n if (pmError) {\n throw new Error(pmError.message || 'Failed to create payment method');\n }\n\n if (!paymentMethod) {\n throw new Error('No payment method returned');\n }\n\n // Step 2: Confirm the payment with the client secret\n const { paymentIntent, error: confirmError } = await stripe.confirmCardPayment(\n clientSecret,\n { payment_method: paymentMethod.id }\n );\n\n if (confirmError) {\n throw new Error(confirmError.message || 'Payment failed');\n }\n\n if (paymentIntent?.status === 'succeeded') {\n setPaymentState('success');\n onSuccess(paymentIntent.id);\n } else if (paymentIntent?.status === 'processing') {\n setPaymentState('processing');\n } else {\n throw new Error('Payment was not successful');\n }\n } catch (err) {\n const errorMessage = err instanceof Error ? err.message : 'Payment failed';\n setError(errorMessage);\n setPaymentState('error');\n onError(errorMessage);\n }\n },\n [clientSecret, cardComplete, onSuccess, onError]\n );\n\n // Render success state\n if (paymentState === 'success') {\n return (\n <div\n className={`p-6 rounded-lg border ${statusTokens.success.border} ${statusTokens.success.bg}`}\n >\n <div className=\"flex items-center gap-3\">\n <div\n className={`p-2 rounded-full ${statusTokens.success.bg} ${statusTokens.success.icon}`}\n >\n <Check className=\"size-6\" />\n </div>\n <div>\n <h3 className={`font-semibold ${statusTokens.success.text}`}>Payment Successful!</h3>\n <p className={`text-sm ${statusTokens.success.text}`}>\n Your payment has been processed successfully.\n </p>\n </div>\n </div>\n </div>\n );\n }\n\n // Show loading while SDK loads\n if (!sdkLoaded) {\n return (\n <div className=\"space-y-4\">\n <div className=\"flex items-center gap-2\">\n <CreditCard className=\"size-5 text-text-secondary\" />\n <span className=\"font-medium text-text-primary\">\n {tr('billing.checkout.cardPayment', 'Card Payment')}\n </span>\n </div>\n <div className=\"p-6 rounded-lg border border-border-subtle bg-bg-sunken flex items-center justify-center\">\n <Loader2 className=\"size-6 animate-spin text-text-secondary\" />\n <span className=\"ml-2 text-text-secondary\">\n {tr('billing.checkout.loadingPaymentForm', 'Loading payment form...')}\n </span>\n </div>\n </div>\n );\n }\n\n // For saved card payments that need confirmation (3D Secure, etc.)\n if (isCompletingExistingPayment) {\n return (\n <div className=\"space-y-4\">\n <div className=\"flex items-center gap-2\">\n <Lock className=\"size-5 text-text-link\" />\n <span className=\"font-medium text-text-primary\">Secure Payment Verification</span>\n </div>\n\n {paymentState === 'processing' && (\n <div className=\"p-6 rounded-lg border border-border-subtle bg-bg-sunken flex flex-col items-center justify-center space-y-3\">\n <Loader2 className=\"size-8 animate-spin text-text-link\" />\n <p className=\"text-sm text-text-secondary text-center\">\n Confirming payment with your saved card…\n <br />\n <span className=\"text-xs\">Please complete any security verification if prompted</span>\n </p>\n </div>\n )}\n\n {error && (\n <div className=\"flex items-center gap-2 p-3 rounded-lg bg-status-error-bg-subtle text-status-error-text text-sm\">\n <AlertCircle className=\"size-4 shrink-0\" />\n <div>\n <p className=\"font-medium\">\n {tr('billing.checkout.paymentFailed', 'Payment Failed')}\n </p>\n <p className=\"text-xs\">{error}</p>\n </div>\n </div>\n )}\n\n {(paymentState === 'error' || error) && onCancel && (\n <div className=\"flex gap-3\">\n <button\n type=\"button\"\n onClick={onCancel}\n className=\"flex-1 px-4 py-3 rounded-lg border border-border-subtle text-text-primary font-medium\n hover:bg-bg-sunken transition-colors\"\n >\n Back to Checkout\n </button>\n </div>\n )}\n </div>\n );\n }\n\n return (\n <form onSubmit={handleSubmit} className=\"space-y-4\">\n {/* Header */}\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-2\">\n <CreditCard className=\"size-5 text-text-secondary\" />\n <span className=\"font-medium text-text-primary\">Card Payment</span>\n </div>\n <div className=\"flex items-center gap-1 text-xs text-text-secondary\">\n <Lock className=\"size-3\" />\n Secured by Stripe\n </div>\n </div>\n\n {/* Stripe Card Element container - NO conditional rendering inside */}\n <div className=\"space-y-2\">\n <div\n ref={cardElementRef}\n className=\"min-h-[50px] rounded-lg border border-border-subtle bg-bg-surface p-4\"\n />\n {cardError && <p className=\"text-sm text-status-error-text\">{cardError}</p>}\n </div>\n\n {/* Error message */}\n {error && (\n <div className=\"flex items-center gap-2 p-3 rounded-lg bg-status-error-bg-subtle text-status-error-text text-sm\">\n <AlertCircle className=\"size-4 shrink-0\" />\n {error}\n </div>\n )}\n\n {/* Actions */}\n <div className=\"flex gap-3\">\n {onCancel && (\n <button\n type=\"button\"\n onClick={onCancel}\n disabled={paymentState === 'processing' || disabled}\n className=\"flex-1 px-4 py-3 rounded-lg border border-border-subtle text-text-primary font-medium\n hover:bg-bg-sunken disabled:opacity-50 disabled:cursor-not-allowed transition-colors\"\n >\n Cancel\n </button>\n )}\n <button\n type=\"submit\"\n disabled={paymentState === 'processing' || disabled || !cardComplete}\n className=\"flex-1 px-4 py-3 rounded-lg bg-action-primary-bg text-action-primary-text font-medium\n hover:bg-action-primary-bgHover disabled:opacity-50 disabled:cursor-not-allowed transition-colors\n flex items-center justify-center gap-2\"\n >\n {paymentState === 'processing' ? (\n <>\n <Loader2 className=\"size-4 animate-spin\" />\n Processing…\n </>\n ) : (\n <>\n Pay{' '}\n {new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: currency.toUpperCase(),\n }).format(amount / 100)}\n </>\n )}\n </button>\n </div>\n\n {/* Security note */}\n <p className=\"text-center text-xs text-text-secondary\">\n Your payment information is encrypted and secure. We never store your full card details.\n </p>\n </form>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"StripePaymentForm.js","names":[],"sources":["../../../../../src/billing/modules/checkout/components/StripePaymentForm.tsx"],"sourcesContent":["/**\n * Stripe Payment Form Component\n * Integrates with Stripe Elements for card payment\n */\n\nimport { type FC, useState, useCallback, useEffect, useRef } from 'react';\nimport { CreditCard, Lock, Loader2, Check, AlertCircle } from 'lucide-react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { getStripeCardElementStyle, statusTokens } from '../../../shared/utils';\n\n// Stripe types - local definitions to avoid conflicts with external type declarations\ninterface LocalStripeCardElement {\n mount: (element: HTMLElement) => void;\n unmount: () => void;\n destroy: () => void;\n on: (event: string, handler: (event: LocalStripeCardChangeEvent) => void) => void;\n}\n\ninterface LocalStripeCardChangeEvent {\n complete: boolean;\n error?: { message: string };\n}\n\ninterface LocalStripeElements {\n create: (type: 'card', options?: Record<string, unknown>) => LocalStripeCardElement;\n}\n\ninterface LocalStripePaymentMethod {\n id: string;\n card?: {\n last4: string;\n brand: string;\n };\n}\n\ninterface LocalStripeInstance {\n elements: () => LocalStripeElements;\n createPaymentMethod: (options: {\n type: 'card';\n card: LocalStripeCardElement;\n }) => Promise<{ paymentMethod?: LocalStripePaymentMethod; error?: { message: string } }>;\n confirmCardPayment: (\n clientSecret: string,\n options?: { payment_method: string }\n ) => Promise<{\n paymentIntent?: { id: string; status: string };\n error?: { message: string };\n }>;\n}\n\n// Use type assertion when accessing window.Stripe\ntype StripeFactory = (key: string) => LocalStripeInstance;\n\ninterface StripePaymentFormProps {\n clientSecret: string;\n publishableKey: string;\n amount: number;\n currency: string;\n onSuccess: (paymentIntentId: string) => void;\n onError: (error: string) => void;\n onCancel?: () => void;\n disabled?: boolean;\n isCompletingExistingPayment?: boolean; // True for saved cards that need 3D Secure\n}\n\ntype PaymentState = 'idle' | 'loading' | 'processing' | 'success' | 'error';\n\n/**\n * Load Stripe.js SDK dynamically\n */\nconst loadStripeSDK = (): Promise<void> => {\n return new Promise((resolve, reject) => {\n if (window.Stripe) {\n resolve();\n return;\n }\n\n const existingScript = document.querySelector('script[src=\"https://js.stripe.com/v3/\"]');\n if (existingScript) {\n // Wait for existing script to load\n const checkStripe = setInterval(() => {\n if (window.Stripe) {\n clearInterval(checkStripe);\n resolve();\n }\n }, 100);\n return;\n }\n\n const script = document.createElement('script');\n script.src = 'https://js.stripe.com/v3/';\n script.async = true;\n script.onload = () => resolve();\n script.onerror = () => reject(new Error('Failed to load Stripe SDK'));\n document.body.appendChild(script);\n });\n};\n\nexport const StripePaymentForm: FC<StripePaymentFormProps> = ({\n clientSecret,\n publishableKey,\n amount,\n currency,\n onSuccess,\n onError,\n onCancel,\n disabled = false,\n isCompletingExistingPayment = false,\n}) => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n const [paymentState, setPaymentState] = useState<PaymentState>('idle');\n const [error, setError] = useState<string | null>(null);\n const [sdkLoaded, setSdkLoaded] = useState(false);\n const [cardComplete, setCardComplete] = useState(false);\n const [cardError, setCardError] = useState<string | null>(null);\n\n const cardElementRef = useRef<HTMLDivElement>(null);\n const stripeRef = useRef<LocalStripeInstance | null>(null);\n const cardElementInstanceRef = useRef<LocalStripeCardElement | null>(null);\n const isMountedRef = useRef(false);\n\n // Load Stripe SDK\n useEffect(() => {\n loadStripeSDK()\n .then(() => {\n setSdkLoaded(true);\n })\n .catch(() => {\n setError('Failed to load payment gateway');\n });\n }, []);\n\n // Initialize Stripe and mount card element (only for new card payments)\n useEffect(() => {\n // Skip card input for completing existing payments (saved cards with 3D Secure)\n if (isCompletingExistingPayment) {\n return;\n }\n\n const windowStripe = (window as unknown as { Stripe?: StripeFactory }).Stripe;\n if (!sdkLoaded || !publishableKey || !windowStripe || isMountedRef.current) {\n return;\n }\n\n // Initialize Stripe\n const stripeInstance = windowStripe(publishableKey);\n stripeRef.current = stripeInstance;\n\n // Create elements\n const elements = stripeInstance.elements();\n const cardElement = elements.create('card', {\n style: getStripeCardElementStyle(),\n });\n cardElementInstanceRef.current = cardElement;\n\n // Mount card element\n if (cardElementRef.current) {\n cardElement.mount(cardElementRef.current);\n isMountedRef.current = true;\n\n // Listen to card element changes\n cardElement.on('change', (event: LocalStripeCardChangeEvent) => {\n setCardError(event.error ? event.error.message : null);\n setCardComplete(event.complete);\n });\n }\n\n // Cleanup\n return () => {\n if (cardElementInstanceRef.current && isMountedRef.current) {\n try {\n cardElementInstanceRef.current.unmount();\n cardElementInstanceRef.current.destroy();\n } catch {\n // Element might already be destroyed\n }\n cardElementInstanceRef.current = null;\n isMountedRef.current = false;\n }\n };\n }, [sdkLoaded, publishableKey, isCompletingExistingPayment]);\n\n // Auto-confirm payment for saved cards (requires 3D Secure or similar action)\n useEffect(() => {\n if (!isCompletingExistingPayment || !sdkLoaded || !publishableKey || paymentState !== 'idle') {\n return;\n }\n\n const confirmExistingPayment = async () => {\n const windowStripe = (window as unknown as { Stripe?: StripeFactory }).Stripe;\n if (!windowStripe) {\n setError('Payment gateway not loaded');\n return;\n }\n\n // Initialize Stripe if not already done\n if (!stripeRef.current) {\n stripeRef.current = windowStripe(publishableKey);\n }\n\n setPaymentState('processing');\n setError(null);\n\n try {\n // Confirm the payment (payment method already attached by backend)\n // This will show 3D Secure popup if needed\n const { paymentIntent, error: confirmError } =\n await stripeRef.current.confirmCardPayment(clientSecret);\n\n if (confirmError) {\n throw new Error(confirmError.message || 'Payment confirmation failed');\n }\n\n if (paymentIntent?.status === 'succeeded') {\n setPaymentState('success');\n onSuccess(paymentIntent.id);\n } else if (paymentIntent?.status === 'processing') {\n setPaymentState('processing');\n // Keep in processing state - webhook will handle completion\n } else {\n throw new Error('Payment was not successful');\n }\n } catch (err) {\n const errorMessage = err instanceof Error ? err.message : 'Payment confirmation failed';\n setError(errorMessage);\n setPaymentState('error');\n onError(errorMessage);\n }\n };\n\n confirmExistingPayment();\n }, [\n isCompletingExistingPayment,\n sdkLoaded,\n publishableKey,\n clientSecret,\n paymentState,\n onSuccess,\n onError,\n ]);\n\n const handleSubmit = useCallback(\n async (e: React.FormEvent) => {\n e.preventDefault();\n\n const stripe = stripeRef.current;\n const cardElement = cardElementInstanceRef.current;\n\n if (!stripe || !cardElement) {\n setError('Payment form not ready');\n return;\n }\n\n if (!cardComplete) {\n setCardError('Please enter complete card details');\n return;\n }\n\n setPaymentState('processing');\n setError(null);\n setCardError(null);\n\n try {\n // Step 1: Create PaymentMethod\n const { paymentMethod, error: pmError } = await stripe.createPaymentMethod({\n type: 'card',\n card: cardElement,\n });\n\n if (pmError) {\n throw new Error(pmError.message || 'Failed to create payment method');\n }\n\n if (!paymentMethod) {\n throw new Error('No payment method returned');\n }\n\n // Step 2: Confirm the payment with the client secret\n const { paymentIntent, error: confirmError } = await stripe.confirmCardPayment(\n clientSecret,\n { payment_method: paymentMethod.id }\n );\n\n if (confirmError) {\n throw new Error(confirmError.message || 'Payment failed');\n }\n\n if (paymentIntent?.status === 'succeeded') {\n setPaymentState('success');\n onSuccess(paymentIntent.id);\n } else if (paymentIntent?.status === 'processing') {\n setPaymentState('processing');\n } else {\n throw new Error('Payment was not successful');\n }\n } catch (err) {\n const errorMessage = err instanceof Error ? err.message : 'Payment failed';\n setError(errorMessage);\n setPaymentState('error');\n onError(errorMessage);\n }\n },\n [clientSecret, cardComplete, onSuccess, onError]\n );\n\n // Render success state\n if (paymentState === 'success') {\n return (\n <div\n className={`p-6 rounded-lg border ${statusTokens.success.border} ${statusTokens.success.bg}`}\n >\n <div className=\"flex items-center gap-3\">\n <div\n className={`p-2 rounded-full ${statusTokens.success.bg} ${statusTokens.success.icon}`}\n >\n <Check className=\"size-6\" />\n </div>\n <div>\n <h3 className={`font-semibold ${statusTokens.success.text}`}>Payment Successful!</h3>\n <p className={`text-sm ${statusTokens.success.text}`}>\n Your payment has been processed successfully.\n </p>\n </div>\n </div>\n </div>\n );\n }\n\n // Show loading while SDK loads\n if (!sdkLoaded) {\n return (\n <div className=\"space-y-4\">\n <div className=\"flex items-center gap-2\">\n <CreditCard className=\"size-5 text-text-secondary\" />\n <span className=\"font-medium text-text-primary\">\n {tr('billing.checkout.cardPayment', 'Card Payment')}\n </span>\n </div>\n <div className=\"p-6 rounded-lg border border-border-subtle bg-bg-sunken flex items-center justify-center\">\n <Loader2 className=\"size-6 animate-spin text-text-secondary\" />\n <span className=\"ml-2 text-text-secondary\">\n {tr('billing.checkout.loadingPaymentForm', 'Loading payment form...')}\n </span>\n </div>\n </div>\n );\n }\n\n // For saved card payments that need confirmation (3D Secure, etc.)\n if (isCompletingExistingPayment) {\n return (\n <div className=\"space-y-4\">\n <div className=\"flex items-center gap-2\">\n <Lock className=\"size-5 text-text-link\" />\n <span className=\"font-medium text-text-primary\">Secure Payment Verification</span>\n </div>\n\n {paymentState === 'processing' && (\n <div className=\"p-6 rounded-lg border border-border-subtle bg-bg-sunken flex flex-col items-center justify-center space-y-3\">\n <Loader2 className=\"size-8 animate-spin text-text-link\" />\n <p className=\"text-sm text-text-secondary text-center\">\n Confirming payment with your saved card…\n <br />\n <span className=\"text-xs\">Please complete any security verification if prompted</span>\n </p>\n </div>\n )}\n\n {error && (\n <div className=\"flex items-center gap-2 p-3 rounded-lg bg-status-error-bg-subtle text-status-error-text text-sm\">\n <AlertCircle className=\"size-4 shrink-0\" />\n <div>\n <p className=\"font-medium\">\n {tr('billing.checkout.paymentFailed', 'Payment Failed')}\n </p>\n <p className=\"text-xs\">{error}</p>\n </div>\n </div>\n )}\n\n {(paymentState === 'error' || error) && onCancel && (\n <div className=\"flex gap-3\">\n <button\n type=\"button\"\n onClick={onCancel}\n className=\"flex-1 px-4 py-3 rounded-lg border border-border-subtle text-text-primary font-medium\n hover:bg-bg-sunken transition-colors\"\n >\n Back to Checkout\n </button>\n </div>\n )}\n </div>\n );\n }\n\n return (\n <form onSubmit={handleSubmit} className=\"space-y-4\">\n {/* Header */}\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-2\">\n <CreditCard className=\"size-5 text-text-secondary\" />\n <span className=\"font-medium text-text-primary\">Card Payment</span>\n </div>\n <div className=\"flex items-center gap-1 text-xs text-text-secondary\">\n <Lock className=\"size-3\" />\n Secured by Stripe\n </div>\n </div>\n\n {/* Stripe Card Element container - NO conditional rendering inside */}\n <div className=\"space-y-2\">\n <div\n ref={cardElementRef}\n className=\"min-h-[50px] rounded-lg border border-border-subtle bg-bg-surface p-4\"\n />\n {cardError && <p className=\"text-sm text-status-error-text\">{cardError}</p>}\n </div>\n\n {/* Error message */}\n {error && (\n <div className=\"flex items-center gap-2 p-3 rounded-lg bg-status-error-bg-subtle text-status-error-text text-sm\">\n <AlertCircle className=\"size-4 shrink-0\" />\n {error}\n </div>\n )}\n\n {/* Actions */}\n <div className=\"flex gap-3\">\n {onCancel && (\n <button\n type=\"button\"\n onClick={onCancel}\n disabled={paymentState === 'processing' || disabled}\n className=\"flex-1 px-4 py-3 rounded-lg border border-border-subtle text-text-primary font-medium\n hover:bg-bg-sunken disabled:opacity-50 disabled:cursor-not-allowed transition-colors\"\n >\n Cancel\n </button>\n )}\n <button\n type=\"submit\"\n disabled={paymentState === 'processing' || disabled || !cardComplete}\n className=\"flex-1 px-4 py-3 rounded-lg bg-action-primary-bg text-action-primary-text font-medium\n hover:bg-action-primary-bgHover disabled:opacity-50 disabled:cursor-not-allowed transition-colors\n flex items-center justify-center gap-2\"\n >\n {paymentState === 'processing' ? (\n <>\n <Loader2 className=\"size-4 animate-spin\" />\n Processing…\n </>\n ) : (\n <>\n Pay{' '}\n {new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: currency.toUpperCase(),\n }).format(amount / 100)}\n </>\n )}\n </button>\n </div>\n\n {/* Security note */}\n <p className=\"text-center text-xs text-text-secondary\">\n Your payment information is encrypted and secure. We never store your full card details.\n </p>\n </form>\n );\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"SubscriptionSelector.js","names":[],"sources":["../../../../../src/billing/modules/checkout/components/SubscriptionSelector.tsx"],"sourcesContent":["/**\n * Subscription Selector Component\n * Compact inline dropdown for choosing subscription when purchasing addons\n * Only shows when multiple subscriptions exist - single subscription auto-selects silently\n */\n\nimport { type FC, useState, useRef, useEffect } from 'react';\nimport { ChevronDown, Check, AlertCircle } from 'lucide-react';\nimport { statusTokens } from '../../../shared/utils/tokens';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\n\ninterface Subscription {\n id: string;\n status: string;\n startDate: string;\n endDate: string;\n plan?: {\n id: string;\n name: string;\n productID?: string;\n product?: {\n id: string;\n name: string;\n } | null;\n } | null;\n}\n\ninterface SubscriptionSelectorProps {\n subscriptions: Subscription[];\n selectedSubscriptionId: string | null;\n onSelect: (subscriptionId: string | null) => void;\n isLoading?: boolean;\n}\n\nexport const SubscriptionSelector: FC<SubscriptionSelectorProps> = ({\n subscriptions,\n selectedSubscriptionId,\n onSelect,\n isLoading = false,\n}) => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n const [isOpen, setIsOpen] = useState(false);\n const dropdownRef = useRef<HTMLDivElement>(null);\n\n // Close dropdown when clicking outside\n useEffect(() => {\n const handleClickOutside = (event: MouseEvent) => {\n if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {\n setIsOpen(false);\n }\n };\n\n document.addEventListener('mousedown', handleClickOutside);\n return () => document.removeEventListener('mousedown', handleClickOutside);\n }, []);\n\n // Auto-select if only one subscription (silently, no UI shown)\n useEffect(() => {\n if (subscriptions.length === 1 && !selectedSubscriptionId) {\n onSelect(subscriptions[0].id);\n }\n }, [subscriptions, selectedSubscriptionId, onSelect]);\n\n const selectedSubscription = subscriptions.find((s) => s.id === selectedSubscriptionId);\n\n // Format subscription display name: \"Product - Plan\" or just \"Plan\"\n const getDisplayName = (subscription: Subscription) => {\n const planName = subscription.plan?.name || 'Subscription';\n const productName = subscription.plan?.product?.name;\n return productName ? `${productName} - ${planName}` : planName;\n };\n\n // Loading state - compact\n if (isLoading) {\n return <div className=\"h-9 bg-bg-sunken animate-pulse rounded-button\" />;\n }\n\n // No subscriptions - compact warning\n if (subscriptions.length === 0) {\n return (\n <div\n className={`flex items-center gap-2 p-2 rounded-button ${statusTokens.warning.bg} border ${statusTokens.warning.border}`}\n >\n <AlertCircle className={`size-4 ${statusTokens.warning.icon} shrink-0`} />\n <span className={`text-sm ${statusTokens.warning.text}`}>\n No active subscription. Subscribe to a plan first.\n </span>\n </div>\n );\n }\n\n // Only one subscription - don't show anything, auto-select happens silently\n if (subscriptions.length === 1) {\n return null;\n }\n\n // Multiple subscriptions - compact dropdown\n return (\n <div className=\"relative\" ref={dropdownRef}>\n <div className=\"flex items-center gap-2\">\n <span className=\"text-sm text-text-secondary shrink-0\">\n {tr('billing.checkout.attachTo', 'Attach to:')}\n </span>\n <button\n type=\"button\"\n onClick={() => setIsOpen(!isOpen)}\n className={`\n flex-1 flex items-center justify-between gap-2 px-3 py-1.5 rounded-button border text-sm\n transition-colors text-left\n ${\n isOpen\n ? 'border-border-strong ring-1 ring-[var(--color-focus-ring)]'\n : selectedSubscriptionId\n ? 'border-border-subtle hover:border-border-strong bg-bg-surface'\n : `${statusTokens.warning.border} ${statusTokens.warning.bg}`\n }\n `}\n >\n {selectedSubscription ? (\n <span className=\"font-medium text-text-primary truncate\">\n {getDisplayName(selectedSubscription)}\n </span>\n ) : (\n <span className={statusTokens.warning.text}>\n {tr('billing.checkout.selectSubscription', 'Select subscription')}\n </span>\n )}\n <ChevronDown\n className={`size-3.5 shrink-0 text-text-secondary transition-transform ${\n isOpen ? 'rotate-180' : ''\n }`}\n />\n </button>\n </div>\n\n {/* Dropdown menu */}\n {isOpen && (\n <div className=\"absolute left-0 right-0 z-50 mt-1 max-h-48 overflow-auto rounded-card border border-border-seam bg-bg-elevated shadow-elevation-3\">\n {subscriptions.map((subscription) => {\n const isSelected = subscription.id === selectedSubscriptionId;\n return (\n <button\n key={subscription.id}\n type=\"button\"\n onClick={() => {\n onSelect(subscription.id);\n setIsOpen(false);\n }}\n className={`\n w-full flex items-center justify-between gap-2 px-3 py-2 text-sm text-left\n transition-colors\n ${isSelected ? 'bg-[var(--color-accent-soft)]' : 'hover:bg-bg-sunken'}\n `}\n >\n <span className=\"font-medium text-text-primary truncate\">\n {getDisplayName(subscription)}\n </span>\n {isSelected && <Check className=\"size-3.5 text-text-link shrink-0\" />}\n </button>\n );\n })}\n </div>\n )}\n </div>\n );\n};\n"],"mappings":";;;;;;AAkCA,IAAa,KAAuD,EAClE,kBACA,2BACA,aACA,eAAY,SACR;CACJ,IAAM,EAAE,SAAM,GAAS,EACjB,KAAM,GAAa,MAA6B;EACpD,IAAM,IAAa,EAAE,EAAI;AACzB,SAAO,MAAe,IAAM,IAAW;IAEnC,CAAC,GAAQ,KAAa,EAAS,GAAM,EACrC,IAAc,EAAuB,KAAK;AAehD,CAZA,QAAgB;EACd,IAAM,KAAsB,MAAsB;AAChD,GAAI,EAAY,WAAW,CAAC,EAAY,QAAQ,SAAS,EAAM,OAAe,IAC5E,EAAU,GAAM;;AAKpB,SADA,SAAS,iBAAiB,aAAa,EAAmB,QAC7C,SAAS,oBAAoB,aAAa,EAAmB;IACzE,EAAE,CAAC,EAGN,QAAgB;AACd,EAAI,EAAc,WAAW,KAAK,CAAC,KACjC,EAAS,EAAc,GAAG,GAAG;IAE9B;EAAC;EAAe;EAAwB;EAAS,CAAC;CAErD,IAAM,IAAuB,EAAc,MAAM,MAAM,EAAE,OAAO,EAAuB,EAGjF,KAAkB,MAA+B;EACrD,IAAM,IAAW,EAAa,MAAM,QAAQ,gBACtC,IAAc,EAAa,MAAM,SAAS;AAChD,SAAO,IAAc,GAAG,EAAY,KAAK,MAAa;;AA4BxD,QAxBI,IACK,kBAAC,OAAD,EAAK,WAAU,iDAAkD,CAAA,GAItE,EAAc,WAAW,IAEzB,kBAAC,OAAD;EACE,WAAW,8CAA8C,EAAa,QAAQ,GAAG,UAAU,EAAa,QAAQ;YADlH,CAGE,kBAAC,GAAD,EAAa,WAAW,UAAU,EAAa,QAAQ,KAAK,YAAc,CAAA,EAC1E,kBAAC,QAAD;GAAM,WAAW,WAAW,EAAa,QAAQ;aAAQ;GAElD,CAAA,CACH;MAKN,EAAc,WAAW,IACpB,OAKP,kBAAC,OAAD;EAAK,WAAU;EAAW,KAAK;YAA/B,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,QAAD;IAAM,WAAU;cACb,EAAG,6BAA6B,aAAa;IACzC,CAAA,EACP,kBAAC,UAAD;IACE,MAAK;IACL,eAAe,EAAU,CAAC,EAAO;IACjC,WAAW;;;cAIP,IACI,+DACA,IACE,kEACA,GAAG,EAAa,QAAQ,OAAO,GAAG,EAAa,QAAQ,KAC9D;;cAZL,CAeG,IACC,kBAAC,QAAD;KAAM,WAAU;eACb,EAAe,EAAqB;KAChC,CAAA,GAEP,kBAAC,QAAD;KAAM,WAAW,EAAa,QAAQ;eACnC,EAAG,uCAAuC,sBAAsB;KAC5D,CAAA,EAET,kBAAC,GAAD,EACE,WAAW,8DACT,IAAS,eAAe,MAE1B,CAAA,CACK;MACL;MAGL,KACC,kBAAC,OAAD;GAAK,WAAU;aACZ,EAAc,KAAK,MAAiB;IACnC,IAAM,IAAa,EAAa,OAAO;AACvC,WACE,kBAAC,UAAD;KAEE,MAAK;KACL,eAAe;AAEb,MADA,EAAS,EAAa,GAAG,EACzB,EAAU,GAAM;;KAElB,WAAW;;;oBAGP,IAAa,kCAAkC,qBAAqB;;eAV1E,CAaE,kBAAC,QAAD;MAAM,WAAU;gBACb,EAAe,EAAa;MACxB,CAAA,EACN,KAAc,kBAAC,GAAD,EAAO,WAAU,oCAAqC,CAAA,CAC9D;OAhBF,EAAa,GAgBX;KAEX;GACE,CAAA,CAEJ"}
1
+ {"version":3,"file":"SubscriptionSelector.js","names":[],"sources":["../../../../../src/billing/modules/checkout/components/SubscriptionSelector.tsx"],"sourcesContent":["/**\n * Subscription Selector Component\n * Compact inline dropdown for choosing subscription when purchasing addons\n * Only shows when multiple subscriptions exist - single subscription auto-selects silently\n */\n\nimport { type FC, useState, useRef, useEffect } from 'react';\nimport { ChevronDown, Check, AlertCircle } from 'lucide-react';\nimport { statusTokens } from '../../../shared/utils/tokens';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\n\ninterface Subscription {\n id: string;\n status: string;\n startDate: string;\n endDate: string;\n plan?: {\n id: string;\n name: string;\n productID?: string;\n product?: {\n id: string;\n name: string;\n } | null;\n } | null;\n}\n\ninterface SubscriptionSelectorProps {\n subscriptions: Subscription[];\n selectedSubscriptionId: string | null;\n onSelect: (subscriptionId: string | null) => void;\n isLoading?: boolean;\n}\n\nexport const SubscriptionSelector: FC<SubscriptionSelectorProps> = ({\n subscriptions,\n selectedSubscriptionId,\n onSelect,\n isLoading = false,\n}) => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n const [isOpen, setIsOpen] = useState(false);\n const dropdownRef = useRef<HTMLDivElement>(null);\n\n // Close dropdown when clicking outside\n useEffect(() => {\n const handleClickOutside = (event: MouseEvent) => {\n if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {\n setIsOpen(false);\n }\n };\n\n document.addEventListener('mousedown', handleClickOutside);\n return () => document.removeEventListener('mousedown', handleClickOutside);\n }, []);\n\n // Auto-select if only one subscription (silently, no UI shown)\n useEffect(() => {\n if (subscriptions.length === 1 && !selectedSubscriptionId) {\n onSelect(subscriptions[0].id);\n }\n }, [subscriptions, selectedSubscriptionId, onSelect]);\n\n const selectedSubscription = subscriptions.find((s) => s.id === selectedSubscriptionId);\n\n // Format subscription display name: \"Product - Plan\" or just \"Plan\"\n const getDisplayName = (subscription: Subscription) => {\n const planName = subscription.plan?.name || 'Subscription';\n const productName = subscription.plan?.product?.name;\n return productName ? `${productName} - ${planName}` : planName;\n };\n\n // Loading state - compact\n if (isLoading) {\n return <div className=\"h-9 bg-bg-sunken animate-pulse rounded-button\" />;\n }\n\n // No subscriptions - compact warning\n if (subscriptions.length === 0) {\n return (\n <div\n className={`flex items-center gap-2 p-2 rounded-button ${statusTokens.warning.bg} border ${statusTokens.warning.border}`}\n >\n <AlertCircle className={`size-4 ${statusTokens.warning.icon} shrink-0`} />\n <span className={`text-sm ${statusTokens.warning.text}`}>\n No active subscription. Subscribe to a plan first.\n </span>\n </div>\n );\n }\n\n // Only one subscription - don't show anything, auto-select happens silently\n if (subscriptions.length === 1) {\n return null;\n }\n\n // Multiple subscriptions - compact dropdown\n return (\n <div className=\"relative\" ref={dropdownRef}>\n <div className=\"flex items-center gap-2\">\n <span className=\"text-sm text-text-secondary shrink-0\">\n {tr('billing.checkout.attachTo', 'Attach to:')}\n </span>\n <button\n type=\"button\"\n onClick={() => setIsOpen(!isOpen)}\n className={`\n flex-1 flex items-center justify-between gap-2 px-3 py-1.5 rounded-button border text-sm\n transition-colors text-left\n ${\n isOpen\n ? 'border-border-strong ring-1 ring-[var(--color-focus-ring)]'\n : selectedSubscriptionId\n ? 'border-border-subtle hover:border-border-strong bg-bg-surface'\n : `${statusTokens.warning.border} ${statusTokens.warning.bg}`\n }\n `}\n >\n {selectedSubscription ? (\n <span className=\"font-medium text-text-primary truncate\">\n {getDisplayName(selectedSubscription)}\n </span>\n ) : (\n <span className={statusTokens.warning.text}>\n {tr('billing.checkout.selectSubscription', 'Select subscription')}\n </span>\n )}\n <ChevronDown\n className={`size-3.5 shrink-0 text-text-secondary transition-transform ${\n isOpen ? 'rotate-180' : ''\n }`}\n />\n </button>\n </div>\n\n {/* Dropdown menu */}\n {isOpen && (\n <div className=\"absolute left-0 right-0 z-50 mt-1 max-h-48 overflow-auto rounded-card border border-border-seam bg-bg-elevated shadow-elevation-3\">\n {subscriptions.map((subscription) => {\n const isSelected = subscription.id === selectedSubscriptionId;\n return (\n <button\n key={subscription.id}\n type=\"button\"\n onClick={() => {\n onSelect(subscription.id);\n setIsOpen(false);\n }}\n className={`\n w-full flex items-center justify-between gap-2 px-3 py-2 text-sm text-left\n transition-colors\n ${isSelected ? 'bg-[var(--color-accent-soft)]' : 'hover:bg-bg-sunken'}\n `}\n >\n <span className=\"font-medium text-text-primary truncate\">\n {getDisplayName(subscription)}\n </span>\n {isSelected && <Check className=\"size-3.5 text-text-link shrink-0\" />}\n </button>\n );\n })}\n </div>\n )}\n </div>\n );\n};\n"],"mappings":";;;;;;AAkCA,IAAa,KAAuD,EAClE,kBACA,2BACA,aACA,eAAY,SACR;CACJ,IAAM,EAAE,SAAM,EAAQ,GAChB,KAAM,GAAa,MAA6B;EACpD,IAAM,IAAa,EAAE,CAAG;EACxB,OAAO,MAAe,IAAM,IAAW;CACzC,GACM,CAAC,GAAQ,KAAa,EAAS,EAAK,GACpC,IAAc,EAAuB,IAAI;CAe/C,AAZA,QAAgB;EACd,IAAM,KAAsB,MAAsB;GAChD,AAAI,EAAY,WAAW,CAAC,EAAY,QAAQ,SAAS,EAAM,MAAc,KAC3E,EAAU,EAAK;EAEnB;EAGA,OADA,SAAS,iBAAiB,aAAa,CAAkB,SAC5C,SAAS,oBAAoB,aAAa,CAAkB;CAC3E,GAAG,CAAC,CAAC,GAGL,QAAgB;EACd,AAAI,EAAc,WAAW,KAAK,CAAC,KACjC,EAAS,EAAc,GAAG,EAAE;CAEhC,GAAG;EAAC;EAAe;EAAwB;CAAQ,CAAC;CAEpD,IAAM,IAAuB,EAAc,MAAM,MAAM,EAAE,OAAO,CAAsB,GAGhF,KAAkB,MAA+B;EACrD,IAAM,IAAW,EAAa,MAAM,QAAQ,gBACtC,IAAc,EAAa,MAAM,SAAS;EAChD,OAAO,IAAc,GAAG,EAAY,KAAK,MAAa;CACxD;CA2BA,OAxBI,IACK,kBAAC,OAAD,EAAK,WAAU,gDAAiD,CAAA,IAIrE,EAAc,WAAW,IAEzB,kBAAC,OAAD;EACE,WAAW,8CAA8C,EAAa,QAAQ,GAAG,UAAU,EAAa,QAAQ;YADlH,CAGE,kBAAC,GAAD,EAAa,WAAW,UAAU,EAAa,QAAQ,KAAK,WAAa,CAAA,GACzE,kBAAC,QAAD;GAAM,WAAW,WAAW,EAAa,QAAQ;aAAQ;EAEnD,CAAA,CACH;MAKL,EAAc,WAAW,IACpB,OAKP,kBAAC,OAAD;EAAK,WAAU;EAAW,KAAK;YAA/B,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,QAAD;IAAM,WAAU;cACb,EAAG,6BAA6B,YAAY;GACzC,CAAA,GACN,kBAAC,UAAD;IACE,MAAK;IACL,eAAe,EAAU,CAAC,CAAM;IAChC,WAAW;;;cAIP,IACI,+DACA,IACE,kEACA,GAAG,EAAa,QAAQ,OAAO,GAAG,EAAa,QAAQ,KAC9D;;cAZL,CAeG,IACC,kBAAC,QAAD;KAAM,WAAU;eACb,EAAe,CAAoB;IAChC,CAAA,IAEN,kBAAC,QAAD;KAAM,WAAW,EAAa,QAAQ;eACnC,EAAG,uCAAuC,qBAAqB;IAC5D,CAAA,GAER,kBAAC,GAAD,EACE,WAAW,8DACT,IAAS,eAAe,KAE3B,CAAA,CACK;KACL;MAGJ,KACC,kBAAC,OAAD;GAAK,WAAU;aACZ,EAAc,KAAK,MAAiB;IACnC,IAAM,IAAa,EAAa,OAAO;IACvC,OACE,kBAAC,UAAD;KAEE,MAAK;KACL,eAAe;MAEb,AADA,EAAS,EAAa,EAAE,GACxB,EAAU,EAAK;KACjB;KACA,WAAW;;;oBAGP,IAAa,kCAAkC,qBAAqB;;eAV1E,CAaE,kBAAC,QAAD;MAAM,WAAU;gBACb,EAAe,CAAY;KACxB,CAAA,GACL,KAAc,kBAAC,GAAD,EAAO,WAAU,mCAAoC,CAAA,CAC9D;OAhBD,EAAa,EAgBZ;GAEZ,CAAC;EACE,CAAA,CAEJ;;AAET"}