@burdenoff/microfe-billing 2026.913.1 → 2026.914.1
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.
- package/dist/billing/BillingAdminRoutes.js +84 -71
- package/dist/billing/BillingAdminRoutes.js.map +1 -1
- package/dist/billing/modules/billing/hooks/useRefunds.js.map +1 -1
- package/dist/billing/modules/billing/pages/PaymentMethodsPage.js +90 -87
- package/dist/billing/modules/billing/pages/PaymentMethodsPage.js.map +1 -1
- package/dist/billing/modules/billing/pages/RefundIssueLogsPage.js +97 -58
- package/dist/billing/modules/billing/pages/RefundIssueLogsPage.js.map +1 -1
- package/dist/billing/modules/checkout/components/OrderSummary.js +22 -19
- package/dist/billing/modules/checkout/components/OrderSummary.js.map +1 -1
- package/dist/billing/modules/checkout/components/RazorpayPaymentButton.js.map +1 -1
- package/dist/billing/modules/checkout/hooks/useCheckout.js +174 -157
- package/dist/billing/modules/checkout/hooks/useCheckout.js.map +1 -1
- package/dist/billing/modules/checkout/pages/CheckoutPage.js +189 -182
- package/dist/billing/modules/checkout/pages/CheckoutPage.js.map +1 -1
- package/dist/billing/modules/credits/pages/CreditWithdrawalPage.js +293 -274
- package/dist/billing/modules/credits/pages/CreditWithdrawalPage.js.map +1 -1
- package/dist/billing/modules/dashboard/components/AddPaymentMethodModal.js +140 -250
- package/dist/billing/modules/dashboard/components/AddPaymentMethodModal.js.map +1 -1
- package/dist/billing/modules/earnings/api.js +79 -42
- package/dist/billing/modules/earnings/api.js.map +1 -1
- package/dist/billing/modules/earnings/index.js +4 -3
- package/dist/billing/modules/earnings/pages/DeveloperRevenueAdminPage.js +103 -310
- package/dist/billing/modules/earnings/pages/DeveloperRevenueAdminPage.js.map +1 -1
- package/dist/billing/modules/earnings/pages/DeveloperRevenueOrgDetailPage.js +545 -0
- package/dist/billing/modules/earnings/pages/DeveloperRevenueOrgDetailPage.js.map +1 -0
- package/dist/billing/modules/earnings/pages/PayoutAdminPage.js +246 -196
- package/dist/billing/modules/earnings/pages/PayoutAdminPage.js.map +1 -1
- package/dist/billing/modules/subscriptions/hooks/useSubscriptions.js +44 -28
- package/dist/billing/modules/subscriptions/hooks/useSubscriptions.js.map +1 -1
- package/dist/billing/modules/subscriptions/pages/SubscriptionDetailPage.js +317 -270
- package/dist/billing/modules/subscriptions/pages/SubscriptionDetailPage.js.map +1 -1
- package/dist/billing/modules/subscriptions/pages/SubscriptionsListPage.js +153 -150
- package/dist/billing/modules/subscriptions/pages/SubscriptionsListPage.js.map +1 -1
- package/dist/billing/modules/subscriptions/pages/UpgradeSubscriptionPage.js +1 -1
- package/dist/billing/modules/subscriptions/pages/UpgradeSubscriptionPage.js.map +1 -1
- package/dist/billing/modules/usage/hooks/useUsage.js +74 -48
- package/dist/billing/modules/usage/hooks/useUsage.js.map +1 -1
- package/dist/generated/global-operations.js +220 -183
- package/dist/generated/global-operations.js.map +1 -1
- package/dist/generated/global-types.js.map +1 -1
- package/dist/node_modules/sonner/dist/index.js +163 -0
- package/dist/node_modules/sonner/dist/index.js.map +1 -0
- package/package.json +1 -1
|
@@ -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 && 'value' in feature.limits && (\n <div className=\"mt-1 flex flex-wrap gap-1.5\">\n <span className=\"inline-flex items-center text-xs px-1.5 py-0.5 rounded bg-bg-sunken text-text-secondary\">\n {formatLimitValue('value', feature.limits.value)}\n </span>\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 — credits purchases don't support coupons yet (BOFF-7192) */}\n {discountAmount > 0 && !showInCredits && (\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
|
+
{"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\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 /**\n * Total credits this order will actually charge — computed by useCheckout\n * (subtotalUSD × CREDITS_PER_USD, with subtotalUSD properly currency-converted\n * for non-USD-native items). Required whenever showInCredits is true.\n *\n * This component must NOT recompute credits itself: it used to fall back to\n * `item.priceUSD ?? item.price`, and for a non-USD-native item (e.g. a\n * store order priced in INR with no priceUSD) that silently treated the raw\n * native-currency price as if it were already USD — a ₹349 item showed as\n * \"34,900 Credits\" instead of ~365 (BOFF-7192, live-reported: the checkout\n * page's \"Insufficient credits\" gate was fixed in useCheckout, but this\n * component's own independent, duplicate — and still-broken — copy of the\n * same fallback kept showing the wrong figure in the Order Summary itself).\n */\n creditsRequired?: number;\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 creditsRequired = 0,\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 // Every item in one checkout shares the same native currency, so a\n // native-currency ratio is safe to apply directly to the (already correctly\n // USD-converted) aggregate creditsRequired — no per-item currency\n // conversion needed here, and no risk of the priceUSD-fallback bug.\n const nativeSubtotal = useMemo(\n () => items.reduce((sum, item) => sum + item.price * item.quantity, 0),\n [items]\n );\n\n const creditsForAmount = (nativeAmount: number) =>\n nativeSubtotal > 0 ? creditsRequired * (nativeAmount / nativeSubtotal) : 0;\n\n // Format amount - show in credits if paying with credits, otherwise in selected currency\n const formatAmount = (amount: number) => {\n if (showInCredits) {\n const creditsAmount = creditsForAmount(amount);\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 && 'value' in feature.limits && (\n <div className=\"mt-1 flex flex-wrap gap-1.5\">\n <span className=\"inline-flex items-center text-xs px-1.5 py-0.5 rounded bg-bg-sunken text-text-secondary\">\n {formatLimitValue('value', feature.limits.value)}\n </span>\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)}\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\n ? `${creditsRequired.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 0 })} Credits`\n : formatAmount(subtotal)}\n </span>\n </div>\n\n {/* Discount — credits purchases don't support coupons yet (BOFF-7192) */}\n {discountAmount > 0 && !showInCredits && (\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 {creditsRequired.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":"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\ndeclare global {\n interface Window {\n Razorpay?: new (options: RazorpayCheckoutOptions) => CheckoutRazorpayInstance;\n }\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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"}
|