@burdenoff/microfe-store 2026.915.1 → 2026.916.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.
@@ -13,16 +13,16 @@ import { useEventBus as C } from "@burdenoff/fe-libs/shared/events";
13
13
  import { useQuotaErrorToast as w } from "@burdenoff/fe-libs/shared/hooks";
14
14
  //#region src/pages/CartPage.tsx
15
15
  var T = () => {
16
- let T = a(), { basePath: E } = e(), { cartItems: D, itemCount: O, total: k, updateQuantity: A, removeProduct: j, clearCart: M, createOrder: N } = t(), [P, F] = i(!1), [I, L] = i(null), { t: R } = o(), z = r(), B = C(), { report: V } = w(), H = D.some((e) => e.itemType === "physical"), U = async () => {
16
+ let T = a(), { basePath: E } = e(), { cart: D, cartItems: O, itemCount: k, total: A, updateQuantity: j, removeProduct: M, clearCart: N, createOrder: P } = t(), F = D?.currency || "USD", [I, L] = i(!1), [R, z] = i(null), { t: B } = o(), V = r(), H = C(), { report: U } = w(), W = O.some((e) => e.itemType === "physical"), G = async () => {
17
17
  try {
18
- F(!0), L(null), B.emit("store.cart.checkout_started", {
18
+ L(!0), z(null), H.emit("store.cart.checkout_started", {
19
19
  cartId: "",
20
- itemCount: O,
21
- total: k
20
+ itemCount: k,
21
+ total: A
22
22
  });
23
- let e = await N({
24
- total: k,
25
- lineItems: D.map((e) => ({
23
+ let e = await P({
24
+ total: A,
25
+ lineItems: O.map((e) => ({
26
26
  productId: e.productId,
27
27
  name: e.product?.displayName || "Product",
28
28
  quantity: e.quantity,
@@ -33,19 +33,19 @@ var T = () => {
33
33
  pricingModel: e.product?.pricingModel || "PAID_ONETIME"
34
34
  }))
35
35
  });
36
- e?.id ? (B.emit("store.order.placed", {
36
+ e?.id ? (H.emit("store.order.placed", {
37
37
  orderId: e.id,
38
- total: k,
39
- itemCount: O
40
- }), M().catch(() => {}), T(`/billing/checkout/store/${e.id}`)) : (L(R("pages.cart.failedToCreate", { defaultValue: "Failed to create order. Please try again." })), F(!1));
38
+ total: A,
39
+ itemCount: k
40
+ }), N().catch(() => {}), T(`/billing/checkout/store/${e.id}`)) : (z(B("pages.cart.failedToCreate", { defaultValue: "Failed to create order. Please try again." })), L(!1));
41
41
  } catch (e) {
42
42
  if (console.error("Checkout error:", e), y(e)) {
43
- V(e), F(!1);
43
+ U(e), L(!1);
44
44
  return;
45
45
  }
46
- L(e instanceof Error ? e.message : z("pages.cart.checkoutError", "An error occurred during checkout")), F(!1);
46
+ z(e instanceof Error ? e.message : V("pages.cart.checkoutError", "An error occurred during checkout")), L(!1);
47
47
  }
48
- }, W = () => {
48
+ }, K = () => {
49
49
  T(`${E}/marketplace`);
50
50
  };
51
51
  return /* @__PURE__ */ v("div", {
@@ -59,7 +59,7 @@ var T = () => {
59
59
  children: [/* @__PURE__ */ _("button", {
60
60
  type: "button",
61
61
  onClick: () => T(-1),
62
- "aria-label": z("common.goBack", "Go back"),
62
+ "aria-label": V("common.goBack", "Go back"),
63
63
  className: "cursor-pointer rounded-lg p-2 text-text-muted transition-colors hover:bg-bg-sunken hover:text-text-primary",
64
64
  children: /* @__PURE__ */ _(s, { className: "size-5" })
65
65
  }), /* @__PURE__ */ v("div", {
@@ -68,19 +68,19 @@ var T = () => {
68
68
  /* @__PURE__ */ _(f, { className: "size-6 text-text-primary" }),
69
69
  /* @__PURE__ */ _("h1", {
70
70
  className: "text-xl font-bold text-text-primary",
71
- children: R("cart.title", { defaultValue: "Shopping Cart" })
71
+ children: B("cart.title", { defaultValue: "Shopping Cart" })
72
72
  }),
73
- O > 0 && /* @__PURE__ */ v("span", {
73
+ k > 0 && /* @__PURE__ */ v("span", {
74
74
  className: "rounded-full bg-action-primary-bg px-2.5 py-0.5 text-sm font-medium text-action-primary-text",
75
75
  children: [
76
- O,
76
+ k,
77
77
  " ",
78
- O === 1 ? R("pages.cart.item", {
79
- defaultValue: `${O} item`,
80
- count: O
81
- }) : R("pages.cart.items", {
82
- defaultValue: `${O} items`,
83
- count: O
78
+ k === 1 ? B("pages.cart.item", {
79
+ defaultValue: `${k} item`,
80
+ count: k
81
+ }) : B("pages.cart.items", {
82
+ defaultValue: `${k} items`,
83
+ count: k
84
84
  })
85
85
  ]
86
86
  })
@@ -90,40 +90,40 @@ var T = () => {
90
90
  })
91
91
  }), /* @__PURE__ */ _("main", {
92
92
  className: "mx-auto max-w-6xl p-6",
93
- children: D.length === 0 ? /* @__PURE__ */ _(x, {
93
+ children: O.length === 0 ? /* @__PURE__ */ _(x, {
94
94
  illustration: "empty-generic",
95
- title: R("cart.empty", { defaultValue: "Your cart is empty" }),
96
- description: R("pages.cart.emptyDescription", { defaultValue: "Looks like you have not added any apps or products to your cart yet. Start exploring the marketplace to find something you will love!" }),
95
+ title: B("cart.empty", { defaultValue: "Your cart is empty" }),
96
+ description: B("pages.cart.emptyDescription", { defaultValue: "Looks like you have not added any apps or products to your cart yet. Start exploring the marketplace to find something you will love!" }),
97
97
  action: /* @__PURE__ */ v("button", {
98
98
  type: "button",
99
- onClick: W,
99
+ onClick: K,
100
100
  className: "cursor-pointer flex items-center gap-2 rounded-lg bg-action-primary-bg px-6 py-3 font-semibold text-action-primary-text transition-opacity hover:opacity-90",
101
- children: [/* @__PURE__ */ _(d, { className: "size-5" }), R("pages.cart.browseMarketplace", { defaultValue: "Browse Marketplace" })]
101
+ children: [/* @__PURE__ */ _(d, { className: "size-5" }), B("pages.cart.browseMarketplace", { defaultValue: "Browse Marketplace" })]
102
102
  })
103
103
  }) : /* @__PURE__ */ v("div", {
104
104
  className: "mx-auto max-w-3xl",
105
105
  children: [/* @__PURE__ */ _(S, {
106
106
  className: "mb-6",
107
- children: z("pages.cart.purpose", "Review the apps and products you're about to buy, adjust quantities for physical items, and confirm the total before checkout. Nothing is charged until you proceed — once you do, your purchases become licenses you can install into a workspace.")
107
+ children: V("pages.cart.purpose", "Review the apps and products you're about to buy, adjust quantities for physical items, and confirm the total before checkout. Nothing is charged until you proceed — once you do, your purchases become licenses you can install into a workspace.")
108
108
  }), /* @__PURE__ */ v("div", { children: [
109
109
  /* @__PURE__ */ v("div", {
110
110
  className: "mb-4 flex items-center justify-between",
111
111
  children: [/* @__PURE__ */ _("h2", {
112
112
  className: "text-lg font-semibold text-text-primary",
113
- children: R("pages.cart.cartItems", {
114
- defaultValue: `Cart Items (${O})`,
115
- count: O
113
+ children: B("pages.cart.cartItems", {
114
+ defaultValue: `Cart Items (${k})`,
115
+ count: k
116
116
  })
117
117
  }), /* @__PURE__ */ v("button", {
118
118
  type: "button",
119
- onClick: M,
119
+ onClick: N,
120
120
  className: "cursor-pointer flex items-center gap-1 text-sm text-text-muted transition-colors hover:text-status-error-text",
121
- children: [/* @__PURE__ */ _(m, { className: "size-4" }), R("cart.clearCart", { defaultValue: "Clear Cart" })]
121
+ children: [/* @__PURE__ */ _(m, { className: "size-4" }), B("cart.clearCart", { defaultValue: "Clear Cart" })]
122
122
  })]
123
123
  }),
124
124
  /* @__PURE__ */ _("ul", {
125
125
  className: "space-y-4",
126
- children: D.map((e) => /* @__PURE__ */ v("li", {
126
+ children: O.map((e) => /* @__PURE__ */ v("li", {
127
127
  className: "flex gap-4 rounded-xl border border-border-seam bg-bg-surface p-4 transition-[box-shadow,border-color] hover:border-border-strong hover:shadow-[var(--shadow-pop)]",
128
128
  children: [/* @__PURE__ */ _("div", {
129
129
  className: "size-24 flex-shrink-0 overflow-hidden rounded-lg bg-bg-sunken",
@@ -155,10 +155,10 @@ var T = () => {
155
155
  })
156
156
  ] }), /* @__PURE__ */ _("button", {
157
157
  type: "button",
158
- onClick: () => j(e.id),
158
+ onClick: () => M(e.id),
159
159
  className: "cursor-pointer rounded-lg p-2 text-text-muted transition-colors hover:bg-status-error-bg-subtle/10 hover:text-status-error-text",
160
- "aria-label": z("cart.removeItem", "Remove {{name}} from cart", { name: e.product?.displayName ?? z("common.item", "item") }),
161
- title: z("cart.removeItemTitle", "Remove item"),
160
+ "aria-label": V("cart.removeItem", "Remove {{name}} from cart", { name: e.product?.displayName ?? V("common.item", "item") }),
161
+ title: V("cart.removeItemTitle", "Remove item"),
162
162
  children: /* @__PURE__ */ _(m, { className: "size-5" })
163
163
  })]
164
164
  }),
@@ -166,10 +166,10 @@ var T = () => {
166
166
  className: "mt-2 flex items-center gap-2",
167
167
  children: [/* @__PURE__ */ _("span", {
168
168
  className: "inline-flex items-center gap-1 rounded-full bg-bg-sunken px-2 py-0.5 text-xs font-medium text-text-muted",
169
- children: e.itemType === "digital" ? /* @__PURE__ */ v(g, { children: [/* @__PURE__ */ _(h, { className: "size-3" }), R("pages.cart.digital", { defaultValue: "Digital" })] }) : /* @__PURE__ */ v(g, { children: [/* @__PURE__ */ _(p, { className: "size-3" }), R("pages.cart.physical", { defaultValue: "Physical" })] })
169
+ children: e.itemType === "digital" ? /* @__PURE__ */ v(g, { children: [/* @__PURE__ */ _(h, { className: "size-3" }), B("pages.cart.digital", { defaultValue: "Digital" })] }) : /* @__PURE__ */ v(g, { children: [/* @__PURE__ */ _(p, { className: "size-3" }), B("pages.cart.physical", { defaultValue: "Physical" })] })
170
170
  }), e.product?.pricingModel === "SUBSCRIPTION" && /* @__PURE__ */ _("span", {
171
171
  className: "inline-flex items-center gap-1 rounded-full bg-bg-accent/10 px-2 py-0.5 text-xs font-medium text-text-link",
172
- children: R("store.pricingModels.subscription", { defaultValue: "Subscription" })
172
+ children: B("store.pricingModels.subscription", { defaultValue: "Subscription" })
173
173
  })]
174
174
  }),
175
175
  /* @__PURE__ */ v("div", {
@@ -178,13 +178,13 @@ var T = () => {
178
178
  className: "flex items-center gap-3",
179
179
  children: [/* @__PURE__ */ _("span", {
180
180
  className: "text-sm text-text-muted",
181
- children: z("pages.cart.qty", "Qty:")
181
+ children: V("pages.cart.qty", "Qty:")
182
182
  }), /* @__PURE__ */ v("div", {
183
183
  className: "flex items-center rounded-lg border border-border-default",
184
184
  children: [
185
185
  /* @__PURE__ */ _("button", {
186
186
  type: "button",
187
- onClick: () => A(e.id, e.quantity - 1),
187
+ onClick: () => j(e.id, e.quantity - 1),
188
188
  className: "cursor-pointer rounded-l-lg p-2 transition-colors hover:bg-bg-sunken disabled:cursor-not-allowed disabled:opacity-50",
189
189
  disabled: e.quantity <= 1,
190
190
  children: /* @__PURE__ */ _(l, { className: "size-4" })
@@ -195,7 +195,7 @@ var T = () => {
195
195
  }),
196
196
  /* @__PURE__ */ _("button", {
197
197
  type: "button",
198
- onClick: () => A(e.id, e.quantity + 1),
198
+ onClick: () => j(e.id, e.quantity + 1),
199
199
  className: "cursor-pointer rounded-r-lg p-2 transition-colors hover:bg-bg-sunken",
200
200
  children: /* @__PURE__ */ _(u, { className: "size-4" })
201
201
  })
@@ -203,15 +203,15 @@ var T = () => {
203
203
  })]
204
204
  }) : /* @__PURE__ */ _("div", {
205
205
  className: "text-sm text-text-muted",
206
- children: R("pages.cart.singleLicense", { defaultValue: "Single license" })
206
+ children: B("pages.cart.singleLicense", { defaultValue: "Single license" })
207
207
  }), /* @__PURE__ */ v("div", {
208
208
  className: "text-right",
209
209
  children: [/* @__PURE__ */ _("p", {
210
210
  className: "text-lg font-bold text-text-primary",
211
- children: n(e.totalPrice)
211
+ children: n(e.totalPrice, F)
212
212
  }), e.quantity > 1 && e.itemType === "physical" && /* @__PURE__ */ _("p", {
213
213
  className: "text-xs text-text-muted",
214
- children: z("pages.cart.each", "{{price}} each", { price: n(e.unitPrice) })
214
+ children: V("pages.cart.each", "{{price}} each", { price: n(e.unitPrice, F) })
215
215
  })]
216
216
  })]
217
217
  })
@@ -222,38 +222,38 @@ var T = () => {
222
222
  /* @__PURE__ */ v(b, {
223
223
  className: "mt-8",
224
224
  children: [
225
- I && /* @__PURE__ */ _("div", {
225
+ R && /* @__PURE__ */ _("div", {
226
226
  role: "alert",
227
227
  className: "mb-4 rounded-lg border border-status-error-border bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text",
228
- children: I
228
+ children: R
229
229
  }),
230
230
  /* @__PURE__ */ _("p", {
231
231
  className: "mb-3 text-xs text-text-muted",
232
- children: H ? z("pages.cart.physicalNotice", "Physical orders ship to your delivery address. Returns and refunds are handled by the seller per their store policy.") : z("pages.cart.digitalNotice", "All digital app purchases are final and non-refundable. Once an order is completed it cannot be cancelled or reversed.")
232
+ children: W ? V("pages.cart.physicalNotice", "Physical orders ship to your delivery address. Returns and refunds are handled by the seller per their store policy.") : V("pages.cart.digitalNotice", "All digital app purchases are final and non-refundable. Once an order is completed it cannot be cancelled or reversed.")
233
233
  }),
234
234
  /* @__PURE__ */ v("div", {
235
235
  className: "flex items-center justify-between",
236
236
  children: [/* @__PURE__ */ v("div", { children: [/* @__PURE__ */ _("p", {
237
237
  className: "text-sm text-text-muted",
238
- children: z("cart.total", "Total")
238
+ children: V("cart.total", "Total")
239
239
  }), /* @__PURE__ */ _("p", {
240
240
  className: "text-2xl font-bold tabular-nums text-text-primary",
241
- children: n(k)
241
+ children: n(A, F)
242
242
  })] }), /* @__PURE__ */ _("button", {
243
243
  type: "button",
244
- onClick: U,
245
- disabled: P,
244
+ onClick: G,
245
+ disabled: I,
246
246
  className: "flex cursor-pointer items-center gap-2 rounded-lg bg-action-primary-bg px-6 py-3 font-semibold text-action-primary-text transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-70",
247
- children: P ? /* @__PURE__ */ v(g, { children: [/* @__PURE__ */ _(c, { className: "size-4 animate-spin" }), R("pages.cart.processing", { defaultValue: "Processing..." })] }) : /* @__PURE__ */ v(g, { children: [/* @__PURE__ */ _(h, { className: "size-4" }), R("pages.cart.proceedToCheckout", { defaultValue: "Proceed to Checkout" })] })
247
+ children: I ? /* @__PURE__ */ v(g, { children: [/* @__PURE__ */ _(c, { className: "size-4 animate-spin" }), B("pages.cart.processing", { defaultValue: "Processing..." })] }) : /* @__PURE__ */ v(g, { children: [/* @__PURE__ */ _(h, { className: "size-4" }), B("pages.cart.proceedToCheckout", { defaultValue: "Proceed to Checkout" })] })
248
248
  })]
249
249
  })
250
250
  ]
251
251
  }),
252
252
  /* @__PURE__ */ v("button", {
253
253
  type: "button",
254
- onClick: W,
254
+ onClick: K,
255
255
  className: "cursor-pointer mt-4 flex items-center gap-2 text-sm font-medium text-text-link hover:underline",
256
- children: [/* @__PURE__ */ _(s, { className: "size-4" }), R("cart.continueShopping", { defaultValue: "Continue Shopping" })]
256
+ children: [/* @__PURE__ */ _(s, { className: "size-4" }), B("cart.continueShopping", { defaultValue: "Continue Shopping" })]
257
257
  })
258
258
  ] })]
259
259
  })
@@ -1 +1 @@
1
- {"version":3,"file":"CartPage.js","names":[],"sources":["../../src/pages/CartPage.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useState } from 'react';\nimport { useNavigate } from 'react-router';\nimport { useEventBus } from '@burdenoff/fe-libs/shared/events';\nimport { useQuotaErrorToast } from '@burdenoff/fe-libs/shared/hooks';\nimport { isQuotaExhaustedError } from '@burdenoff/fe-libs/shared/utils';\nimport {\n ShoppingCart,\n Trash2,\n Plus,\n Minus,\n ArrowLeft,\n ShoppingBag,\n Zap,\n Tag,\n Loader2,\n} from 'lucide-react';\nimport { useStore } from '../providers/StoreProvider';\nimport { useCart } from '../hooks/useCart';\nimport { formatPrice } from '../utils';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useTr } from '../shared/hooks/useTr';\nimport { EmphasisPanel, IllustratedEmptyState, PagePurpose } from '@burdenoff/fe-libs/ui';\n\n/**\n * CartPage component\n *\n * Full page cart view with items, summary, and checkout\n */\nexport const CartPage: FC = () => {\n const navigate = useNavigate();\n const { basePath } = useStore();\n const { cartItems, itemCount, total, updateQuantity, removeProduct, clearCart, createOrder } =\n useCart();\n const [isCheckingOut, setIsCheckingOut] = useState(false);\n const [checkoutError, setCheckoutError] = useState<string | null>(null);\n const { t } = useI18n();\n const tr = useTr();\n const bus = useEventBus();\n // Surface QUOTA_EXHAUSTED gateway errors as a toast (e.g. when the\n // workspace has hit its store-purchases-per-month limit). The hook also\n // listens for the global `burdenoff:quota-exhausted` event so any other\n // mutation in the page that triggers it will toast as well.\n const { report: reportQuotaError } = useQuotaErrorToast();\n\n // Cart can hold physical (produce) items alongside digital apps. The page\n // compares item.itemType lowercase (see quantity controls below), so match\n // that casing here to avoid claiming physical/mixed orders \"cannot be\n // cancelled\" — those support returns/refunds via the seller's store policy.\n const hasPhysical = cartItems.some((i) => i.itemType === 'physical');\n\n const handleCheckout = async () => {\n try {\n setIsCheckingOut(true);\n setCheckoutError(null);\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n bus.emit('store.cart.checkout_started' as any, {\n cartId: '',\n itemCount,\n total,\n });\n\n // Build line items from cart\n const lineItems = cartItems.map((item) => ({\n productId: item.productId,\n name: item.product?.displayName || 'Product',\n quantity: item.quantity,\n unitPrice: item.unitPrice,\n subtotal: item.totalPrice,\n itemType: item.itemType.toLowerCase(),\n variantId: item.variantId,\n pricingModel: item.product?.pricingModel || 'PAID_ONETIME',\n }));\n\n // Create order input\n const orderInput = {\n total,\n lineItems,\n };\n\n // Call createStoreOrder mutation to create PENDING order\n const order = await createOrder(orderInput);\n\n if (order?.id) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n bus.emit('store.order.placed' as any, {\n orderId: order.id,\n total,\n itemCount,\n });\n // Clear cart now that the order is created — non-blocking so a\n // clear failure never prevents the user from reaching billing.\n clearCart().catch(() => {});\n // Navigate to billing page with the order ID\n navigate(`/billing/checkout/store/${order.id}`);\n } else {\n setCheckoutError(\n t('pages.cart.failedToCreate', {\n defaultValue: 'Failed to create order. Please try again.',\n })\n );\n setIsCheckingOut(false);\n }\n } catch (error) {\n console.error('Checkout error:', error);\n // Quota-exhausted errors surface a global toast via the shell's\n // GlobalUiProvider; suppress the inline checkout error so we don't\n // stack two error UIs on top of each other.\n if (isQuotaExhaustedError(error)) {\n reportQuotaError(error);\n setIsCheckingOut(false);\n return;\n }\n setCheckoutError(\n error instanceof Error\n ? error.message\n : tr('pages.cart.checkoutError', 'An error occurred during checkout')\n );\n setIsCheckingOut(false);\n }\n };\n\n const handleContinueShopping = () => {\n navigate(`${basePath}/marketplace`);\n };\n\n return (\n <div className=\"h-full overflow-y-auto\">\n {/* Header */}\n <header className=\"sticky top-0 z-10 border-b border-border-seam bg-bg-surface px-6 py-4\">\n <div className=\"mx-auto max-w-6xl\">\n <div className=\"flex items-center gap-4\">\n <button\n type=\"button\"\n onClick={() => navigate(-1)}\n aria-label={tr('common.goBack', 'Go back')}\n className=\"cursor-pointer rounded-lg p-2 text-text-muted transition-colors hover:bg-bg-sunken hover:text-text-primary\"\n >\n <ArrowLeft className=\"size-5\" />\n </button>\n <div className=\"flex items-center gap-3\">\n <ShoppingCart className=\"size-6 text-text-primary\" />\n <h1 className=\"text-xl font-bold text-text-primary\">\n {t('cart.title', { defaultValue: 'Shopping Cart' })}\n </h1>\n {itemCount > 0 && (\n <span className=\"rounded-full bg-action-primary-bg px-2.5 py-0.5 text-sm font-medium text-action-primary-text\">\n {itemCount}{' '}\n {itemCount === 1\n ? t('pages.cart.item', { defaultValue: `${itemCount} item`, count: itemCount })\n : t('pages.cart.items', {\n defaultValue: `${itemCount} items`,\n count: itemCount,\n })}\n </span>\n )}\n </div>\n </div>\n </div>\n </header>\n\n <main className=\"mx-auto max-w-6xl p-6\">\n {cartItems.length === 0 ? (\n // Empty Cart State\n <IllustratedEmptyState\n illustration=\"empty-generic\"\n title={t('cart.empty', { defaultValue: 'Your cart is empty' })}\n description={t('pages.cart.emptyDescription', {\n defaultValue:\n 'Looks like you have not added any apps or products to your cart yet. Start exploring the marketplace to find something you will love!',\n })}\n action={\n <button\n type=\"button\"\n onClick={handleContinueShopping}\n className=\"cursor-pointer flex items-center gap-2 rounded-lg bg-action-primary-bg px-6 py-3 font-semibold text-action-primary-text transition-opacity hover:opacity-90\"\n >\n <ShoppingBag className=\"size-5\" />\n {t('pages.cart.browseMarketplace', { defaultValue: 'Browse Marketplace' })}\n </button>\n }\n />\n ) : (\n // Cart with Items\n <div className=\"mx-auto max-w-3xl\">\n <PagePurpose className=\"mb-6\">\n {tr(\n 'pages.cart.purpose',\n \"Review the apps and products you're about to buy, adjust quantities for physical items, and confirm the total before checkout. Nothing is charged until you proceed — once you do, your purchases become licenses you can install into a workspace.\"\n )}\n </PagePurpose>\n {/* Cart Items */}\n <div>\n {/* Cart Actions */}\n <div className=\"mb-4 flex items-center justify-between\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {t('pages.cart.cartItems', {\n defaultValue: `Cart Items (${itemCount})`,\n count: itemCount,\n })}\n </h2>\n <button\n type=\"button\"\n onClick={clearCart}\n className=\"cursor-pointer flex items-center gap-1 text-sm text-text-muted transition-colors hover:text-status-error-text\"\n >\n <Trash2 className=\"size-4\" />\n {t('cart.clearCart', { defaultValue: 'Clear Cart' })}\n </button>\n </div>\n\n {/* Items List */}\n <ul className=\"space-y-4\">\n {cartItems.map((item) => (\n <li\n key={item.id}\n className=\"flex gap-4 rounded-xl border border-border-seam bg-bg-surface p-4 transition-[box-shadow,border-color] hover:border-border-strong hover:shadow-[var(--shadow-pop)]\"\n >\n {/* Product Image */}\n <div className=\"size-24 flex-shrink-0 overflow-hidden rounded-lg bg-bg-sunken\">\n {item.product?.iconUrl ? (\n <img\n src={item.product.iconUrl}\n alt={item.product.displayName}\n className=\"size-full object-cover\"\n />\n ) : (\n <div className=\"flex size-full items-center justify-center text-2xl font-bold text-text-muted\">\n {item.product?.displayName?.charAt(0) || 'P'}\n </div>\n )}\n </div>\n\n {/* Product Info */}\n <div className=\"flex flex-1 flex-col\">\n <div className=\"flex items-start justify-between\">\n <div>\n <h3 className=\"font-semibold text-text-primary\">\n {item.product?.displayName || 'Product'}\n </h3>\n <p className=\"text-sm text-text-muted\">\n {item.product?.publisher?.displayName || 'Unknown Publisher'}\n </p>\n {item.variant && (\n <p className=\"mt-1 text-xs text-text-muted\">\n Version {item.variant.version}\n </p>\n )}\n </div>\n <button\n type=\"button\"\n onClick={() => removeProduct(item.id)}\n className=\"cursor-pointer rounded-lg p-2 text-text-muted transition-colors hover:bg-status-error-bg-subtle/10 hover:text-status-error-text\"\n aria-label={tr('cart.removeItem', 'Remove {{name}} from cart', {\n name: item.product?.displayName ?? tr('common.item', 'item'),\n })}\n title={tr('cart.removeItemTitle', 'Remove item')}\n >\n <Trash2 className=\"size-5\" />\n </button>\n </div>\n\n {/* Item Type Badge */}\n <div className=\"mt-2 flex items-center gap-2\">\n <span className=\"inline-flex items-center gap-1 rounded-full bg-bg-sunken px-2 py-0.5 text-xs font-medium text-text-muted\">\n {item.itemType === 'digital' ? (\n <>\n <Zap className=\"size-3\" />\n {t('pages.cart.digital', { defaultValue: 'Digital' })}\n </>\n ) : (\n <>\n <Tag className=\"size-3\" />\n {t('pages.cart.physical', { defaultValue: 'Physical' })}\n </>\n )}\n </span>\n {item.product?.pricingModel === 'SUBSCRIPTION' && (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-bg-accent/10 px-2 py-0.5 text-xs font-medium text-text-link\">\n {t('store.pricingModels.subscription', {\n defaultValue: 'Subscription',\n })}\n </span>\n )}\n </div>\n\n {/* Quantity and Price Row */}\n <div className=\"mt-auto flex items-center justify-between pt-3\">\n {/* Quantity Controls - Only show for physical items */}\n {item.itemType === 'physical' ? (\n <div className=\"flex items-center gap-3\">\n <span className=\"text-sm text-text-muted\">\n {tr('pages.cart.qty', 'Qty:')}\n </span>\n <div className=\"flex items-center rounded-lg border border-border-default\">\n <button\n type=\"button\"\n onClick={() => updateQuantity(item.id, item.quantity - 1)}\n className=\"cursor-pointer rounded-l-lg p-2 transition-colors hover:bg-bg-sunken disabled:cursor-not-allowed disabled:opacity-50\"\n disabled={item.quantity <= 1}\n >\n <Minus className=\"size-4\" />\n </button>\n <span className=\"min-w-[3rem] text-center font-medium\">\n {item.quantity}\n </span>\n <button\n type=\"button\"\n onClick={() => updateQuantity(item.id, item.quantity + 1)}\n className=\"cursor-pointer rounded-r-lg p-2 transition-colors hover:bg-bg-sunken\"\n >\n <Plus className=\"size-4\" />\n </button>\n </div>\n </div>\n ) : (\n <div className=\"text-sm text-text-muted\">\n {t('pages.cart.singleLicense', { defaultValue: 'Single license' })}\n </div>\n )}\n\n {/* Price */}\n <div className=\"text-right\">\n <p className=\"text-lg font-bold text-text-primary\">\n {formatPrice(item.totalPrice)}\n </p>\n {item.quantity > 1 && item.itemType === 'physical' && (\n <p className=\"text-xs text-text-muted\">\n {tr('pages.cart.each', '{{price}} each', {\n price: formatPrice(item.unitPrice),\n })}\n </p>\n )}\n </div>\n </div>\n </div>\n </li>\n ))}\n </ul>\n\n {/* Checkout Footer */}\n <EmphasisPanel className=\"mt-8\">\n {checkoutError && (\n <div\n role=\"alert\"\n className=\"mb-4 rounded-lg border border-status-error-border bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\"\n >\n {checkoutError}\n </div>\n )}\n <p className=\"mb-3 text-xs text-text-muted\">\n {hasPhysical\n ? tr(\n 'pages.cart.physicalNotice',\n 'Physical orders ship to your delivery address. Returns and refunds are handled by the seller per their store policy.'\n )\n : tr(\n 'pages.cart.digitalNotice',\n 'All digital app purchases are final and non-refundable. Once an order is completed it cannot be cancelled or reversed.'\n )}\n </p>\n <div className=\"flex items-center justify-between\">\n <div>\n <p className=\"text-sm text-text-muted\">{tr('cart.total', 'Total')}</p>\n <p className=\"text-2xl font-bold tabular-nums text-text-primary\">\n {formatPrice(total)}\n </p>\n </div>\n <button\n type=\"button\"\n onClick={handleCheckout}\n disabled={isCheckingOut}\n className=\"flex cursor-pointer items-center gap-2 rounded-lg bg-action-primary-bg px-6 py-3 font-semibold text-action-primary-text transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-70\"\n >\n {isCheckingOut ? (\n <>\n <Loader2 className=\"size-4 animate-spin\" />\n {t('pages.cart.processing', { defaultValue: 'Processing...' })}\n </>\n ) : (\n <>\n <Zap className=\"size-4\" />\n {t('pages.cart.proceedToCheckout', { defaultValue: 'Proceed to Checkout' })}\n </>\n )}\n </button>\n </div>\n </EmphasisPanel>\n\n {/* Continue Shopping */}\n <button\n type=\"button\"\n onClick={handleContinueShopping}\n className=\"cursor-pointer mt-4 flex items-center gap-2 text-sm font-medium text-text-link hover:underline\"\n >\n <ArrowLeft className=\"size-4\" />\n {t('cart.continueShopping', { defaultValue: 'Continue Shopping' })}\n </button>\n </div>\n </div>\n )}\n </main>\n </div>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"CartPage.js","names":[],"sources":["../../src/pages/CartPage.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useState } from 'react';\nimport { useNavigate } from 'react-router';\nimport { useEventBus } from '@burdenoff/fe-libs/shared/events';\nimport { useQuotaErrorToast } from '@burdenoff/fe-libs/shared/hooks';\nimport { isQuotaExhaustedError } from '@burdenoff/fe-libs/shared/utils';\nimport {\n ShoppingCart,\n Trash2,\n Plus,\n Minus,\n ArrowLeft,\n ShoppingBag,\n Zap,\n Tag,\n Loader2,\n} from 'lucide-react';\nimport { useStore } from '../providers/StoreProvider';\nimport { useCart } from '../hooks/useCart';\nimport { formatPrice } from '../utils';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useTr } from '../shared/hooks/useTr';\nimport { EmphasisPanel, IllustratedEmptyState, PagePurpose } from '@burdenoff/fe-libs/ui';\n\n/**\n * CartPage component\n *\n * Full page cart view with items, summary, and checkout\n */\nexport const CartPage: FC = () => {\n const navigate = useNavigate();\n const { basePath } = useStore();\n const {\n cart,\n cartItems,\n itemCount,\n total,\n updateQuantity,\n removeProduct,\n clearCart,\n createOrder,\n } = useCart();\n // Cart-wide, not per-item — see useBackendCart.ts's Cart type. formatPrice\n // defaults to USD when omitted, so every price rendered the raw store-native\n // number (e.g. 349 INR) with a \"$\" symbol instead of \"₹349.00\".\n const cartCurrency = cart?.currency || 'USD';\n const [isCheckingOut, setIsCheckingOut] = useState(false);\n const [checkoutError, setCheckoutError] = useState<string | null>(null);\n const { t } = useI18n();\n const tr = useTr();\n const bus = useEventBus();\n // Surface QUOTA_EXHAUSTED gateway errors as a toast (e.g. when the\n // workspace has hit its store-purchases-per-month limit). The hook also\n // listens for the global `burdenoff:quota-exhausted` event so any other\n // mutation in the page that triggers it will toast as well.\n const { report: reportQuotaError } = useQuotaErrorToast();\n\n // Cart can hold physical (produce) items alongside digital apps. The page\n // compares item.itemType lowercase (see quantity controls below), so match\n // that casing here to avoid claiming physical/mixed orders \"cannot be\n // cancelled\" — those support returns/refunds via the seller's store policy.\n const hasPhysical = cartItems.some((i) => i.itemType === 'physical');\n\n const handleCheckout = async () => {\n try {\n setIsCheckingOut(true);\n setCheckoutError(null);\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n bus.emit('store.cart.checkout_started' as any, {\n cartId: '',\n itemCount,\n total,\n });\n\n // Build line items from cart\n const lineItems = cartItems.map((item) => ({\n productId: item.productId,\n name: item.product?.displayName || 'Product',\n quantity: item.quantity,\n unitPrice: item.unitPrice,\n subtotal: item.totalPrice,\n itemType: item.itemType.toLowerCase(),\n variantId: item.variantId,\n pricingModel: item.product?.pricingModel || 'PAID_ONETIME',\n }));\n\n // Create order input\n const orderInput = {\n total,\n lineItems,\n };\n\n // Call createStoreOrder mutation to create PENDING order\n const order = await createOrder(orderInput);\n\n if (order?.id) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n bus.emit('store.order.placed' as any, {\n orderId: order.id,\n total,\n itemCount,\n });\n // Clear cart now that the order is created — non-blocking so a\n // clear failure never prevents the user from reaching billing.\n clearCart().catch(() => {});\n // Navigate to billing page with the order ID\n navigate(`/billing/checkout/store/${order.id}`);\n } else {\n setCheckoutError(\n t('pages.cart.failedToCreate', {\n defaultValue: 'Failed to create order. Please try again.',\n })\n );\n setIsCheckingOut(false);\n }\n } catch (error) {\n console.error('Checkout error:', error);\n // Quota-exhausted errors surface a global toast via the shell's\n // GlobalUiProvider; suppress the inline checkout error so we don't\n // stack two error UIs on top of each other.\n if (isQuotaExhaustedError(error)) {\n reportQuotaError(error);\n setIsCheckingOut(false);\n return;\n }\n setCheckoutError(\n error instanceof Error\n ? error.message\n : tr('pages.cart.checkoutError', 'An error occurred during checkout')\n );\n setIsCheckingOut(false);\n }\n };\n\n const handleContinueShopping = () => {\n navigate(`${basePath}/marketplace`);\n };\n\n return (\n <div className=\"h-full overflow-y-auto\">\n {/* Header */}\n <header className=\"sticky top-0 z-10 border-b border-border-seam bg-bg-surface px-6 py-4\">\n <div className=\"mx-auto max-w-6xl\">\n <div className=\"flex items-center gap-4\">\n <button\n type=\"button\"\n onClick={() => navigate(-1)}\n aria-label={tr('common.goBack', 'Go back')}\n className=\"cursor-pointer rounded-lg p-2 text-text-muted transition-colors hover:bg-bg-sunken hover:text-text-primary\"\n >\n <ArrowLeft className=\"size-5\" />\n </button>\n <div className=\"flex items-center gap-3\">\n <ShoppingCart className=\"size-6 text-text-primary\" />\n <h1 className=\"text-xl font-bold text-text-primary\">\n {t('cart.title', { defaultValue: 'Shopping Cart' })}\n </h1>\n {itemCount > 0 && (\n <span className=\"rounded-full bg-action-primary-bg px-2.5 py-0.5 text-sm font-medium text-action-primary-text\">\n {itemCount}{' '}\n {itemCount === 1\n ? t('pages.cart.item', { defaultValue: `${itemCount} item`, count: itemCount })\n : t('pages.cart.items', {\n defaultValue: `${itemCount} items`,\n count: itemCount,\n })}\n </span>\n )}\n </div>\n </div>\n </div>\n </header>\n\n <main className=\"mx-auto max-w-6xl p-6\">\n {cartItems.length === 0 ? (\n // Empty Cart State\n <IllustratedEmptyState\n illustration=\"empty-generic\"\n title={t('cart.empty', { defaultValue: 'Your cart is empty' })}\n description={t('pages.cart.emptyDescription', {\n defaultValue:\n 'Looks like you have not added any apps or products to your cart yet. Start exploring the marketplace to find something you will love!',\n })}\n action={\n <button\n type=\"button\"\n onClick={handleContinueShopping}\n className=\"cursor-pointer flex items-center gap-2 rounded-lg bg-action-primary-bg px-6 py-3 font-semibold text-action-primary-text transition-opacity hover:opacity-90\"\n >\n <ShoppingBag className=\"size-5\" />\n {t('pages.cart.browseMarketplace', { defaultValue: 'Browse Marketplace' })}\n </button>\n }\n />\n ) : (\n // Cart with Items\n <div className=\"mx-auto max-w-3xl\">\n <PagePurpose className=\"mb-6\">\n {tr(\n 'pages.cart.purpose',\n \"Review the apps and products you're about to buy, adjust quantities for physical items, and confirm the total before checkout. Nothing is charged until you proceed — once you do, your purchases become licenses you can install into a workspace.\"\n )}\n </PagePurpose>\n {/* Cart Items */}\n <div>\n {/* Cart Actions */}\n <div className=\"mb-4 flex items-center justify-between\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {t('pages.cart.cartItems', {\n defaultValue: `Cart Items (${itemCount})`,\n count: itemCount,\n })}\n </h2>\n <button\n type=\"button\"\n onClick={clearCart}\n className=\"cursor-pointer flex items-center gap-1 text-sm text-text-muted transition-colors hover:text-status-error-text\"\n >\n <Trash2 className=\"size-4\" />\n {t('cart.clearCart', { defaultValue: 'Clear Cart' })}\n </button>\n </div>\n\n {/* Items List */}\n <ul className=\"space-y-4\">\n {cartItems.map((item) => (\n <li\n key={item.id}\n className=\"flex gap-4 rounded-xl border border-border-seam bg-bg-surface p-4 transition-[box-shadow,border-color] hover:border-border-strong hover:shadow-[var(--shadow-pop)]\"\n >\n {/* Product Image */}\n <div className=\"size-24 flex-shrink-0 overflow-hidden rounded-lg bg-bg-sunken\">\n {item.product?.iconUrl ? (\n <img\n src={item.product.iconUrl}\n alt={item.product.displayName}\n className=\"size-full object-cover\"\n />\n ) : (\n <div className=\"flex size-full items-center justify-center text-2xl font-bold text-text-muted\">\n {item.product?.displayName?.charAt(0) || 'P'}\n </div>\n )}\n </div>\n\n {/* Product Info */}\n <div className=\"flex flex-1 flex-col\">\n <div className=\"flex items-start justify-between\">\n <div>\n <h3 className=\"font-semibold text-text-primary\">\n {item.product?.displayName || 'Product'}\n </h3>\n <p className=\"text-sm text-text-muted\">\n {item.product?.publisher?.displayName || 'Unknown Publisher'}\n </p>\n {item.variant && (\n <p className=\"mt-1 text-xs text-text-muted\">\n Version {item.variant.version}\n </p>\n )}\n </div>\n <button\n type=\"button\"\n onClick={() => removeProduct(item.id)}\n className=\"cursor-pointer rounded-lg p-2 text-text-muted transition-colors hover:bg-status-error-bg-subtle/10 hover:text-status-error-text\"\n aria-label={tr('cart.removeItem', 'Remove {{name}} from cart', {\n name: item.product?.displayName ?? tr('common.item', 'item'),\n })}\n title={tr('cart.removeItemTitle', 'Remove item')}\n >\n <Trash2 className=\"size-5\" />\n </button>\n </div>\n\n {/* Item Type Badge */}\n <div className=\"mt-2 flex items-center gap-2\">\n <span className=\"inline-flex items-center gap-1 rounded-full bg-bg-sunken px-2 py-0.5 text-xs font-medium text-text-muted\">\n {item.itemType === 'digital' ? (\n <>\n <Zap className=\"size-3\" />\n {t('pages.cart.digital', { defaultValue: 'Digital' })}\n </>\n ) : (\n <>\n <Tag className=\"size-3\" />\n {t('pages.cart.physical', { defaultValue: 'Physical' })}\n </>\n )}\n </span>\n {item.product?.pricingModel === 'SUBSCRIPTION' && (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-bg-accent/10 px-2 py-0.5 text-xs font-medium text-text-link\">\n {t('store.pricingModels.subscription', {\n defaultValue: 'Subscription',\n })}\n </span>\n )}\n </div>\n\n {/* Quantity and Price Row */}\n <div className=\"mt-auto flex items-center justify-between pt-3\">\n {/* Quantity Controls - Only show for physical items */}\n {item.itemType === 'physical' ? (\n <div className=\"flex items-center gap-3\">\n <span className=\"text-sm text-text-muted\">\n {tr('pages.cart.qty', 'Qty:')}\n </span>\n <div className=\"flex items-center rounded-lg border border-border-default\">\n <button\n type=\"button\"\n onClick={() => updateQuantity(item.id, item.quantity - 1)}\n className=\"cursor-pointer rounded-l-lg p-2 transition-colors hover:bg-bg-sunken disabled:cursor-not-allowed disabled:opacity-50\"\n disabled={item.quantity <= 1}\n >\n <Minus className=\"size-4\" />\n </button>\n <span className=\"min-w-[3rem] text-center font-medium\">\n {item.quantity}\n </span>\n <button\n type=\"button\"\n onClick={() => updateQuantity(item.id, item.quantity + 1)}\n className=\"cursor-pointer rounded-r-lg p-2 transition-colors hover:bg-bg-sunken\"\n >\n <Plus className=\"size-4\" />\n </button>\n </div>\n </div>\n ) : (\n <div className=\"text-sm text-text-muted\">\n {t('pages.cart.singleLicense', { defaultValue: 'Single license' })}\n </div>\n )}\n\n {/* Price */}\n <div className=\"text-right\">\n <p className=\"text-lg font-bold text-text-primary\">\n {formatPrice(item.totalPrice, cartCurrency)}\n </p>\n {item.quantity > 1 && item.itemType === 'physical' && (\n <p className=\"text-xs text-text-muted\">\n {tr('pages.cart.each', '{{price}} each', {\n price: formatPrice(item.unitPrice, cartCurrency),\n })}\n </p>\n )}\n </div>\n </div>\n </div>\n </li>\n ))}\n </ul>\n\n {/* Checkout Footer */}\n <EmphasisPanel className=\"mt-8\">\n {checkoutError && (\n <div\n role=\"alert\"\n className=\"mb-4 rounded-lg border border-status-error-border bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\"\n >\n {checkoutError}\n </div>\n )}\n <p className=\"mb-3 text-xs text-text-muted\">\n {hasPhysical\n ? tr(\n 'pages.cart.physicalNotice',\n 'Physical orders ship to your delivery address. Returns and refunds are handled by the seller per their store policy.'\n )\n : tr(\n 'pages.cart.digitalNotice',\n 'All digital app purchases are final and non-refundable. Once an order is completed it cannot be cancelled or reversed.'\n )}\n </p>\n <div className=\"flex items-center justify-between\">\n <div>\n <p className=\"text-sm text-text-muted\">{tr('cart.total', 'Total')}</p>\n <p className=\"text-2xl font-bold tabular-nums text-text-primary\">\n {formatPrice(total, cartCurrency)}\n </p>\n </div>\n <button\n type=\"button\"\n onClick={handleCheckout}\n disabled={isCheckingOut}\n className=\"flex cursor-pointer items-center gap-2 rounded-lg bg-action-primary-bg px-6 py-3 font-semibold text-action-primary-text transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-70\"\n >\n {isCheckingOut ? (\n <>\n <Loader2 className=\"size-4 animate-spin\" />\n {t('pages.cart.processing', { defaultValue: 'Processing...' })}\n </>\n ) : (\n <>\n <Zap className=\"size-4\" />\n {t('pages.cart.proceedToCheckout', { defaultValue: 'Proceed to Checkout' })}\n </>\n )}\n </button>\n </div>\n </EmphasisPanel>\n\n {/* Continue Shopping */}\n <button\n type=\"button\"\n onClick={handleContinueShopping}\n className=\"cursor-pointer mt-4 flex items-center gap-2 text-sm font-medium text-text-link hover:underline\"\n >\n <ArrowLeft className=\"size-4\" />\n {t('cart.continueShopping', { defaultValue: 'Continue Shopping' })}\n </button>\n </div>\n </div>\n )}\n </main>\n </div>\n );\n};\n"],"mappings":"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"}