@burdenoff/microfe-billing 2026.1027.3 → 2026.1030.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.
Files changed (23) hide show
  1. package/dist/billing/modules/addons/hooks/index.js +1 -0
  2. package/dist/billing/modules/addons/hooks/useAddonPurchaseEligibility.js +27 -0
  3. package/dist/billing/modules/addons/hooks/useAddonPurchaseEligibility.js.map +1 -0
  4. package/dist/billing/modules/addons/index.js +1 -0
  5. package/dist/billing/modules/addons/pages/AddonBrowseDetailPage.js +124 -108
  6. package/dist/billing/modules/addons/pages/AddonBrowseDetailPage.js.map +1 -1
  7. package/dist/billing/modules/addons/pages/AddonEditPage.js +172 -151
  8. package/dist/billing/modules/addons/pages/AddonEditPage.js.map +1 -1
  9. package/dist/billing/modules/addons/pages/AddonsBrowsePage.js +296 -260
  10. package/dist/billing/modules/addons/pages/AddonsBrowsePage.js.map +1 -1
  11. package/dist/billing/modules/usage/components/WorkspaceAiSpendPanel.js +152 -0
  12. package/dist/billing/modules/usage/components/WorkspaceAiSpendPanel.js.map +1 -0
  13. package/dist/billing/modules/usage/hooks/useWorkspaceAiSpend.js +44 -0
  14. package/dist/billing/modules/usage/hooks/useWorkspaceAiSpend.js.map +1 -0
  15. package/dist/billing/modules/usage/pages/UsagePage.js +399 -395
  16. package/dist/billing/modules/usage/pages/UsagePage.js.map +1 -1
  17. package/dist/billing/modules/usage/utils/aiSpend.js +78 -0
  18. package/dist/billing/modules/usage/utils/aiSpend.js.map +1 -0
  19. package/dist/billing/shared/types/graphql.js.map +1 -1
  20. package/dist/generated/global-operations.js +335 -311
  21. package/dist/generated/global-operations.js.map +1 -1
  22. package/dist/generated/global-types.js.map +1 -1
  23. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"AddonsBrowsePage.js","names":[],"sources":["../../../../../src/billing/modules/addons/pages/AddonsBrowsePage.tsx"],"sourcesContent":["/**\n * Addons Module - Addons Browse Page (User-facing)\n * Browse available addons and add to cart. No create/edit/toggle actions.\n */\n\nimport { useState, useMemo, type FC } from 'react';\nimport { createPortal } from 'react-dom';\nimport { ASSISTANT_BOTTOM_CHROME_CLEARANCE } from '@burdenoff/fe-libs/shared/assistant';\nimport { useDefaultBillingCurrency } from '../../../hooks/useDefaultBillingCurrency';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { useBilling } from '../../../providers/BillingProvider';\nimport { ShoppingCart, Plus, Minus, Trash2, X, Package } from 'lucide-react';\nimport { useAddons } from '../hooks';\nimport { useAddonCartStore } from '../store/addonCartStore';\nimport {\n formatCurrency,\n formatPlanDuration,\n formatPlanDurationLabel,\n} from '../../../shared/utils/format';\nimport type { Addon, PlanDuration, SubscriptionFeatures } from '../../../shared/types';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { PagePurpose, IllustratedEmptyState } from '@burdenoff/fe-libs/ui';\nimport { getPlanFeatureQuantity } from '../../../shared/utils/planFeatureQuantity';\n\ntype ViewMode = 'grid' | 'list';\n\n// ---------------------------------------------------------------------------\n// Grouping logic\n// Addons named \"Group — Variant\" (em dash) are grouped together.\n// Addons with a unique prefix or no em dash are shown standalone.\n// ---------------------------------------------------------------------------\n\ntype AddonGroupItem =\n | { type: 'group'; name: string; addons: Addon[] }\n | { type: 'standalone'; addon: Addon };\n\nfunction groupAddons(addons: Addon[]): AddonGroupItem[] {\n const groups = new Map<string, Addon[]>();\n for (const addon of addons) {\n const sepIdx = addon.name.indexOf(' — ');\n const key = sepIdx !== -1 ? addon.name.slice(0, sepIdx).trim() : addon.name;\n const bucket = groups.get(key) ?? [];\n bucket.push(addon);\n groups.set(key, bucket);\n }\n\n const items: AddonGroupItem[] = [];\n for (const [name, groupAddons] of groups) {\n if (groupAddons.length > 1) {\n items.push({ type: 'group', name, addons: groupAddons });\n } else {\n items.push({ type: 'standalone', addon: groupAddons[0] });\n }\n }\n return items;\n}\n\ninterface CombinedQuota {\n quotaId: string;\n name: string;\n totalValue: number;\n}\n\nfunction combineQuotas(features: SubscriptionFeatures[]): CombinedQuota[] {\n const quotaMap = new Map<string, CombinedQuota>();\n for (const feature of features) {\n if (!feature.quota) continue;\n const quotaId = feature.quota.id;\n const limitValue = getPlanFeatureQuantity(feature);\n const existing = quotaMap.get(quotaId);\n if (existing) {\n existing.totalValue += limitValue;\n } else {\n quotaMap.set(quotaId, {\n quotaId,\n name: feature.quota.name ?? 'Unknown Quota',\n totalValue: limitValue,\n });\n }\n }\n return Array.from(quotaMap.values());\n}\n\nfunction getAddonDisplayPrice(\n addon: Addon,\n displayCurrency: string = 'USD'\n): { price: number; currency: string } {\n if (addon.currencyPrices && typeof addon.currencyPrices === 'object') {\n const currencyPricesObj = addon.currencyPrices as Record<string, number>;\n if (\n currencyPricesObj[displayCurrency] !== undefined &&\n currencyPricesObj[displayCurrency] !== null\n ) {\n return { price: currencyPricesObj[displayCurrency], currency: displayCurrency };\n }\n }\n return { price: addon.price, currency: addon.currency || 'USD' };\n}\n\nexport const AddonsBrowsePage: FC = () => {\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 navigateTo = useBillingNavigate();\n const { productId } = useBilling();\n const { addons, isLoading, error, refetch } = useAddons({\n includeFree: false,\n productId: productId ?? undefined,\n });\n const { addToCart, removeFromCart, updateQuantity, isInCart, getCartItem } = useAddonCartStore();\n\n const [viewMode, setViewMode] = useState<ViewMode>('grid');\n const [billingInterval, setBillingInterval] = useState<PlanDuration | 'all'>('all');\n const displayCurrency = useDefaultBillingCurrency();\n\n // Only show active addons to users\n const filteredAddons = addons.filter((addon) => {\n if (!addon.isActive) return false;\n if (billingInterval !== 'all' && addon.duration !== billingInterval) return false;\n return true;\n });\n\n const groupedItems = useMemo(() => groupAddons(filteredAddons), [filteredAddons]);\n\n if (isLoading && addons.length === 0) {\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"h-8 w-48 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"grid grid-cols-1 md:grid-cols-3 gap-6\">\n {[1, 2, 3].map((i) => (\n <div key={i} className=\"border border-border-subtle rounded-lg p-6 space-y-4\">\n <div className=\"h-6 w-24 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"h-8 w-32 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"space-y-2\">\n {[1, 2, 3].map((j) => (\n <div key={j} className=\"h-4 w-full bg-bg-sunken animate-pulse rounded\" />\n ))}\n </div>\n </div>\n ))}\n </div>\n </div>\n );\n }\n\n if (error) {\n return (\n <div className=\"flex items-center justify-center h-full min-h-[400px]\">\n <div className=\"text-center space-y-4\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.addons.failedToLoad', 'Failed to load addons')}\n </h2>\n <p className=\"text-sm text-text-secondary\">{error.message}</p>\n <button\n type=\"button\"\n onClick={() => refetch()}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90 transition-colors\"\n >\n Try Again\n </button>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6 p-6\">\n {/* Header */}\n <div>\n <h1 className=\"text-2xl font-bold text-text-primary\">\n {tr('billing.addons.title', 'Addons')}\n </h1>\n <p className=\"text-sm text-text-secondary mt-1\">\n {tr('billing.addons.subtitle', 'Enhance your subscription with additional features')}\n </p>\n <PagePurpose className=\"mt-3\">\n {tr(\n 'billing.addons.browsePurpose',\n 'Add extra capacity or capabilities on top of your current plan — like more seats, storage or usage credits — without upgrading your whole subscription. Add the add-ons you want to your cart and check out when ready.'\n )}\n </PagePurpose>\n </div>\n\n {/* Filters */}\n <div className=\"flex flex-wrap items-center gap-4 pb-4 border-b border-border-subtle\">\n <div className=\"inline-flex rounded-lg border border-border-subtle p-1 bg-bg-sunken/50\">\n {(['all', 'monthly', 'yearly'] as const).map((interval) => (\n <button\n type=\"button\"\n key={interval}\n onClick={() => setBillingInterval(interval as PlanDuration | 'all')}\n className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${\n billingInterval === interval\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-secondary hover:text-text-primary'\n }`}\n >\n {interval === 'all' ? 'All' : interval === 'monthly' ? 'Monthly' : 'Yearly'}\n </button>\n ))}\n </div>\n\n <div className=\"ml-auto inline-flex rounded-lg border border-border-subtle p-1 bg-bg-sunken/50\">\n <button\n type=\"button\"\n onClick={() => setViewMode('grid')}\n className={`p-1.5 rounded-md transition-colors ${\n viewMode === 'grid'\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-secondary hover:text-text-primary'\n }`}\n title=\"Grid view\"\n >\n <svg className=\"size-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z\"\n />\n </svg>\n </button>\n <button\n type=\"button\"\n onClick={() => setViewMode('list')}\n className={`p-1.5 rounded-md transition-colors ${\n viewMode === 'list'\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-secondary hover:text-text-primary'\n }`}\n title=\"List view\"\n >\n <svg className=\"size-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M4 6h16M4 12h16M4 18h16\"\n />\n </svg>\n </button>\n </div>\n </div>\n\n {/* Addons */}\n {filteredAddons.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={tr('billing.addons.noAddonsAvailable', 'No addons available')}\n description={tr('billing.addons.checkBackLater', 'Check back later for new addons.')}\n />\n ) : (\n <div className=\"space-y-8\">\n {groupedItems.map((item) => {\n if (item.type === 'group') {\n return (\n <AddonGroupSection\n key={item.name}\n groupName={item.name}\n addons={item.addons}\n viewMode={viewMode}\n getCartItem={getCartItem}\n isInCart={isInCart}\n addToCart={addToCart}\n updateQuantity={updateQuantity}\n removeFromCart={removeFromCart}\n displayCurrency={displayCurrency}\n navigateTo={navigateTo}\n />\n );\n }\n const addon = item.addon;\n const cartItem = getCartItem(addon.id);\n return viewMode === 'grid' ? (\n <BrowseAddonCard\n key={addon.id}\n addon={addon}\n onView={() => navigateTo(`/addons/${addon.id}`)}\n isInCart={isInCart(addon.id)}\n cartQuantity={cartItem?.quantity || 0}\n onAddToCart={addToCart}\n onUpdateQuantity={updateQuantity}\n onRemoveFromCart={removeFromCart}\n displayCurrency={displayCurrency}\n />\n ) : (\n <BrowseAddonRow\n key={addon.id}\n addon={addon}\n onView={() => navigateTo(`/addons/${addon.id}`)}\n isInCart={isInCart(addon.id)}\n cartQuantity={cartItem?.quantity || 0}\n onAddToCart={addToCart}\n onUpdateQuantity={updateQuantity}\n onRemoveFromCart={removeFromCart}\n displayCurrency={displayCurrency}\n />\n );\n })}\n </div>\n )}\n\n <FloatingCartDrawer displayCurrency={displayCurrency} />\n </div>\n );\n};\n\n// ============================================================================\n// Addon Group Section\n// ============================================================================\n\ninterface AddonGroupSectionProps {\n groupName: string;\n addons: Addon[];\n viewMode: ViewMode;\n getCartItem: (id: string) => { quantity: number } | undefined;\n isInCart: (id: string) => boolean;\n addToCart: (addon: Addon) => void;\n updateQuantity: (id: string, qty: number) => void;\n removeFromCart: (id: string) => void;\n displayCurrency: string;\n navigateTo: (path: string) => void;\n}\n\nconst AddonGroupSection: FC<AddonGroupSectionProps> = ({\n groupName,\n addons,\n viewMode,\n getCartItem,\n isInCart,\n addToCart,\n updateQuantity,\n removeFromCart,\n displayCurrency,\n navigateTo,\n}) => {\n const cartCount = addons.filter((a) => isInCart(a.id)).length;\n\n return (\n <div className=\"border border-border-subtle rounded-xl overflow-hidden\">\n {/* Group header */}\n <div className=\"flex items-center justify-between px-5 py-3 bg-bg-sunken/40 border-b border-border-subtle\">\n <div className=\"flex items-center gap-2\">\n <Package className=\"size-4 text-text-secondary\" />\n <h2 className=\"font-semibold text-text-primary\">{groupName}</h2>\n <span className=\"text-xs text-text-secondary\">\n {addons.length} option{addons.length !== 1 ? 's' : ''}\n </span>\n </div>\n {cartCount > 0 && (\n <span className=\"text-xs font-medium px-2 py-0.5 bg-action-primary-bg text-action-primary-text rounded-full\">\n {cartCount} in cart\n </span>\n )}\n </div>\n\n {/* Variants */}\n {viewMode === 'grid' ? (\n <div className=\"p-4 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4\">\n {addons.map((addon) => {\n const cartItem = getCartItem(addon.id);\n const variantName = addon.name.includes(' — ')\n ? addon.name.slice(addon.name.indexOf(' — ') + 3).trim()\n : addon.name;\n const inCart = isInCart(addon.id);\n const qty = cartItem?.quantity ?? 0;\n const { price, currency } = (() => {\n if (addon.currencyPrices && typeof addon.currencyPrices === 'object') {\n const prices = addon.currencyPrices as Record<string, number>;\n if (prices[displayCurrency] != null)\n return { price: prices[displayCurrency], currency: displayCurrency };\n }\n return { price: addon.price, currency: addon.currency || 'USD' };\n })();\n\n return (\n <div\n key={addon.id}\n className={`relative flex flex-col rounded-lg border p-4 transition-all ${\n inCart\n ? 'border-primary bg-action-primary-bg/5'\n : 'border-border-subtle bg-bg-surface hover:border-primary/40 hover:shadow-sm'\n }`}\n >\n {inCart && (\n <span className=\"absolute top-2 right-2 text-xs font-medium bg-action-primary-bg text-action-primary-text px-1.5 py-0.5 rounded\">\n ×{qty}\n </span>\n )}\n <p className=\"font-semibold text-text-primary text-sm mb-1\">{variantName}</p>\n <p className=\"text-xs text-text-secondary mb-3\">\n {formatPlanDurationLabel(addon.duration)}\n </p>\n <p className=\"text-lg font-bold text-text-primary mb-3\">\n {formatCurrency(price, currency)}\n <span className=\"text-xs font-normal text-text-secondary ml-1\">\n / {formatPlanDuration(addon.duration)}\n </span>\n </p>\n <div className=\"mt-auto flex flex-col gap-2\">\n {inCart ? (\n <div className=\"flex items-center gap-1\">\n <button\n type=\"button\"\n onClick={() => updateQuantity(addon.id, qty - 1)}\n className=\"p-1 rounded border border-border-subtle hover:bg-bg-sunken transition-colors\"\n >\n <Minus className=\"size-3\" />\n </button>\n <span className=\"flex-1 text-center text-sm font-medium\">{qty}</span>\n <button\n type=\"button\"\n onClick={() => updateQuantity(addon.id, qty + 1)}\n className=\"p-1 rounded border border-border-subtle hover:bg-bg-sunken transition-colors\"\n >\n <Plus className=\"size-3\" />\n </button>\n <button\n type=\"button\"\n onClick={() => removeFromCart(addon.id)}\n className=\"p-1 rounded text-status-error-text hover:bg-status-error-bg/10 transition-colors\"\n >\n <Trash2 className=\"size-3\" />\n </button>\n </div>\n ) : (\n <button\n type=\"button\"\n onClick={() => addToCart(addon)}\n className=\"w-full py-1.5 text-xs font-medium bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90 transition-colors flex items-center justify-center gap-1\"\n >\n <ShoppingCart className=\"size-3\" />\n Add to Cart\n </button>\n )}\n <button\n type=\"button\"\n onClick={() => navigateTo(`/addons/${addon.id}`)}\n className=\"w-full py-1.5 text-xs font-medium border border-border-subtle text-text-primary rounded-md hover:bg-bg-sunken transition-colors\"\n >\n Details\n </button>\n </div>\n </div>\n );\n })}\n </div>\n ) : (\n <div className=\"divide-y divide-border\">\n {addons.map((addon) => {\n const cartItem = getCartItem(addon.id);\n const variantName = addon.name.includes(' — ')\n ? addon.name.slice(addon.name.indexOf(' — ') + 3).trim()\n : addon.name;\n const inCart = isInCart(addon.id);\n const qty = cartItem?.quantity ?? 0;\n const { price, currency } = (() => {\n if (addon.currencyPrices && typeof addon.currencyPrices === 'object') {\n const prices = addon.currencyPrices as Record<string, number>;\n if (prices[displayCurrency] != null)\n return { price: prices[displayCurrency], currency: displayCurrency };\n }\n return { price: addon.price, currency: addon.currency || 'USD' };\n })();\n\n return (\n <div\n key={addon.id}\n className={`flex items-center gap-4 px-5 py-3 transition-colors ${\n inCart ? 'bg-action-primary-bg/5' : 'hover:bg-bg-sunken/30'\n }`}\n >\n <div className=\"flex-1 min-w-0\">\n <p className=\"font-medium text-text-primary text-sm\">{variantName}</p>\n <p className=\"text-xs text-text-secondary\">\n {formatPlanDurationLabel(addon.duration)}\n </p>\n </div>\n <p className=\"font-semibold text-text-primary text-sm whitespace-nowrap\">\n {formatCurrency(price, currency)}\n <span className=\"text-xs font-normal text-text-secondary ml-1\">\n / {formatPlanDuration(addon.duration)}\n </span>\n </p>\n {inCart ? (\n <div className=\"flex items-center gap-1\">\n <button\n type=\"button\"\n onClick={() => updateQuantity(addon.id, qty - 1)}\n className=\"p-1 rounded border border-border-subtle hover:bg-bg-sunken transition-colors\"\n >\n <Minus className=\"size-3\" />\n </button>\n <span className=\"w-6 text-center text-sm font-medium\">{qty}</span>\n <button\n type=\"button\"\n onClick={() => updateQuantity(addon.id, qty + 1)}\n className=\"p-1 rounded border border-border-subtle hover:bg-bg-sunken transition-colors\"\n >\n <Plus className=\"size-3\" />\n </button>\n <button\n type=\"button\"\n onClick={() => removeFromCart(addon.id)}\n className=\"p-1 rounded text-status-error-text hover:bg-status-error-bg/10 transition-colors\"\n >\n <Trash2 className=\"size-3\" />\n </button>\n </div>\n ) : (\n <button\n type=\"button\"\n onClick={() => addToCart(addon)}\n className=\"px-2 py-1 text-xs font-medium bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90 transition-colors flex items-center gap-1\"\n >\n <ShoppingCart className=\"size-3\" />\n Add\n </button>\n )}\n <button\n type=\"button\"\n onClick={() => navigateTo(`/addons/${addon.id}`)}\n className=\"px-2 py-1 text-xs font-medium border border-border-subtle text-text-primary rounded-md hover:bg-bg-sunken transition-colors\"\n >\n Details\n </button>\n </div>\n );\n })}\n </div>\n )}\n </div>\n );\n};\n\n// ============================================================================\n// Browse Addon Card (no edit/toggle)\n// ============================================================================\n\ninterface BrowseAddonCardProps {\n addon: Addon;\n onView: () => void;\n isInCart: boolean;\n cartQuantity: number;\n onAddToCart: (addon: Addon) => void;\n onUpdateQuantity: (addonId: string, quantity: number) => void;\n onRemoveFromCart: (addonId: string) => void;\n displayCurrency: string;\n}\n\nconst BrowseAddonCard: FC<BrowseAddonCardProps> = ({\n addon,\n onView,\n isInCart,\n cartQuantity,\n onAddToCart,\n onUpdateQuantity,\n onRemoveFromCart,\n displayCurrency,\n}) => {\n const combinedQuotas = useMemo(() => combineQuotas(addon.features || []), [addon.features]);\n\n return (\n <div\n className={`relative border rounded-lg p-6 transition-all hover:shadow-md flex flex-col h-full ${\n isInCart ? 'border-primary bg-action-primary-bg/5' : 'border-border-subtle bg-bg-surface'\n }`}\n >\n {isInCart && (\n <span className=\"absolute top-4 right-4 px-2 py-1 text-xs font-medium bg-action-primary-bg text-action-primary-text rounded\">\n In Cart ({cartQuantity})\n </span>\n )}\n\n <div className=\"size-10 rounded-lg bg-action-primary-bg/10 flex items-center justify-center mb-4\">\n <svg className=\"size-5 text-primary\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4\"\n />\n </svg>\n </div>\n\n <div className=\"mb-4\">\n <h3 className=\"text-lg font-bold text-text-primary\">{addon.name}</h3>\n <span className=\"text-sm text-text-secondary\">\n {formatPlanDurationLabel(addon.duration)}\n </span>\n </div>\n\n <div className=\"flex items-baseline gap-1 mb-4\">\n <span className=\"text-2xl font-bold text-text-primary\">\n {(() => {\n const { price, currency } = getAddonDisplayPrice(addon, displayCurrency);\n return formatCurrency(price, currency);\n })()}\n </span>\n <span className=\"text-text-secondary text-sm\">/ {formatPlanDuration(addon.duration)}</span>\n </div>\n\n {combinedQuotas.length > 0 && (\n <ul className=\"space-y-2 mb-4\">\n {combinedQuotas.slice(0, 4).map((quota) => (\n <li key={quota.quotaId} className=\"flex items-center justify-between text-sm\">\n <div className=\"flex items-center gap-2\">\n <svg\n className=\"size-4 text-status-success-text shrink-0\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M5 13l4 4L19 7\"\n />\n </svg>\n <span className=\"text-text-primary\">{quota.name}</span>\n </div>\n <span className=\"font-medium text-text-primary\">\n {quota.totalValue.toLocaleString()}\n </span>\n </li>\n ))}\n {combinedQuotas.length > 4 && (\n <li className=\"text-sm text-text-secondary pl-6\">\n +{combinedQuotas.length - 4} more quotas\n </li>\n )}\n </ul>\n )}\n\n <div className=\"space-y-2 pt-4 border-t border-border-subtle mt-auto\">\n {isInCart ? (\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"flex items-center gap-1\">\n <button\n type=\"button\"\n onClick={() => onUpdateQuantity(addon.id, cartQuantity - 1)}\n className=\"p-1.5 rounded border border-border-subtle hover:bg-bg-sunken transition-colors\"\n >\n <Minus className=\"size-4\" />\n </button>\n <span className=\"w-10 text-center font-medium\">{cartQuantity}</span>\n <button\n type=\"button\"\n onClick={() => onUpdateQuantity(addon.id, cartQuantity + 1)}\n className=\"p-1.5 rounded border border-border-subtle hover:bg-bg-sunken transition-colors\"\n >\n <Plus className=\"size-4\" />\n </button>\n </div>\n <button\n type=\"button\"\n onClick={() => onRemoveFromCart(addon.id)}\n className=\"p-1.5 rounded text-status-error-text hover:bg-status-error-bg/10 transition-colors\"\n title=\"Remove from cart\"\n >\n <Trash2 className=\"size-4\" />\n </button>\n </div>\n ) : (\n <button\n type=\"button\"\n onClick={() => onAddToCart(addon)}\n className=\"w-full px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90 transition-colors flex items-center justify-center gap-2\"\n >\n <ShoppingCart className=\"size-4\" />\n Add to Cart\n </button>\n )}\n <button\n type=\"button\"\n onClick={onView}\n className=\"w-full px-4 py-2 text-sm font-medium border border-border-subtle text-text-primary rounded-md hover:bg-bg-sunken transition-colors\"\n >\n View Details\n </button>\n </div>\n </div>\n );\n};\n\n// ============================================================================\n// Browse Addon Row (no edit/toggle)\n// ============================================================================\n\ninterface BrowseAddonRowProps {\n addon: Addon;\n onView: () => void;\n isInCart: boolean;\n cartQuantity: number;\n onAddToCart: (addon: Addon) => void;\n onUpdateQuantity: (addonId: string, quantity: number) => void;\n onRemoveFromCart: (addonId: string) => void;\n displayCurrency: string;\n}\n\nconst BrowseAddonRow: FC<BrowseAddonRowProps> = ({\n addon,\n onView,\n isInCart,\n cartQuantity,\n onAddToCart,\n onUpdateQuantity,\n onRemoveFromCart,\n displayCurrency,\n}) => {\n const combinedQuotas = useMemo(() => combineQuotas(addon.features || []), [addon.features]);\n\n return (\n <div\n className={`flex items-center gap-4 p-4 border rounded-lg transition-all hover:shadow-sm ${\n isInCart ? 'border-primary bg-action-primary-bg/5' : 'border-border-subtle bg-bg-surface'\n }`}\n >\n <div className=\"size-10 rounded-lg bg-action-primary-bg/10 flex items-center justify-center shrink-0\">\n <svg className=\"size-5 text-primary\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4\"\n />\n </svg>\n </div>\n\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2\">\n <h3 className=\"font-semibold text-text-primary truncate\">{addon.name}</h3>\n {isInCart && (\n <span className=\"px-2 py-0.5 text-xs font-medium bg-action-primary-bg text-action-primary-text rounded\">\n In Cart ({cartQuantity})\n </span>\n )}\n </div>\n <p className=\"text-sm text-text-secondary\">\n {combinedQuotas.length} quota{combinedQuotas.length !== 1 ? 's' : ''}\n </p>\n </div>\n\n <div className=\"text-right\">\n <div className=\"font-semibold text-text-primary\">\n {(() => {\n const { price, currency } = getAddonDisplayPrice(addon, displayCurrency);\n return formatCurrency(price, currency);\n })()}\n </div>\n <div className=\"text-sm text-text-secondary\">per {formatPlanDuration(addon.duration)}</div>\n </div>\n\n {isInCart ? (\n <div className=\"flex items-center gap-1\">\n <button\n type=\"button\"\n onClick={() => onUpdateQuantity(addon.id, cartQuantity - 1)}\n className=\"p-1.5 rounded border border-border-subtle hover:bg-bg-sunken transition-colors\"\n >\n <Minus className=\"size-4\" />\n </button>\n <span className=\"w-8 text-center font-medium\">{cartQuantity}</span>\n <button\n type=\"button\"\n onClick={() => onUpdateQuantity(addon.id, cartQuantity + 1)}\n className=\"p-1.5 rounded border border-border-subtle hover:bg-bg-sunken transition-colors\"\n >\n <Plus className=\"size-4\" />\n </button>\n <button\n type=\"button\"\n onClick={() => onRemoveFromCart(addon.id)}\n className=\"p-1.5 rounded text-status-error-text hover:bg-status-error-bg/10 transition-colors ml-1\"\n title=\"Remove from cart\"\n >\n <Trash2 className=\"size-4\" />\n </button>\n </div>\n ) : (\n <button\n type=\"button\"\n onClick={() => onAddToCart(addon)}\n className=\"px-3 py-1.5 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90 transition-colors flex items-center gap-1.5\"\n >\n <ShoppingCart className=\"size-4\" />\n Add\n </button>\n )}\n\n <button\n type=\"button\"\n onClick={onView}\n className=\"px-3 py-1.5 text-sm font-medium border border-border-subtle text-text-primary rounded-md hover:bg-bg-sunken transition-colors\"\n >\n View\n </button>\n </div>\n );\n};\n\n// ============================================================================\n// Floating Cart Drawer\n// ============================================================================\n\nconst FloatingCartDrawer: FC<{ displayCurrency: string }> = ({ displayCurrency }) => {\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 navigateTo = useBillingNavigate();\n const { items, isCartOpen, toggleCart, closeCart, removeFromCart, updateQuantity, clearCart } =\n useAddonCartStore();\n\n const itemCount = items.reduce((sum, item) => sum + item.quantity, 0);\n\n if (items.length === 0 && !isCartOpen) return null;\n\n return (\n <>\n {!isCartOpen &&\n items.length > 0 &&\n createPortal(\n // Portaled to <body>, not rendered inside the page tree: on the\n // installed app, NativePageTransition's route wrapper animates with\n // a CSS transform, which makes any descendant `position: fixed`\n // element fixed to THAT transformed ancestor instead of the\n // viewport — the chip scrolled with the page and drifted on every\n // navigation. The assistant launcher never had this bug because\n // it already mounts at the AppShell root, outside that wrapper;\n // this gives the chip the same real viewport anchoring.\n //\n // Bottom-left on phone widths only (the assistant launcher owns\n // bottom-right there, BOFF-9854) so the two floating controls never\n // compete for the same corner; unchanged bottom-right at `sm:` and\n // up, matching this page's own desktop/mobile split (see the cart\n // drawer modal below) and preserving desktop's existing layout.\n // The bottom offset reuses the assistant launcher's own tab-bar /\n // safe-area clearance formula, which resolves to the same plain\n // 1.5rem on desktop (no tab bar, no safe-area inset there).\n <button\n type=\"button\"\n onClick={toggleCart}\n style={{ bottom: `calc(1.5rem + ${ASSISTANT_BOTTOM_CHROME_CLEARANCE})` }}\n className=\"fixed left-6 sm:left-auto sm:right-6 z-50 flex items-center gap-2 px-4 py-3 bg-action-primary-bg text-action-primary-text rounded-full shadow-lg hover:bg-action-primary-bg/90 transition-all\"\n >\n <ShoppingCart className=\"size-5\" />\n <span className=\"font-medium\">\n {itemCount} item{itemCount !== 1 ? 's' : ''}\n </span>\n <span className=\"px-2 py-0.5 bg-primary-foreground/20 rounded-full text-sm\">\n {(() => {\n const total = items.reduce((sum, { addon, quantity }) => {\n const { price } = getAddonDisplayPrice(addon, displayCurrency);\n return sum + price * quantity;\n }, 0);\n return formatCurrency(total, displayCurrency);\n })()}\n </span>\n </button>,\n document.body\n )}\n\n {isCartOpen && (\n <div className=\"fixed inset-0 z-50 flex items-end sm:items-center justify-center\">\n <div className=\"absolute inset-0 bg-overlay-scrim\" onClick={closeCart} />\n <div className=\"relative w-full max-w-md max-h-[80vh] bg-bg-surface border border-border-subtle rounded-t-xl sm:rounded-xl shadow-xl overflow-hidden\">\n <div className=\"flex items-center justify-between px-4 py-3 border-b border-border-subtle bg-bg-sunken/30\">\n <div className=\"flex items-center gap-2\">\n <ShoppingCart className=\"size-5 text-text-primary\" />\n <h3 className=\"font-semibold text-text-primary\">\n {tr('billing.addons.yourCart', 'Your Cart')}\n </h3>\n <span className=\"px-2 py-0.5 text-xs bg-action-primary-bg text-action-primary-text rounded-full\">\n {itemCount} item{itemCount !== 1 ? 's' : ''}\n </span>\n </div>\n <button\n type=\"button\"\n onClick={closeCart}\n className=\"p-1.5 rounded hover:bg-bg-sunken transition-colors\"\n >\n <X className=\"size-5\" />\n </button>\n </div>\n\n <div className=\"overflow-y-auto max-h-[50vh] divide-y divide-border\">\n {items.map(({ addon, quantity }) => (\n <div key={addon.id} className=\"p-4 flex items-center gap-3\">\n <div className=\"flex-1 min-w-0\">\n <h4 className=\"font-medium text-text-primary truncate\">{addon.name}</h4>\n <p className=\"text-sm text-text-secondary\">\n {(() => {\n const { price, currency } = getAddonDisplayPrice(addon, displayCurrency);\n return formatCurrency(price, currency);\n })()}{' '}\n / {formatPlanDuration(addon.duration)}\n </p>\n </div>\n <div className=\"flex items-center gap-1\">\n <button\n type=\"button\"\n onClick={() => updateQuantity(addon.id, quantity - 1)}\n className=\"p-1 rounded border border-border-subtle hover:bg-bg-sunken transition-colors\"\n >\n <Minus className=\"size-3\" />\n </button>\n <span className=\"w-6 text-center text-sm font-medium\">{quantity}</span>\n <button\n type=\"button\"\n onClick={() => updateQuantity(addon.id, quantity + 1)}\n className=\"p-1 rounded border border-border-subtle hover:bg-bg-sunken transition-colors\"\n >\n <Plus className=\"size-3\" />\n </button>\n </div>\n <p className=\"font-medium text-text-primary\">\n {(() => {\n const { price, currency } = getAddonDisplayPrice(addon, displayCurrency);\n return formatCurrency(price * quantity, currency);\n })()}\n </p>\n <button\n type=\"button\"\n onClick={() => removeFromCart(addon.id)}\n className=\"p-1.5 rounded text-status-error-text hover:bg-status-error-bg/10 transition-colors\"\n >\n <Trash2 className=\"size-4\" />\n </button>\n </div>\n ))}\n </div>\n\n <div className=\"border-t border-border-subtle p-4 space-y-3 bg-bg-sunken/20\">\n <div className=\"flex items-center justify-between text-lg font-semibold\">\n <span className=\"text-text-primary\">{tr('billing.addons.total', 'Total')}</span>\n <span className=\"text-text-primary\">\n {(() => {\n const total = items.reduce((sum, { addon, quantity }) => {\n const { price } = getAddonDisplayPrice(addon, displayCurrency);\n return sum + price * quantity;\n }, 0);\n return formatCurrency(total, displayCurrency);\n })()}\n </span>\n </div>\n <div className=\"flex gap-2\">\n <button\n type=\"button\"\n onClick={clearCart}\n className=\"flex-1 px-4 py-2.5 text-sm font-medium border border-border-subtle text-text-primary rounded-md hover:bg-bg-sunken transition-colors\"\n >\n Clear Cart\n </button>\n <button\n type=\"button\"\n onClick={() => {\n closeCart();\n navigateTo('/checkout/addons');\n }}\n className=\"flex-1 px-4 py-2.5 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90 transition-colors\"\n >\n Checkout\n </button>\n </div>\n </div>\n </div>\n </div>\n )}\n </>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"AddonsBrowsePage.js","names":[],"sources":["../../../../../src/billing/modules/addons/pages/AddonsBrowsePage.tsx"],"sourcesContent":["/**\n * Addons Module - Addons Browse Page (User-facing)\n * Browse available addons and add to cart. No create/edit/toggle actions.\n */\n\nimport { useState, useMemo, type FC } from 'react';\nimport { createPortal } from 'react-dom';\nimport { ASSISTANT_BOTTOM_CHROME_CLEARANCE } from '@burdenoff/fe-libs/shared/assistant';\nimport { useDefaultBillingCurrency } from '../../../hooks/useDefaultBillingCurrency';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { useBilling } from '../../../providers/BillingProvider';\nimport { ShoppingCart, Plus, Minus, Trash2, X, Package, Lock } from 'lucide-react';\nimport { useAddons, useAddonPurchaseEligibility } from '../hooks';\nimport { useAddonCartStore } from '../store/addonCartStore';\nimport {\n formatCurrency,\n formatPlanDuration,\n formatPlanDurationLabel,\n} from '../../../shared/utils/format';\nimport type { Addon, PlanDuration, SubscriptionFeatures } from '../../../shared/types';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { PagePurpose, IllustratedEmptyState } from '@burdenoff/fe-libs/ui';\nimport { getPlanFeatureQuantity } from '../../../shared/utils/planFeatureQuantity';\n\ntype ViewMode = 'grid' | 'list';\n\n// ---------------------------------------------------------------------------\n// Grouping logic\n// Addons named \"Group — Variant\" (em dash) are grouped together.\n// Addons with a unique prefix or no em dash are shown standalone.\n// ---------------------------------------------------------------------------\n\ntype AddonGroupItem =\n | { type: 'group'; name: string; addons: Addon[] }\n | { type: 'standalone'; addon: Addon };\n\nfunction groupAddons(addons: Addon[]): AddonGroupItem[] {\n const groups = new Map<string, Addon[]>();\n for (const addon of addons) {\n const sepIdx = addon.name.indexOf(' — ');\n const key = sepIdx !== -1 ? addon.name.slice(0, sepIdx).trim() : addon.name;\n const bucket = groups.get(key) ?? [];\n bucket.push(addon);\n groups.set(key, bucket);\n }\n\n const items: AddonGroupItem[] = [];\n for (const [name, groupAddons] of groups) {\n if (groupAddons.length > 1) {\n items.push({ type: 'group', name, addons: groupAddons });\n } else {\n items.push({ type: 'standalone', addon: groupAddons[0] });\n }\n }\n return items;\n}\n\ninterface CombinedQuota {\n quotaId: string;\n name: string;\n totalValue: number;\n}\n\nfunction combineQuotas(features: SubscriptionFeatures[]): CombinedQuota[] {\n const quotaMap = new Map<string, CombinedQuota>();\n for (const feature of features) {\n if (!feature.quota) continue;\n const quotaId = feature.quota.id;\n const limitValue = getPlanFeatureQuantity(feature);\n const existing = quotaMap.get(quotaId);\n if (existing) {\n existing.totalValue += limitValue;\n } else {\n quotaMap.set(quotaId, {\n quotaId,\n name: feature.quota.name ?? 'Unknown Quota',\n totalValue: limitValue,\n });\n }\n }\n return Array.from(quotaMap.values());\n}\n\nfunction getAddonDisplayPrice(\n addon: Addon,\n displayCurrency: string = 'USD'\n): { price: number; currency: string } {\n if (addon.currencyPrices && typeof addon.currencyPrices === 'object') {\n const currencyPricesObj = addon.currencyPrices as Record<string, number>;\n if (\n currencyPricesObj[displayCurrency] !== undefined &&\n currencyPricesObj[displayCurrency] !== null\n ) {\n return { price: currencyPricesObj[displayCurrency], currency: displayCurrency };\n }\n }\n return { price: addon.price, currency: addon.currency || 'USD' };\n}\n\nexport const AddonsBrowsePage: FC = () => {\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 navigateTo = useBillingNavigate();\n const { productId } = useBilling();\n const { addons, isLoading, error, refetch } = useAddons({\n includeFree: false,\n productId: productId ?? undefined,\n });\n const { addToCart, removeFromCart, updateQuantity, isInCart, getCartItem } = useAddonCartStore();\n\n const [viewMode, setViewMode] = useState<ViewMode>('grid');\n const [billingInterval, setBillingInterval] = useState<PlanDuration | 'all'>('all');\n const displayCurrency = useDefaultBillingCurrency();\n\n // Only show active addons to users\n const filteredAddons = addons.filter((addon) => {\n if (!addon.isActive) return false;\n if (billingInterval !== 'all' && addon.duration !== billingInterval) return false;\n return true;\n });\n\n const groupedItems = useMemo(() => groupAddons(filteredAddons), [filteredAddons]);\n\n // BOFF-9889: add-ons require a paid subscription to their product.\n const browseAddonIds = useMemo(() => filteredAddons.map((addon) => addon.id), [filteredAddons]);\n const { blocked: blockedAddons, isBlocked } = useAddonPurchaseEligibility(browseAddonIds);\n const paidPlanRequiredLabel = tr(\n 'billing.addons.paidPlanRequiredShort',\n 'Add-ons need a paid plan. Upgrade first.'\n );\n\n if (isLoading && addons.length === 0) {\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"h-8 w-48 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"grid grid-cols-1 md:grid-cols-3 gap-6\">\n {[1, 2, 3].map((i) => (\n <div key={i} className=\"border border-border-subtle rounded-lg p-6 space-y-4\">\n <div className=\"h-6 w-24 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"h-8 w-32 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"space-y-2\">\n {[1, 2, 3].map((j) => (\n <div key={j} className=\"h-4 w-full bg-bg-sunken animate-pulse rounded\" />\n ))}\n </div>\n </div>\n ))}\n </div>\n </div>\n );\n }\n\n if (error) {\n return (\n <div className=\"flex items-center justify-center h-full min-h-[400px]\">\n <div className=\"text-center space-y-4\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.addons.failedToLoad', 'Failed to load addons')}\n </h2>\n <p className=\"text-sm text-text-secondary\">{error.message}</p>\n <button\n type=\"button\"\n onClick={() => refetch()}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90 transition-colors\"\n >\n Try Again\n </button>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6 p-6\">\n {/* Header */}\n <div>\n <h1 className=\"text-2xl font-bold text-text-primary\">\n {tr('billing.addons.title', 'Addons')}\n </h1>\n <p className=\"text-sm text-text-secondary mt-1\">\n {tr('billing.addons.subtitle', 'Enhance your subscription with additional features')}\n </p>\n <PagePurpose className=\"mt-3\">\n {tr(\n 'billing.addons.browsePurpose',\n 'Add extra capacity or capabilities on top of your current plan — like more seats, storage or usage credits — without upgrading your whole subscription. Add the add-ons you want to your cart and check out when ready.'\n )}\n </PagePurpose>\n </div>\n\n {blockedAddons.size > 0 && (\n <div\n role=\"status\"\n data-testid=\"addons-paid-plan-required\"\n className=\"flex flex-col gap-3 rounded-lg border border-status-warning-border bg-status-warning-bg-subtle p-4 sm:flex-row sm:items-center sm:justify-between\"\n >\n <div className=\"flex items-start gap-3\">\n <Lock className=\"mt-0.5 size-4 shrink-0 text-status-warning-text\" aria-hidden=\"true\" />\n <div>\n <p className=\"text-sm font-medium text-text-primary\">\n {tr('billing.addons.paidPlanRequiredTitle', 'Add-ons need a paid plan')}\n </p>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {tr(\n 'billing.addons.paidPlanRequiredBody',\n 'Add-ons extend a paid subscription. You are on the Free plan for this product, so upgrade to a paid plan first, then come back to add extra capacity.'\n )}\n </p>\n </div>\n </div>\n <button\n type=\"button\"\n onClick={() => navigateTo('/plans')}\n className=\"shrink-0 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90\"\n >\n {tr('billing.addons.viewPlans', 'View plans')}\n </button>\n </div>\n )}\n\n {/* Filters */}\n <div className=\"flex flex-wrap items-center gap-4 pb-4 border-b border-border-subtle\">\n <div className=\"inline-flex rounded-lg border border-border-subtle p-1 bg-bg-sunken/50\">\n {(['all', 'monthly', 'yearly'] as const).map((interval) => (\n <button\n type=\"button\"\n key={interval}\n onClick={() => setBillingInterval(interval as PlanDuration | 'all')}\n className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${\n billingInterval === interval\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-secondary hover:text-text-primary'\n }`}\n >\n {interval === 'all' ? 'All' : interval === 'monthly' ? 'Monthly' : 'Yearly'}\n </button>\n ))}\n </div>\n\n <div className=\"ml-auto inline-flex rounded-lg border border-border-subtle p-1 bg-bg-sunken/50\">\n <button\n type=\"button\"\n onClick={() => setViewMode('grid')}\n className={`p-1.5 rounded-md transition-colors ${\n viewMode === 'grid'\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-secondary hover:text-text-primary'\n }`}\n title=\"Grid view\"\n >\n <svg className=\"size-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z\"\n />\n </svg>\n </button>\n <button\n type=\"button\"\n onClick={() => setViewMode('list')}\n className={`p-1.5 rounded-md transition-colors ${\n viewMode === 'list'\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-secondary hover:text-text-primary'\n }`}\n title=\"List view\"\n >\n <svg className=\"size-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M4 6h16M4 12h16M4 18h16\"\n />\n </svg>\n </button>\n </div>\n </div>\n\n {/* Addons */}\n {filteredAddons.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={tr('billing.addons.noAddonsAvailable', 'No addons available')}\n description={tr('billing.addons.checkBackLater', 'Check back later for new addons.')}\n />\n ) : (\n <div className=\"space-y-8\">\n {groupedItems.map((item) => {\n if (item.type === 'group') {\n return (\n <AddonGroupSection\n key={item.name}\n groupName={item.name}\n addons={item.addons}\n viewMode={viewMode}\n getCartItem={getCartItem}\n isInCart={isInCart}\n addToCart={addToCart}\n updateQuantity={updateQuantity}\n removeFromCart={removeFromCart}\n displayCurrency={displayCurrency}\n navigateTo={navigateTo}\n isBlocked={isBlocked}\n blockedLabel={paidPlanRequiredLabel}\n />\n );\n }\n const addon = item.addon;\n const cartItem = getCartItem(addon.id);\n return viewMode === 'grid' ? (\n <BrowseAddonCard\n key={addon.id}\n addon={addon}\n onView={() => navigateTo(`/addons/${addon.id}`)}\n isInCart={isInCart(addon.id)}\n cartQuantity={cartItem?.quantity || 0}\n onAddToCart={addToCart}\n onUpdateQuantity={updateQuantity}\n onRemoveFromCart={removeFromCart}\n displayCurrency={displayCurrency}\n purchaseBlocked={isBlocked(addon.id)}\n blockedLabel={paidPlanRequiredLabel}\n />\n ) : (\n <BrowseAddonRow\n key={addon.id}\n addon={addon}\n onView={() => navigateTo(`/addons/${addon.id}`)}\n isInCart={isInCart(addon.id)}\n cartQuantity={cartItem?.quantity || 0}\n onAddToCart={addToCart}\n onUpdateQuantity={updateQuantity}\n onRemoveFromCart={removeFromCart}\n displayCurrency={displayCurrency}\n purchaseBlocked={isBlocked(addon.id)}\n blockedLabel={paidPlanRequiredLabel}\n />\n );\n })}\n </div>\n )}\n\n <FloatingCartDrawer displayCurrency={displayCurrency} />\n </div>\n );\n};\n\n// ============================================================================\n// Addon Group Section\n// ============================================================================\n\ninterface AddonGroupSectionProps {\n groupName: string;\n addons: Addon[];\n viewMode: ViewMode;\n getCartItem: (id: string) => { quantity: number } | undefined;\n isInCart: (id: string) => boolean;\n addToCart: (addon: Addon) => void;\n updateQuantity: (id: string, qty: number) => void;\n removeFromCart: (id: string) => void;\n displayCurrency: string;\n navigateTo: (path: string) => void;\n isBlocked: (addonId: string) => boolean;\n blockedLabel: string;\n}\n\nconst AddonGroupSection: FC<AddonGroupSectionProps> = ({\n groupName,\n addons,\n viewMode,\n getCartItem,\n isInCart,\n addToCart,\n updateQuantity,\n removeFromCart,\n displayCurrency,\n navigateTo,\n isBlocked,\n blockedLabel,\n}) => {\n const cartCount = addons.filter((a) => isInCart(a.id)).length;\n\n return (\n <div className=\"border border-border-subtle rounded-xl overflow-hidden\">\n {/* Group header */}\n <div className=\"flex items-center justify-between px-5 py-3 bg-bg-sunken/40 border-b border-border-subtle\">\n <div className=\"flex items-center gap-2\">\n <Package className=\"size-4 text-text-secondary\" />\n <h2 className=\"font-semibold text-text-primary\">{groupName}</h2>\n <span className=\"text-xs text-text-secondary\">\n {addons.length} option{addons.length !== 1 ? 's' : ''}\n </span>\n </div>\n {cartCount > 0 && (\n <span className=\"text-xs font-medium px-2 py-0.5 bg-action-primary-bg text-action-primary-text rounded-full\">\n {cartCount} in cart\n </span>\n )}\n </div>\n\n {/* Variants */}\n {viewMode === 'grid' ? (\n <div className=\"p-4 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4\">\n {addons.map((addon) => {\n const cartItem = getCartItem(addon.id);\n const variantName = addon.name.includes(' — ')\n ? addon.name.slice(addon.name.indexOf(' — ') + 3).trim()\n : addon.name;\n const inCart = isInCart(addon.id);\n const qty = cartItem?.quantity ?? 0;\n const { price, currency } = (() => {\n if (addon.currencyPrices && typeof addon.currencyPrices === 'object') {\n const prices = addon.currencyPrices as Record<string, number>;\n if (prices[displayCurrency] != null)\n return { price: prices[displayCurrency], currency: displayCurrency };\n }\n return { price: addon.price, currency: addon.currency || 'USD' };\n })();\n\n return (\n <div\n key={addon.id}\n className={`relative flex flex-col rounded-lg border p-4 transition-all ${\n inCart\n ? 'border-primary bg-action-primary-bg/5'\n : 'border-border-subtle bg-bg-surface hover:border-primary/40 hover:shadow-sm'\n }`}\n >\n {inCart && (\n <span className=\"absolute top-2 right-2 text-xs font-medium bg-action-primary-bg text-action-primary-text px-1.5 py-0.5 rounded\">\n ×{qty}\n </span>\n )}\n <p className=\"font-semibold text-text-primary text-sm mb-1\">{variantName}</p>\n <p className=\"text-xs text-text-secondary mb-3\">\n {formatPlanDurationLabel(addon.duration)}\n </p>\n <p className=\"text-lg font-bold text-text-primary mb-3\">\n {formatCurrency(price, currency)}\n <span className=\"text-xs font-normal text-text-secondary ml-1\">\n / {formatPlanDuration(addon.duration)}\n </span>\n </p>\n <div className=\"mt-auto flex flex-col gap-2\">\n {inCart ? (\n <div className=\"flex items-center gap-1\">\n <button\n type=\"button\"\n onClick={() => updateQuantity(addon.id, qty - 1)}\n className=\"p-1 rounded border border-border-subtle hover:bg-bg-sunken transition-colors\"\n >\n <Minus className=\"size-3\" />\n </button>\n <span className=\"flex-1 text-center text-sm font-medium\">{qty}</span>\n <button\n type=\"button\"\n onClick={() => updateQuantity(addon.id, qty + 1)}\n className=\"p-1 rounded border border-border-subtle hover:bg-bg-sunken transition-colors\"\n >\n <Plus className=\"size-3\" />\n </button>\n <button\n type=\"button\"\n onClick={() => removeFromCart(addon.id)}\n className=\"p-1 rounded text-status-error-text hover:bg-status-error-bg/10 transition-colors\"\n >\n <Trash2 className=\"size-3\" />\n </button>\n </div>\n ) : (\n <button\n type=\"button\"\n onClick={() => addToCart(addon)}\n disabled={isBlocked(addon.id)}\n title={isBlocked(addon.id) ? blockedLabel : undefined}\n className=\"w-full py-1.5 text-xs font-medium bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90 transition-colors flex items-center justify-center gap-1 disabled:cursor-not-allowed disabled:opacity-50\"\n >\n <ShoppingCart className=\"size-3\" />\n Add to Cart\n </button>\n )}\n <button\n type=\"button\"\n onClick={() => navigateTo(`/addons/${addon.id}`)}\n className=\"w-full py-1.5 text-xs font-medium border border-border-subtle text-text-primary rounded-md hover:bg-bg-sunken transition-colors\"\n >\n Details\n </button>\n </div>\n </div>\n );\n })}\n </div>\n ) : (\n <div className=\"divide-y divide-border\">\n {addons.map((addon) => {\n const cartItem = getCartItem(addon.id);\n const variantName = addon.name.includes(' — ')\n ? addon.name.slice(addon.name.indexOf(' — ') + 3).trim()\n : addon.name;\n const inCart = isInCart(addon.id);\n const qty = cartItem?.quantity ?? 0;\n const { price, currency } = (() => {\n if (addon.currencyPrices && typeof addon.currencyPrices === 'object') {\n const prices = addon.currencyPrices as Record<string, number>;\n if (prices[displayCurrency] != null)\n return { price: prices[displayCurrency], currency: displayCurrency };\n }\n return { price: addon.price, currency: addon.currency || 'USD' };\n })();\n\n return (\n <div\n key={addon.id}\n className={`flex items-center gap-4 px-5 py-3 transition-colors ${\n inCart ? 'bg-action-primary-bg/5' : 'hover:bg-bg-sunken/30'\n }`}\n >\n <div className=\"flex-1 min-w-0\">\n <p className=\"font-medium text-text-primary text-sm\">{variantName}</p>\n <p className=\"text-xs text-text-secondary\">\n {formatPlanDurationLabel(addon.duration)}\n </p>\n </div>\n <p className=\"font-semibold text-text-primary text-sm whitespace-nowrap\">\n {formatCurrency(price, currency)}\n <span className=\"text-xs font-normal text-text-secondary ml-1\">\n / {formatPlanDuration(addon.duration)}\n </span>\n </p>\n {inCart ? (\n <div className=\"flex items-center gap-1\">\n <button\n type=\"button\"\n onClick={() => updateQuantity(addon.id, qty - 1)}\n className=\"p-1 rounded border border-border-subtle hover:bg-bg-sunken transition-colors\"\n >\n <Minus className=\"size-3\" />\n </button>\n <span className=\"w-6 text-center text-sm font-medium\">{qty}</span>\n <button\n type=\"button\"\n onClick={() => updateQuantity(addon.id, qty + 1)}\n className=\"p-1 rounded border border-border-subtle hover:bg-bg-sunken transition-colors\"\n >\n <Plus className=\"size-3\" />\n </button>\n <button\n type=\"button\"\n onClick={() => removeFromCart(addon.id)}\n className=\"p-1 rounded text-status-error-text hover:bg-status-error-bg/10 transition-colors\"\n >\n <Trash2 className=\"size-3\" />\n </button>\n </div>\n ) : (\n <button\n type=\"button\"\n onClick={() => addToCart(addon)}\n disabled={isBlocked(addon.id)}\n title={isBlocked(addon.id) ? blockedLabel : undefined}\n className=\"px-2 py-1 text-xs font-medium bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90 transition-colors flex items-center gap-1 disabled:cursor-not-allowed disabled:opacity-50\"\n >\n <ShoppingCart className=\"size-3\" />\n Add\n </button>\n )}\n <button\n type=\"button\"\n onClick={() => navigateTo(`/addons/${addon.id}`)}\n className=\"px-2 py-1 text-xs font-medium border border-border-subtle text-text-primary rounded-md hover:bg-bg-sunken transition-colors\"\n >\n Details\n </button>\n </div>\n );\n })}\n </div>\n )}\n </div>\n );\n};\n\n// ============================================================================\n// Browse Addon Card (no edit/toggle)\n// ============================================================================\n\ninterface BrowseAddonCardProps {\n addon: Addon;\n onView: () => void;\n isInCart: boolean;\n cartQuantity: number;\n onAddToCart: (addon: Addon) => void;\n onUpdateQuantity: (addonId: string, quantity: number) => void;\n onRemoveFromCart: (addonId: string) => void;\n displayCurrency: string;\n purchaseBlocked: boolean;\n blockedLabel: string;\n}\n\nconst BrowseAddonCard: FC<BrowseAddonCardProps> = ({\n addon,\n onView,\n isInCart,\n cartQuantity,\n onAddToCart,\n onUpdateQuantity,\n onRemoveFromCart,\n displayCurrency,\n purchaseBlocked,\n blockedLabel,\n}) => {\n const combinedQuotas = useMemo(() => combineQuotas(addon.features || []), [addon.features]);\n\n return (\n <div\n className={`relative border rounded-lg p-6 transition-all hover:shadow-md flex flex-col h-full ${\n isInCart ? 'border-primary bg-action-primary-bg/5' : 'border-border-subtle bg-bg-surface'\n }`}\n >\n {isInCart && (\n <span className=\"absolute top-4 right-4 px-2 py-1 text-xs font-medium bg-action-primary-bg text-action-primary-text rounded\">\n In Cart ({cartQuantity})\n </span>\n )}\n\n <div className=\"size-10 rounded-lg bg-action-primary-bg/10 flex items-center justify-center mb-4\">\n <svg className=\"size-5 text-primary\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4\"\n />\n </svg>\n </div>\n\n <div className=\"mb-4\">\n <h3 className=\"text-lg font-bold text-text-primary\">{addon.name}</h3>\n <span className=\"text-sm text-text-secondary\">\n {formatPlanDurationLabel(addon.duration)}\n </span>\n </div>\n\n <div className=\"flex items-baseline gap-1 mb-4\">\n <span className=\"text-2xl font-bold text-text-primary\">\n {(() => {\n const { price, currency } = getAddonDisplayPrice(addon, displayCurrency);\n return formatCurrency(price, currency);\n })()}\n </span>\n <span className=\"text-text-secondary text-sm\">/ {formatPlanDuration(addon.duration)}</span>\n </div>\n\n {combinedQuotas.length > 0 && (\n <ul className=\"space-y-2 mb-4\">\n {combinedQuotas.slice(0, 4).map((quota) => (\n <li key={quota.quotaId} className=\"flex items-center justify-between text-sm\">\n <div className=\"flex items-center gap-2\">\n <svg\n className=\"size-4 text-status-success-text shrink-0\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M5 13l4 4L19 7\"\n />\n </svg>\n <span className=\"text-text-primary\">{quota.name}</span>\n </div>\n <span className=\"font-medium text-text-primary\">\n {quota.totalValue.toLocaleString()}\n </span>\n </li>\n ))}\n {combinedQuotas.length > 4 && (\n <li className=\"text-sm text-text-secondary pl-6\">\n +{combinedQuotas.length - 4} more quotas\n </li>\n )}\n </ul>\n )}\n\n <div className=\"space-y-2 pt-4 border-t border-border-subtle mt-auto\">\n {isInCart ? (\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"flex items-center gap-1\">\n <button\n type=\"button\"\n onClick={() => onUpdateQuantity(addon.id, cartQuantity - 1)}\n className=\"p-1.5 rounded border border-border-subtle hover:bg-bg-sunken transition-colors\"\n >\n <Minus className=\"size-4\" />\n </button>\n <span className=\"w-10 text-center font-medium\">{cartQuantity}</span>\n <button\n type=\"button\"\n onClick={() => onUpdateQuantity(addon.id, cartQuantity + 1)}\n className=\"p-1.5 rounded border border-border-subtle hover:bg-bg-sunken transition-colors\"\n >\n <Plus className=\"size-4\" />\n </button>\n </div>\n <button\n type=\"button\"\n onClick={() => onRemoveFromCart(addon.id)}\n className=\"p-1.5 rounded text-status-error-text hover:bg-status-error-bg/10 transition-colors\"\n title=\"Remove from cart\"\n >\n <Trash2 className=\"size-4\" />\n </button>\n </div>\n ) : (\n <button\n type=\"button\"\n onClick={() => onAddToCart(addon)}\n disabled={purchaseBlocked}\n title={purchaseBlocked ? blockedLabel : undefined}\n className=\"w-full px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90 transition-colors flex items-center justify-center gap-2 disabled:cursor-not-allowed disabled:opacity-50\"\n >\n {purchaseBlocked ? <Lock className=\"size-4\" /> : <ShoppingCart className=\"size-4\" />}\n Add to Cart\n </button>\n )}\n <button\n type=\"button\"\n onClick={onView}\n className=\"w-full px-4 py-2 text-sm font-medium border border-border-subtle text-text-primary rounded-md hover:bg-bg-sunken transition-colors\"\n >\n View Details\n </button>\n </div>\n </div>\n );\n};\n\n// ============================================================================\n// Browse Addon Row (no edit/toggle)\n// ============================================================================\n\ninterface BrowseAddonRowProps {\n addon: Addon;\n onView: () => void;\n isInCart: boolean;\n cartQuantity: number;\n onAddToCart: (addon: Addon) => void;\n onUpdateQuantity: (addonId: string, quantity: number) => void;\n onRemoveFromCart: (addonId: string) => void;\n displayCurrency: string;\n purchaseBlocked: boolean;\n blockedLabel: string;\n}\n\nconst BrowseAddonRow: FC<BrowseAddonRowProps> = ({\n addon,\n onView,\n isInCart,\n cartQuantity,\n onAddToCart,\n onUpdateQuantity,\n onRemoveFromCart,\n displayCurrency,\n purchaseBlocked,\n blockedLabel,\n}) => {\n const combinedQuotas = useMemo(() => combineQuotas(addon.features || []), [addon.features]);\n\n return (\n <div\n className={`flex items-center gap-4 p-4 border rounded-lg transition-all hover:shadow-sm ${\n isInCart ? 'border-primary bg-action-primary-bg/5' : 'border-border-subtle bg-bg-surface'\n }`}\n >\n <div className=\"size-10 rounded-lg bg-action-primary-bg/10 flex items-center justify-center shrink-0\">\n <svg className=\"size-5 text-primary\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4\"\n />\n </svg>\n </div>\n\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2\">\n <h3 className=\"font-semibold text-text-primary truncate\">{addon.name}</h3>\n {isInCart && (\n <span className=\"px-2 py-0.5 text-xs font-medium bg-action-primary-bg text-action-primary-text rounded\">\n In Cart ({cartQuantity})\n </span>\n )}\n </div>\n <p className=\"text-sm text-text-secondary\">\n {combinedQuotas.length} quota{combinedQuotas.length !== 1 ? 's' : ''}\n </p>\n </div>\n\n <div className=\"text-right\">\n <div className=\"font-semibold text-text-primary\">\n {(() => {\n const { price, currency } = getAddonDisplayPrice(addon, displayCurrency);\n return formatCurrency(price, currency);\n })()}\n </div>\n <div className=\"text-sm text-text-secondary\">per {formatPlanDuration(addon.duration)}</div>\n </div>\n\n {isInCart ? (\n <div className=\"flex items-center gap-1\">\n <button\n type=\"button\"\n onClick={() => onUpdateQuantity(addon.id, cartQuantity - 1)}\n className=\"p-1.5 rounded border border-border-subtle hover:bg-bg-sunken transition-colors\"\n >\n <Minus className=\"size-4\" />\n </button>\n <span className=\"w-8 text-center font-medium\">{cartQuantity}</span>\n <button\n type=\"button\"\n onClick={() => onUpdateQuantity(addon.id, cartQuantity + 1)}\n className=\"p-1.5 rounded border border-border-subtle hover:bg-bg-sunken transition-colors\"\n >\n <Plus className=\"size-4\" />\n </button>\n <button\n type=\"button\"\n onClick={() => onRemoveFromCart(addon.id)}\n className=\"p-1.5 rounded text-status-error-text hover:bg-status-error-bg/10 transition-colors ml-1\"\n title=\"Remove from cart\"\n >\n <Trash2 className=\"size-4\" />\n </button>\n </div>\n ) : (\n <button\n type=\"button\"\n onClick={() => onAddToCart(addon)}\n disabled={purchaseBlocked}\n title={purchaseBlocked ? blockedLabel : undefined}\n className=\"px-3 py-1.5 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90 transition-colors flex items-center gap-1.5 disabled:cursor-not-allowed disabled:opacity-50\"\n >\n {purchaseBlocked ? <Lock className=\"size-4\" /> : <ShoppingCart className=\"size-4\" />}\n Add\n </button>\n )}\n\n <button\n type=\"button\"\n onClick={onView}\n className=\"px-3 py-1.5 text-sm font-medium border border-border-subtle text-text-primary rounded-md hover:bg-bg-sunken transition-colors\"\n >\n View\n </button>\n </div>\n );\n};\n\n// ============================================================================\n// Floating Cart Drawer\n// ============================================================================\n\nconst FloatingCartDrawer: FC<{ displayCurrency: string }> = ({ displayCurrency }) => {\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 navigateTo = useBillingNavigate();\n const { items, isCartOpen, toggleCart, closeCart, removeFromCart, updateQuantity, clearCart } =\n useAddonCartStore();\n\n const itemCount = items.reduce((sum, item) => sum + item.quantity, 0);\n\n if (items.length === 0 && !isCartOpen) return null;\n\n return (\n <>\n {!isCartOpen &&\n items.length > 0 &&\n createPortal(\n // Portaled to <body>, not rendered inside the page tree: on the\n // installed app, NativePageTransition's route wrapper animates with\n // a CSS transform, which makes any descendant `position: fixed`\n // element fixed to THAT transformed ancestor instead of the\n // viewport — the chip scrolled with the page and drifted on every\n // navigation. The assistant launcher never had this bug because\n // it already mounts at the AppShell root, outside that wrapper;\n // this gives the chip the same real viewport anchoring.\n //\n // Bottom-left on phone widths only (the assistant launcher owns\n // bottom-right there, BOFF-9854) so the two floating controls never\n // compete for the same corner; unchanged bottom-right at `sm:` and\n // up, matching this page's own desktop/mobile split (see the cart\n // drawer modal below) and preserving desktop's existing layout.\n // The bottom offset reuses the assistant launcher's own tab-bar /\n // safe-area clearance formula, which resolves to the same plain\n // 1.5rem on desktop (no tab bar, no safe-area inset there).\n <button\n type=\"button\"\n onClick={toggleCart}\n style={{ bottom: `calc(1.5rem + ${ASSISTANT_BOTTOM_CHROME_CLEARANCE})` }}\n className=\"fixed left-6 sm:left-auto sm:right-6 z-50 flex items-center gap-2 px-4 py-3 bg-action-primary-bg text-action-primary-text rounded-full shadow-lg hover:bg-action-primary-bg/90 transition-all\"\n >\n <ShoppingCart className=\"size-5\" />\n <span className=\"font-medium\">\n {itemCount} item{itemCount !== 1 ? 's' : ''}\n </span>\n <span className=\"px-2 py-0.5 bg-primary-foreground/20 rounded-full text-sm\">\n {(() => {\n const total = items.reduce((sum, { addon, quantity }) => {\n const { price } = getAddonDisplayPrice(addon, displayCurrency);\n return sum + price * quantity;\n }, 0);\n return formatCurrency(total, displayCurrency);\n })()}\n </span>\n </button>,\n document.body\n )}\n\n {isCartOpen && (\n <div className=\"fixed inset-0 z-50 flex items-end sm:items-center justify-center\">\n <div className=\"absolute inset-0 bg-overlay-scrim\" onClick={closeCart} />\n <div className=\"relative w-full max-w-md max-h-[80vh] bg-bg-surface border border-border-subtle rounded-t-xl sm:rounded-xl shadow-xl overflow-hidden\">\n <div className=\"flex items-center justify-between px-4 py-3 border-b border-border-subtle bg-bg-sunken/30\">\n <div className=\"flex items-center gap-2\">\n <ShoppingCart className=\"size-5 text-text-primary\" />\n <h3 className=\"font-semibold text-text-primary\">\n {tr('billing.addons.yourCart', 'Your Cart')}\n </h3>\n <span className=\"px-2 py-0.5 text-xs bg-action-primary-bg text-action-primary-text rounded-full\">\n {itemCount} item{itemCount !== 1 ? 's' : ''}\n </span>\n </div>\n <button\n type=\"button\"\n onClick={closeCart}\n className=\"p-1.5 rounded hover:bg-bg-sunken transition-colors\"\n >\n <X className=\"size-5\" />\n </button>\n </div>\n\n <div className=\"overflow-y-auto max-h-[50vh] divide-y divide-border\">\n {items.map(({ addon, quantity }) => (\n <div key={addon.id} className=\"p-4 flex items-center gap-3\">\n <div className=\"flex-1 min-w-0\">\n <h4 className=\"font-medium text-text-primary truncate\">{addon.name}</h4>\n <p className=\"text-sm text-text-secondary\">\n {(() => {\n const { price, currency } = getAddonDisplayPrice(addon, displayCurrency);\n return formatCurrency(price, currency);\n })()}{' '}\n / {formatPlanDuration(addon.duration)}\n </p>\n </div>\n <div className=\"flex items-center gap-1\">\n <button\n type=\"button\"\n onClick={() => updateQuantity(addon.id, quantity - 1)}\n className=\"p-1 rounded border border-border-subtle hover:bg-bg-sunken transition-colors\"\n >\n <Minus className=\"size-3\" />\n </button>\n <span className=\"w-6 text-center text-sm font-medium\">{quantity}</span>\n <button\n type=\"button\"\n onClick={() => updateQuantity(addon.id, quantity + 1)}\n className=\"p-1 rounded border border-border-subtle hover:bg-bg-sunken transition-colors\"\n >\n <Plus className=\"size-3\" />\n </button>\n </div>\n <p className=\"font-medium text-text-primary\">\n {(() => {\n const { price, currency } = getAddonDisplayPrice(addon, displayCurrency);\n return formatCurrency(price * quantity, currency);\n })()}\n </p>\n <button\n type=\"button\"\n onClick={() => removeFromCart(addon.id)}\n className=\"p-1.5 rounded text-status-error-text hover:bg-status-error-bg/10 transition-colors\"\n >\n <Trash2 className=\"size-4\" />\n </button>\n </div>\n ))}\n </div>\n\n <div className=\"border-t border-border-subtle p-4 space-y-3 bg-bg-sunken/20\">\n <div className=\"flex items-center justify-between text-lg font-semibold\">\n <span className=\"text-text-primary\">{tr('billing.addons.total', 'Total')}</span>\n <span className=\"text-text-primary\">\n {(() => {\n const total = items.reduce((sum, { addon, quantity }) => {\n const { price } = getAddonDisplayPrice(addon, displayCurrency);\n return sum + price * quantity;\n }, 0);\n return formatCurrency(total, displayCurrency);\n })()}\n </span>\n </div>\n <div className=\"flex gap-2\">\n <button\n type=\"button\"\n onClick={clearCart}\n className=\"flex-1 px-4 py-2.5 text-sm font-medium border border-border-subtle text-text-primary rounded-md hover:bg-bg-sunken transition-colors\"\n >\n Clear Cart\n </button>\n <button\n type=\"button\"\n onClick={() => {\n closeCart();\n navigateTo('/checkout/addons');\n }}\n className=\"flex-1 px-4 py-2.5 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90 transition-colors\"\n >\n Checkout\n </button>\n </div>\n </div>\n </div>\n </div>\n )}\n </>\n );\n};\n"],"mappings":"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"}
@@ -0,0 +1,152 @@
1
+ import { statusTokens as e } from "../../../shared/utils/tokens.js";
2
+ import { formatNumber as t } from "../../../shared/utils/format.js";
3
+ import { AI_SPEND_WINDOWS as n, formatMicroUsd as r, formatProductKey as i } from "../utils/aiSpend.js";
4
+ import { useWorkspaceAiSpend as a } from "../hooks/useWorkspaceAiSpend.js";
5
+ import { useState as o } from "react";
6
+ import { AlertTriangle as s, RefreshCw as c, Sparkles as l } from "lucide-react";
7
+ import { Fragment as u, jsx as d, jsxs as f } from "react/jsx-runtime";
8
+ //#region src/billing/modules/usage/components/WorkspaceAiSpendPanel.tsx
9
+ var p = ({ workspaceId: p }) => {
10
+ let [m, h] = o("month"), { summary: g, truncated: _, isLoading: v, isForbidden: y, error: b, refetch: x } = a(p, m);
11
+ if (y) return null;
12
+ let S = n.find((e) => e.value === m)?.label ?? "";
13
+ return /* @__PURE__ */ f("section", {
14
+ "aria-labelledby": "workspace-ai-spend-heading",
15
+ "data-testid": "workspace-ai-spend",
16
+ className: "rounded-card border border-border-seam bg-bg-surface shadow-elevation-1 p-4 space-y-3",
17
+ children: [
18
+ /* @__PURE__ */ f("div", {
19
+ className: "flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between",
20
+ children: [/* @__PURE__ */ f("div", {
21
+ className: "flex items-center gap-2",
22
+ children: [/* @__PURE__ */ d("span", {
23
+ className: `flex size-7 items-center justify-center rounded-lg ${e.info.bg}`,
24
+ children: /* @__PURE__ */ d(l, { className: `size-4 ${e.info.icon}` })
25
+ }), /* @__PURE__ */ f("div", { children: [/* @__PURE__ */ d("h3", {
26
+ id: "workspace-ai-spend-heading",
27
+ className: "text-sm font-semibold text-text-primary",
28
+ children: "AI spend"
29
+ }), /* @__PURE__ */ d("p", {
30
+ className: "text-xs text-text-secondary",
31
+ children: "What AI calls cost this workspace, in US dollars."
32
+ })] })]
33
+ }), /* @__PURE__ */ f("div", {
34
+ className: "flex items-center gap-2",
35
+ children: [
36
+ /* @__PURE__ */ d("label", {
37
+ htmlFor: "workspace-ai-spend-window",
38
+ className: "sr-only",
39
+ children: "AI spend period"
40
+ }),
41
+ /* @__PURE__ */ d("select", {
42
+ id: "workspace-ai-spend-window",
43
+ value: m,
44
+ onChange: (e) => h(e.target.value),
45
+ className: "px-2 py-1.5 text-sm border border-border-subtle rounded-button bg-bg-surface text-text-primary",
46
+ children: n.map((e) => /* @__PURE__ */ d("option", {
47
+ value: e.value,
48
+ children: e.label
49
+ }, e.value))
50
+ }),
51
+ /* @__PURE__ */ d("button", {
52
+ type: "button",
53
+ onClick: x,
54
+ className: "p-2 border border-border-subtle rounded-button text-text-secondary hover:text-text-primary hover:bg-bg-sunken transition-colors",
55
+ "aria-label": "Refresh AI spend",
56
+ title: "Refresh AI spend",
57
+ children: /* @__PURE__ */ d(c, { className: "size-4" })
58
+ })
59
+ ]
60
+ })]
61
+ }),
62
+ v && !g && /* @__PURE__ */ d("div", {
63
+ className: "h-16 bg-bg-sunken animate-pulse rounded-card",
64
+ "aria-busy": "true"
65
+ }),
66
+ b && !g && /* @__PURE__ */ f("div", {
67
+ className: "flex items-center gap-2 text-sm text-text-secondary",
68
+ children: [
69
+ /* @__PURE__ */ d(s, { className: `size-4 ${e.error.icon}` }),
70
+ /* @__PURE__ */ d("span", { children: "AI spend could not be loaded." }),
71
+ /* @__PURE__ */ d("button", {
72
+ type: "button",
73
+ onClick: x,
74
+ className: "underline text-text-primary hover:text-text-secondary",
75
+ children: "Retry"
76
+ })
77
+ ]
78
+ }),
79
+ g && /* @__PURE__ */ f(u, { children: [
80
+ /* @__PURE__ */ f("div", { children: [/* @__PURE__ */ d("p", {
81
+ className: "text-2xl font-semibold tabular-nums text-text-primary",
82
+ "data-testid": "workspace-ai-spend-total",
83
+ children: r(g.totalMicroUsd)
84
+ }), /* @__PURE__ */ f("p", {
85
+ className: "text-xs text-text-secondary",
86
+ children: [
87
+ S,
88
+ " (UTC) · ",
89
+ t(g.attempts),
90
+ " AI",
91
+ " ",
92
+ g.attempts === 1 ? "call" : "calls",
93
+ g.unpricedAttempts > 0 && ` · ${t(g.unpricedAttempts)} without a recorded cost`
94
+ ]
95
+ })] }),
96
+ g.products.length === 0 ? /* @__PURE__ */ d("p", {
97
+ className: "text-sm text-text-secondary",
98
+ children: "No AI usage in this period."
99
+ }) : /* @__PURE__ */ d("div", {
100
+ className: "overflow-x-auto",
101
+ children: /* @__PURE__ */ f("table", {
102
+ className: "w-full text-sm",
103
+ children: [/* @__PURE__ */ d("thead", { children: /* @__PURE__ */ f("tr", {
104
+ className: "text-left text-xs text-text-secondary border-b border-border-seam",
105
+ children: [
106
+ /* @__PURE__ */ d("th", {
107
+ scope: "col",
108
+ className: "py-2 pr-4 font-medium",
109
+ children: "Product"
110
+ }),
111
+ /* @__PURE__ */ d("th", {
112
+ scope: "col",
113
+ className: "py-2 pr-4 font-medium text-right",
114
+ children: "AI calls"
115
+ }),
116
+ /* @__PURE__ */ d("th", {
117
+ scope: "col",
118
+ className: "py-2 font-medium text-right",
119
+ children: "Cost (USD)"
120
+ })
121
+ ]
122
+ }) }), /* @__PURE__ */ d("tbody", { children: g.products.map((e) => /* @__PURE__ */ f("tr", {
123
+ className: "border-b border-border-seam last:border-0",
124
+ children: [
125
+ /* @__PURE__ */ d("td", {
126
+ className: "py-2 pr-4 text-text-primary",
127
+ children: i(e.productKey)
128
+ }),
129
+ /* @__PURE__ */ d("td", {
130
+ className: "py-2 pr-4 text-right tabular-nums text-text-secondary",
131
+ children: t(e.attempts)
132
+ }),
133
+ /* @__PURE__ */ d("td", {
134
+ className: "py-2 text-right tabular-nums text-text-primary",
135
+ children: r(e.costMicroUsd)
136
+ })
137
+ ]
138
+ }, e.productKey)) })]
139
+ })
140
+ }),
141
+ /* @__PURE__ */ f("p", {
142
+ className: "text-xs text-text-secondary",
143
+ children: ["Costs come from the workspace's AI ledger at the provider prices recorded for each call. AI credits are charged at USD 0.000003 per credit.", _ && " Only the products with the highest cost are listed."]
144
+ })
145
+ ] })
146
+ ]
147
+ });
148
+ };
149
+ //#endregion
150
+ export { p as WorkspaceAiSpendPanel };
151
+
152
+ //# sourceMappingURL=WorkspaceAiSpendPanel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WorkspaceAiSpendPanel.js","names":[],"sources":["../../../../../src/billing/modules/usage/components/WorkspaceAiSpendPanel.tsx"],"sourcesContent":["/**\n * Workspace AI spend in US dollars (BOFF-10273).\n *\n * Sits in the workspace view of the usage page, above the workspace quotas, so\n * a workspace admin sees what AI cost the workspace in dollars next to the AI\n * credit quota that limits it. Members who may not read the AI ledger see\n * nothing: the gateway refuses the query and the panel stays hidden.\n */\n\nimport { useState, type FC } from 'react';\nimport { AlertTriangle, RefreshCw, Sparkles } from 'lucide-react';\nimport { useWorkspaceAiSpend } from '../hooks/useWorkspaceAiSpend';\nimport {\n AI_SPEND_WINDOWS,\n formatMicroUsd,\n formatProductKey,\n type AiSpendWindow,\n} from '../utils/aiSpend';\nimport { formatNumber } from '../../../shared/utils/format';\nimport { statusTokens } from '../../../shared/utils/tokens';\n\ninterface WorkspaceAiSpendPanelProps {\n workspaceId: string;\n}\n\nexport const WorkspaceAiSpendPanel: FC<WorkspaceAiSpendPanelProps> = ({ workspaceId }) => {\n const [spendWindow, setSpendWindow] = useState<AiSpendWindow>('month');\n const { summary, truncated, isLoading, isForbidden, error, refetch } = useWorkspaceAiSpend(\n workspaceId,\n spendWindow\n );\n\n if (isForbidden) return null;\n\n const windowLabel = AI_SPEND_WINDOWS.find((option) => option.value === spendWindow)?.label ?? '';\n\n return (\n <section\n aria-labelledby=\"workspace-ai-spend-heading\"\n data-testid=\"workspace-ai-spend\"\n className=\"rounded-card border border-border-seam bg-bg-surface shadow-elevation-1 p-4 space-y-3\"\n >\n <div className=\"flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"flex items-center gap-2\">\n <span\n className={`flex size-7 items-center justify-center rounded-lg ${statusTokens.info.bg}`}\n >\n <Sparkles className={`size-4 ${statusTokens.info.icon}`} />\n </span>\n <div>\n <h3 id=\"workspace-ai-spend-heading\" className=\"text-sm font-semibold text-text-primary\">\n AI spend\n </h3>\n <p className=\"text-xs text-text-secondary\">\n What AI calls cost this workspace, in US dollars.\n </p>\n </div>\n </div>\n <div className=\"flex items-center gap-2\">\n <label htmlFor=\"workspace-ai-spend-window\" className=\"sr-only\">\n AI spend period\n </label>\n <select\n id=\"workspace-ai-spend-window\"\n value={spendWindow}\n onChange={(event) => setSpendWindow(event.target.value as AiSpendWindow)}\n className=\"px-2 py-1.5 text-sm border border-border-subtle rounded-button bg-bg-surface text-text-primary\"\n >\n {AI_SPEND_WINDOWS.map((option) => (\n <option key={option.value} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n <button\n type=\"button\"\n onClick={refetch}\n className=\"p-2 border border-border-subtle rounded-button text-text-secondary hover:text-text-primary hover:bg-bg-sunken transition-colors\"\n aria-label=\"Refresh AI spend\"\n title=\"Refresh AI spend\"\n >\n <RefreshCw className=\"size-4\" />\n </button>\n </div>\n </div>\n\n {isLoading && !summary && (\n <div className=\"h-16 bg-bg-sunken animate-pulse rounded-card\" aria-busy=\"true\" />\n )}\n\n {error && !summary && (\n <div className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <AlertTriangle className={`size-4 ${statusTokens.error.icon}`} />\n <span>AI spend could not be loaded.</span>\n <button\n type=\"button\"\n onClick={refetch}\n className=\"underline text-text-primary hover:text-text-secondary\"\n >\n Retry\n </button>\n </div>\n )}\n\n {summary && (\n <>\n <div>\n <p\n className=\"text-2xl font-semibold tabular-nums text-text-primary\"\n data-testid=\"workspace-ai-spend-total\"\n >\n {formatMicroUsd(summary.totalMicroUsd)}\n </p>\n <p className=\"text-xs text-text-secondary\">\n {windowLabel} (UTC) · {formatNumber(summary.attempts)} AI{' '}\n {summary.attempts === 1 ? 'call' : 'calls'}\n {summary.unpricedAttempts > 0 &&\n ` · ${formatNumber(summary.unpricedAttempts)} without a recorded cost`}\n </p>\n </div>\n\n {summary.products.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">No AI usage in this period.</p>\n ) : (\n <div className=\"overflow-x-auto\">\n <table className=\"w-full text-sm\">\n <thead>\n <tr className=\"text-left text-xs text-text-secondary border-b border-border-seam\">\n <th scope=\"col\" className=\"py-2 pr-4 font-medium\">\n Product\n </th>\n <th scope=\"col\" className=\"py-2 pr-4 font-medium text-right\">\n AI calls\n </th>\n <th scope=\"col\" className=\"py-2 font-medium text-right\">\n Cost (USD)\n </th>\n </tr>\n </thead>\n <tbody>\n {summary.products.map((row) => (\n <tr key={row.productKey} className=\"border-b border-border-seam last:border-0\">\n <td className=\"py-2 pr-4 text-text-primary\">\n {formatProductKey(row.productKey)}\n </td>\n <td className=\"py-2 pr-4 text-right tabular-nums text-text-secondary\">\n {formatNumber(row.attempts)}\n </td>\n <td className=\"py-2 text-right tabular-nums text-text-primary\">\n {formatMicroUsd(row.costMicroUsd)}\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n )}\n\n <p className=\"text-xs text-text-secondary\">\n Costs come from the workspace&apos;s AI ledger at the provider prices recorded for each\n call. AI credits are charged at USD 0.000003 per credit.\n {truncated && ' Only the products with the highest cost are listed.'}\n </p>\n </>\n )}\n </section>\n );\n};\n"],"mappings":"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"}
@@ -0,0 +1,44 @@
1
+ import { aiSpendRange as e, isAiSpendPermissionError as t, summarizeAiSpend as n } from "../utils/aiSpend.js";
2
+ import { useMemo as r } from "react";
3
+ import { gql as i, useGatewayClient as a, useQuery as o } from "@burdenoff/fe-libs/shared/graphql";
4
+ //#region src/billing/modules/usage/hooks/useWorkspaceAiSpend.ts
5
+ var s = i`
6
+ query WorkspaceAiSpendByProduct($from: DateTime!, $to: DateTime!) {
7
+ promptUsageBreakdownV2(from: $from, to: $to, groupBy: [PRODUCT]) {
8
+ workspaceId
9
+ currency
10
+ truncated
11
+ rows {
12
+ productKey
13
+ attempts
14
+ costMicroUsd {
15
+ knownSubtotal
16
+ knownAttempts
17
+ missingAttempts
18
+ }
19
+ }
20
+ }
21
+ }
22
+ `;
23
+ function c(i, c) {
24
+ let { data: l, loading: u, error: d, refetch: f } = o(s, {
25
+ client: a("workspace"),
26
+ variables: r(() => e(c), [c]),
27
+ skip: !i,
28
+ fetchPolicy: "cache-and-network"
29
+ }), p = l?.promptUsageBreakdownV2, m = r(() => p ? n(p.rows) : null, [p]), h = t(d);
30
+ return {
31
+ summary: m,
32
+ truncated: p?.truncated ?? !1,
33
+ isLoading: u,
34
+ isForbidden: h,
35
+ error: d && !h ? d : null,
36
+ refetch: () => {
37
+ f();
38
+ }
39
+ };
40
+ }
41
+ //#endregion
42
+ export { c as useWorkspaceAiSpend };
43
+
44
+ //# sourceMappingURL=useWorkspaceAiSpend.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useWorkspaceAiSpend.js","names":[],"sources":["../../../../../src/billing/modules/usage/hooks/useWorkspaceAiSpend.ts"],"sourcesContent":["/**\n * Workspace AI spend in US dollars (BOFF-10273).\n *\n * Reads the workspace's AI ledger through `promptUsageBreakdownV2` on the\n * workspace gateway (wspace-prompts-svc), grouped by product. The workspace is\n * the one in the request context; the gateway enforces\n * `read prompt-invocation` on it, so only members allowed to see AI usage get\n * a result.\n */\n\nimport { useMemo } from 'react';\nimport { useGatewayClient, useQuery, gql } from '@burdenoff/fe-libs/shared/graphql';\nimport {\n aiSpendRange,\n isAiSpendPermissionError,\n summarizeAiSpend,\n type AiSpendBreakdownRowInput,\n type AiSpendSummary,\n type AiSpendWindow,\n} from '../utils/aiSpend';\n\nexport const WORKSPACE_AI_SPEND_QUERY = gql`\n query WorkspaceAiSpendByProduct($from: DateTime!, $to: DateTime!) {\n promptUsageBreakdownV2(from: $from, to: $to, groupBy: [PRODUCT]) {\n workspaceId\n currency\n truncated\n rows {\n productKey\n attempts\n costMicroUsd {\n knownSubtotal\n knownAttempts\n missingAttempts\n }\n }\n }\n }\n`;\n\ninterface WorkspaceAiSpendQueryResult {\n promptUsageBreakdownV2: {\n workspaceId: string;\n currency: string;\n truncated: boolean;\n rows: AiSpendBreakdownRowInput[];\n };\n}\n\nexport interface UseWorkspaceAiSpendResult {\n summary: AiSpendSummary | null;\n truncated: boolean;\n isLoading: boolean;\n /** The caller may not read this workspace's AI usage; render nothing. */\n isForbidden: boolean;\n error: Error | null;\n refetch: () => void;\n}\n\nexport function useWorkspaceAiSpend(\n workspaceId: string | undefined,\n window: AiSpendWindow\n): UseWorkspaceAiSpendResult {\n const wsClient = useGatewayClient('workspace');\n // The range is fixed when the window is chosen, so a re-render does not\n // move it and refetch; choosing a window again recomputes it.\n const range = useMemo(() => aiSpendRange(window), [window]);\n const { data, loading, error, refetch } = useQuery<WorkspaceAiSpendQueryResult>(\n WORKSPACE_AI_SPEND_QUERY,\n {\n client: wsClient,\n variables: range,\n skip: !workspaceId,\n fetchPolicy: 'cache-and-network',\n }\n );\n\n const breakdown = data?.promptUsageBreakdownV2;\n const summary = useMemo(() => (breakdown ? summarizeAiSpend(breakdown.rows) : null), [breakdown]);\n const isForbidden = isAiSpendPermissionError(error);\n\n return {\n summary,\n truncated: breakdown?.truncated ?? false,\n isLoading: loading,\n isForbidden,\n error: error && !isForbidden ? error : null,\n refetch: () => {\n void refetch();\n },\n };\n}\n"],"mappings":";;;;AAqBA,IAAa,IAA2B,CAAG;;;;;;;;;;;;;;;;;;AAsC3C,SAAgB,EACd,GACA,GAC2B;CAK3B,IAAM,EAAE,SAAM,YAAS,UAAO,eAAY,EACxC,GACA;EACE,QAPa,EAAiB,WAOtB;EACR,WALU,QAAc,EAAa,CAAM,GAAG,CAAC,CAAM,CAK1C;EACX,MAAM,CAAC;EACP,aAAa;CACf,CACF,GAEM,IAAY,GAAM,wBAClB,IAAU,QAAe,IAAY,EAAiB,EAAU,IAAI,IAAI,MAAO,CAAC,CAAS,CAAC,GAC1F,IAAc,EAAyB,CAAK;CAElD,OAAO;EACL;EACA,WAAW,GAAW,aAAa;EACnC,WAAW;EACX;EACA,OAAO,KAAS,CAAC,IAAc,IAAQ;EACvC,eAAe;GACb,EAAa;EACf;CACF;AACF"}