@burdenoff/microfe-store 2026.704.1 → 2026.705.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/pages/CartPage.js +27 -20
- package/dist/pages/CartPage.js.map +1 -1
- package/dist/pages/OrderConfirmationPage.js +4 -0
- package/dist/pages/OrderConfirmationPage.js.map +1 -1
- package/dist/pages/ProductDetailPage.js +338 -334
- package/dist/pages/ProductDetailPage.js.map +1 -1
- package/package.json +1 -1
package/dist/pages/CartPage.js
CHANGED
|
@@ -220,26 +220,33 @@ var w = () => {
|
|
|
220
220
|
}),
|
|
221
221
|
/* @__PURE__ */ g(y, {
|
|
222
222
|
className: "mt-8",
|
|
223
|
-
children: [
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
className: "text-
|
|
231
|
-
children: "
|
|
232
|
-
}),
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
223
|
+
children: [
|
|
224
|
+
F && /* @__PURE__ */ h("div", {
|
|
225
|
+
role: "alert",
|
|
226
|
+
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",
|
|
227
|
+
children: F
|
|
228
|
+
}),
|
|
229
|
+
/* @__PURE__ */ h("p", {
|
|
230
|
+
className: "mb-3 text-xs text-text-muted",
|
|
231
|
+
children: "All digital app purchases are final and non-refundable. Once an order is completed it cannot be cancelled or reversed."
|
|
232
|
+
}),
|
|
233
|
+
/* @__PURE__ */ g("div", {
|
|
234
|
+
className: "flex items-center justify-between",
|
|
235
|
+
children: [/* @__PURE__ */ g("div", { children: [/* @__PURE__ */ h("p", {
|
|
236
|
+
className: "text-sm text-text-muted",
|
|
237
|
+
children: "Total"
|
|
238
|
+
}), /* @__PURE__ */ h("p", {
|
|
239
|
+
className: "text-2xl font-bold tabular-nums text-text-primary",
|
|
240
|
+
children: n(O)
|
|
241
|
+
})] }), /* @__PURE__ */ h("button", {
|
|
242
|
+
type: "button",
|
|
243
|
+
onClick: B,
|
|
244
|
+
disabled: N,
|
|
245
|
+
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",
|
|
246
|
+
children: N ? /* @__PURE__ */ g(m, { children: [/* @__PURE__ */ h(o, { className: "size-4 animate-spin" }), L("pages.cart.processing", { defaultValue: "Processing..." })] }) : /* @__PURE__ */ g(m, { children: [/* @__PURE__ */ h(p, { className: "size-4" }), L("pages.cart.proceedToCheckout", { defaultValue: "Proceed to Checkout" })] })
|
|
247
|
+
})]
|
|
248
|
+
})
|
|
249
|
+
]
|
|
243
250
|
}),
|
|
244
251
|
/* @__PURE__ */ g("button", {
|
|
245
252
|
type: "button",
|
|
@@ -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-dom';\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 { 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 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 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 ? error.message : '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=\"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 Review the apps and products you're about to buy, adjust quantities for physical\n items, and confirm the total before checkout. Nothing is charged until you proceed —\n once you do, your purchases become licenses you can install into a workspace.\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={`Remove ${item.product?.displayName ?? 'item'} from cart`}\n title=\"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\">Qty:</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 {formatPrice(item.unitPrice)} each\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 <div className=\"flex items-center justify-between\">\n <div>\n <p className=\"text-sm text-text-muted\">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-dom';\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 { 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 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 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 ? error.message : '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=\"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 Review the apps and products you're about to buy, adjust quantities for physical\n items, and confirm the total before checkout. Nothing is charged until you proceed —\n once you do, your purchases become licenses you can install into a workspace.\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={`Remove ${item.product?.displayName ?? 'item'} from cart`}\n title=\"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\">Qty:</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 {formatPrice(item.unitPrice)} each\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 All digital app purchases are final and non-refundable. Once an order is\n completed it cannot be cancelled or reversed.\n </p>\n <div className=\"flex items-center justify-between\">\n <div>\n <p className=\"text-sm text-text-muted\">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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"}
|
|
@@ -79,6 +79,10 @@ var b = "\n query OrderConfirmation($id: ID!) {\n getStoreOrder(id: $id) {\n
|
|
|
79
79
|
})
|
|
80
80
|
]
|
|
81
81
|
}),
|
|
82
|
+
/* @__PURE__ */ m("p", {
|
|
83
|
+
className: "mt-3 text-center text-xs text-text-tertiary",
|
|
84
|
+
children: "This purchase is final and non-refundable. Digital app orders cannot be cancelled once completed."
|
|
85
|
+
}),
|
|
82
86
|
j && /* @__PURE__ */ m("div", {
|
|
83
87
|
role: "alert",
|
|
84
88
|
className: "mt-4 rounded-md border border-border-danger bg-surface-danger-muted px-4 py-3 text-sm text-text-danger",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OrderConfirmationPage.js","names":[],"sources":["../../src/pages/OrderConfirmationPage.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useEffect, useState } from 'react';\nimport { useNavigate, useParams } from 'react-router-dom';\nimport { CheckCircle2, Receipt, ArrowRight, Copy, Download, KeyRound } from 'lucide-react';\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\nimport { EmphasisPanel, NextSteps } from '@burdenoff/fe-libs/ui';\nimport { useStore } from '../providers/StoreProvider';\nimport { nativeCopyText, nativeImpact, nativeNotify } from '../utils/nativeBridge';\nimport { formatPrice as formatCurrency } from '../utils';\n\ntype LineItem = {\n productId: string;\n name: string;\n quantity: number;\n unitPrice: number;\n subtotal: number;\n itemType?: string;\n};\n\ntype OrderDetails = {\n id: string;\n status: string;\n total: number;\n currency: string;\n purchasedAt?: string | null;\n lineItems: LineItem[];\n context?: Record<string, unknown> | null;\n};\n\ntype Entitlement = {\n id: string;\n productId: string;\n licenseKey: string | null;\n expiresAt: string | null;\n features: string[];\n product: {\n id: string;\n name: string;\n slug: string;\n icon: string | null;\n manifest: Record<string, unknown> | null;\n } | null;\n};\n\nconst ORDER_QUERY = /* GraphQL */ `\n query OrderConfirmation($id: ID!) {\n getStoreOrder(id: $id) {\n id\n status\n total\n currency\n purchasedAt\n lineItems\n context\n }\n myEntitlements {\n id\n productId\n licenseKey\n expiresAt\n features\n product {\n id\n name\n slug\n icon\n manifest\n }\n }\n }\n`;\n\nexport const OrderConfirmationPage: FC = () => {\n const navigate = useNavigate();\n const { basePath, authToken } = useStore();\n const { orderId } = useParams<{ orderId: string }>();\n const [order, setOrder] = useState<OrderDetails | null>(null);\n const [entitlements, setEntitlements] = useState<Entitlement[]>([]);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<string | null>(null);\n const [copiedKey, setCopiedKey] = useState<string | null>(null);\n\n useEffect(() => {\n if (!orderId) return;\n let cancelled = false;\n const load = async () => {\n setLoading(true);\n setError(null);\n try {\n const result = await graphqlFetch<{\n getStoreOrder: OrderDetails | null;\n myEntitlements: Entitlement[];\n }>({\n gateway: 'global',\n query: ORDER_QUERY,\n variables: { id: orderId },\n authToken: authToken || undefined,\n });\n if (cancelled) return;\n if (result.errors?.length) {\n throw new Error(result.errors[0]?.message ?? 'Failed to load order');\n }\n setOrder(result.data?.getStoreOrder ?? null);\n // Filter entitlements to only those matching products in this order\n const productIds = new Set(\n (result.data?.getStoreOrder?.lineItems ?? []).map((item) => item.productId)\n );\n const orderEntitlements = (result.data?.myEntitlements ?? []).filter((e) =>\n productIds.has(e.productId)\n );\n setEntitlements(orderEntitlements);\n } catch (err) {\n if (cancelled) return;\n setError(err instanceof Error ? err.message : 'Failed to load order');\n } finally {\n if (!cancelled) setLoading(false);\n }\n };\n void load();\n return () => {\n cancelled = true;\n };\n }, [orderId, authToken]);\n\n const copyKey = async (key: string) => {\n void nativeImpact('light');\n try {\n const ok = await nativeCopyText(key);\n if (ok) {\n setCopiedKey(key);\n void nativeNotify('success');\n setTimeout(() => setCopiedKey(null), 1500);\n } else {\n void nativeNotify('error');\n }\n } catch {\n void nativeNotify('error');\n }\n };\n\n const installInWorkspace = (productId: string, licenseKey: string | null) => {\n const params = new URLSearchParams();\n params.set('install', productId);\n if (licenseKey) params.set('licenseKey', licenseKey);\n // Navigate to the vibecontrols plugin install page via the top-level router.\n // Uses a plain anchor to avoid cross-MFE router imports.\n window.location.href = `/vibecontrols/agents?${params.toString()}`;\n };\n\n const formatPrice = (amount: number, currency: string) =>\n formatCurrency(amount, currency || 'USD');\n\n return (\n <main className=\"mx-auto max-w-3xl p-6\">\n <EmphasisPanel className=\"rounded-2xl p-6 text-center\">\n <CheckCircle2 className=\"mx-auto size-12 text-status-success-text\" />\n <h1 className=\"mt-3 text-2xl font-bold text-text-primary\">Order placed successfully</h1>\n {order && (\n <p className=\"mt-1 text-sm tabular-nums text-text-secondary\">\n Total: {formatPrice(order.total, order.currency)}\n {order.purchasedAt && ` • ${new Date(order.purchasedAt).toLocaleString()}`}\n </p>\n )}\n {orderId && (\n <div className=\"mx-auto mt-4 max-w-sm rounded-lg border border-border-seam bg-surface-default px-4 py-2 text-left\">\n <p className=\"text-xs uppercase tracking-wide text-text-tertiary\">Order ID</p>\n <p className=\"mt-0.5 break-all font-mono text-xs text-text-primary\">{orderId}</p>\n </div>\n )}\n </EmphasisPanel>\n\n {error && (\n <div\n role=\"alert\"\n className=\"mt-4 rounded-md border border-border-danger bg-surface-danger-muted px-4 py-3 text-sm text-text-danger\"\n >\n {error}\n </div>\n )}\n\n {loading ? (\n <output className=\"mt-6 block py-12 text-center text-text-secondary\">\n Loading licenses…\n </output>\n ) : entitlements.length > 0 ? (\n <div className=\"mt-6\">\n <h2 className=\"mb-3 flex items-center gap-2 text-sm font-semibold text-text-primary\">\n <KeyRound className=\"size-4\" />\n Your licenses ({entitlements.length})\n </h2>\n <ul className=\"space-y-3\">\n {entitlements.map((ent) => (\n <li\n key={ent.id}\n className=\"rounded-lg border border-border-seam bg-surface-raised p-4 transition-[box-shadow,border-color] hover:border-border-strong hover:shadow-[var(--shadow-pop)]\"\n >\n <div className=\"flex items-start gap-3\">\n {ent.product?.icon ? (\n <img\n src={ent.product.icon}\n alt=\"\"\n className=\"size-12 flex-shrink-0 rounded-md object-cover\"\n />\n ) : (\n <div className=\"flex size-12 flex-shrink-0 items-center justify-center rounded-md bg-surface-muted text-xs text-text-secondary\">\n {ent.product?.name.slice(0, 2).toUpperCase() ?? '?'}\n </div>\n )}\n <div className=\"min-w-0 flex-1\">\n <h3 className=\"text-base font-semibold text-text-primary\">\n {ent.product?.name ?? 'Unknown product'}\n </h3>\n {ent.expiresAt && (\n <p className=\"text-xs text-text-secondary\">\n Expires: {new Date(ent.expiresAt).toLocaleDateString()}\n </p>\n )}\n {ent.licenseKey && (\n <div className=\"mt-2 flex items-center gap-2\">\n <code className=\"flex-1 truncate rounded bg-surface-muted px-2 py-1 font-mono text-[11px] text-text-primary\">\n {ent.licenseKey}\n </code>\n <button\n type=\"button\"\n onClick={() => void copyKey(ent.licenseKey!)}\n className=\"rounded-md border border-border-default bg-surface-default p-1 text-text-primary hover:bg-surface-hover\"\n title=\"Copy license key\"\n >\n <Copy className=\"size-3.5\" />\n </button>\n </div>\n )}\n {copiedKey === ent.licenseKey && (\n <p className=\"mt-1 text-xs text-status-success-text\">Copied to clipboard</p>\n )}\n </div>\n <button\n type=\"button\"\n onClick={() => installInWorkspace(ent.productId, ent.licenseKey)}\n className=\"inline-flex items-center gap-1 rounded-md bg-action-primary-bg px-3 py-1.5 text-xs font-semibold text-action-primary-text hover:opacity-90\"\n >\n <Download className=\"size-3.5\" />\n Install to workspace\n </button>\n </div>\n </li>\n ))}\n </ul>\n </div>\n ) : null}\n\n {!loading && (\n <NextSteps\n className=\"mt-6\"\n storageKey=\"store-order-confirmation\"\n title=\"What's next?\"\n steps={[\n {\n id: 'install',\n label: 'Install what you bought',\n description: 'Add each purchased app to a workspace using its license.',\n icon: <Download className=\"size-4\" />,\n onClick: () => {\n const first = entitlements[0];\n if (first) installInWorkspace(first.productId, first.licenseKey);\n },\n done: entitlements.length === 0,\n },\n {\n id: 'orders',\n label: 'Keep this receipt',\n description: 'Find this order any time under My Orders.',\n icon: <Receipt className=\"size-4\" />,\n onClick: () => navigate(`${basePath}/orders`),\n },\n {\n id: 'shop',\n label: 'Discover more',\n description: 'Browse the marketplace for more apps and tools.',\n icon: <ArrowRight className=\"size-4\" />,\n onClick: () => navigate(`${basePath}/marketplace`),\n },\n ]}\n />\n )}\n\n <div className=\"mt-6 flex flex-col gap-2 sm:flex-row sm:justify-center\">\n <button\n type=\"button\"\n onClick={() => navigate(`${basePath}/orders`)}\n className=\"cursor-pointer inline-flex items-center justify-center gap-2 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-semibold text-action-primary-text\"\n >\n <Receipt className=\"size-4\" />\n View My Orders\n </button>\n <button\n type=\"button\"\n onClick={() => navigate(`${basePath}/marketplace`)}\n className=\"cursor-pointer inline-flex items-center justify-center gap-2 rounded-lg border border-border-default px-4 py-2 text-sm font-semibold text-text-primary hover:bg-surface-hover\"\n >\n Continue Shopping\n <ArrowRight className=\"size-4\" />\n </button>\n </div>\n </main>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"OrderConfirmationPage.js","names":[],"sources":["../../src/pages/OrderConfirmationPage.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useEffect, useState } from 'react';\nimport { useNavigate, useParams } from 'react-router-dom';\nimport { CheckCircle2, Receipt, ArrowRight, Copy, Download, KeyRound } from 'lucide-react';\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\nimport { EmphasisPanel, NextSteps } from '@burdenoff/fe-libs/ui';\nimport { useStore } from '../providers/StoreProvider';\nimport { nativeCopyText, nativeImpact, nativeNotify } from '../utils/nativeBridge';\nimport { formatPrice as formatCurrency } from '../utils';\n\ntype LineItem = {\n productId: string;\n name: string;\n quantity: number;\n unitPrice: number;\n subtotal: number;\n itemType?: string;\n};\n\ntype OrderDetails = {\n id: string;\n status: string;\n total: number;\n currency: string;\n purchasedAt?: string | null;\n lineItems: LineItem[];\n context?: Record<string, unknown> | null;\n};\n\ntype Entitlement = {\n id: string;\n productId: string;\n licenseKey: string | null;\n expiresAt: string | null;\n features: string[];\n product: {\n id: string;\n name: string;\n slug: string;\n icon: string | null;\n manifest: Record<string, unknown> | null;\n } | null;\n};\n\nconst ORDER_QUERY = /* GraphQL */ `\n query OrderConfirmation($id: ID!) {\n getStoreOrder(id: $id) {\n id\n status\n total\n currency\n purchasedAt\n lineItems\n context\n }\n myEntitlements {\n id\n productId\n licenseKey\n expiresAt\n features\n product {\n id\n name\n slug\n icon\n manifest\n }\n }\n }\n`;\n\nexport const OrderConfirmationPage: FC = () => {\n const navigate = useNavigate();\n const { basePath, authToken } = useStore();\n const { orderId } = useParams<{ orderId: string }>();\n const [order, setOrder] = useState<OrderDetails | null>(null);\n const [entitlements, setEntitlements] = useState<Entitlement[]>([]);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<string | null>(null);\n const [copiedKey, setCopiedKey] = useState<string | null>(null);\n\n useEffect(() => {\n if (!orderId) return;\n let cancelled = false;\n const load = async () => {\n setLoading(true);\n setError(null);\n try {\n const result = await graphqlFetch<{\n getStoreOrder: OrderDetails | null;\n myEntitlements: Entitlement[];\n }>({\n gateway: 'global',\n query: ORDER_QUERY,\n variables: { id: orderId },\n authToken: authToken || undefined,\n });\n if (cancelled) return;\n if (result.errors?.length) {\n throw new Error(result.errors[0]?.message ?? 'Failed to load order');\n }\n setOrder(result.data?.getStoreOrder ?? null);\n // Filter entitlements to only those matching products in this order\n const productIds = new Set(\n (result.data?.getStoreOrder?.lineItems ?? []).map((item) => item.productId)\n );\n const orderEntitlements = (result.data?.myEntitlements ?? []).filter((e) =>\n productIds.has(e.productId)\n );\n setEntitlements(orderEntitlements);\n } catch (err) {\n if (cancelled) return;\n setError(err instanceof Error ? err.message : 'Failed to load order');\n } finally {\n if (!cancelled) setLoading(false);\n }\n };\n void load();\n return () => {\n cancelled = true;\n };\n }, [orderId, authToken]);\n\n const copyKey = async (key: string) => {\n void nativeImpact('light');\n try {\n const ok = await nativeCopyText(key);\n if (ok) {\n setCopiedKey(key);\n void nativeNotify('success');\n setTimeout(() => setCopiedKey(null), 1500);\n } else {\n void nativeNotify('error');\n }\n } catch {\n void nativeNotify('error');\n }\n };\n\n const installInWorkspace = (productId: string, licenseKey: string | null) => {\n const params = new URLSearchParams();\n params.set('install', productId);\n if (licenseKey) params.set('licenseKey', licenseKey);\n // Navigate to the vibecontrols plugin install page via the top-level router.\n // Uses a plain anchor to avoid cross-MFE router imports.\n window.location.href = `/vibecontrols/agents?${params.toString()}`;\n };\n\n const formatPrice = (amount: number, currency: string) =>\n formatCurrency(amount, currency || 'USD');\n\n return (\n <main className=\"mx-auto max-w-3xl p-6\">\n <EmphasisPanel className=\"rounded-2xl p-6 text-center\">\n <CheckCircle2 className=\"mx-auto size-12 text-status-success-text\" />\n <h1 className=\"mt-3 text-2xl font-bold text-text-primary\">Order placed successfully</h1>\n {order && (\n <p className=\"mt-1 text-sm tabular-nums text-text-secondary\">\n Total: {formatPrice(order.total, order.currency)}\n {order.purchasedAt && ` • ${new Date(order.purchasedAt).toLocaleString()}`}\n </p>\n )}\n {orderId && (\n <div className=\"mx-auto mt-4 max-w-sm rounded-lg border border-border-seam bg-surface-default px-4 py-2 text-left\">\n <p className=\"text-xs uppercase tracking-wide text-text-tertiary\">Order ID</p>\n <p className=\"mt-0.5 break-all font-mono text-xs text-text-primary\">{orderId}</p>\n </div>\n )}\n </EmphasisPanel>\n\n <p className=\"mt-3 text-center text-xs text-text-tertiary\">\n This purchase is final and non-refundable. Digital app orders cannot be cancelled once\n completed.\n </p>\n\n {error && (\n <div\n role=\"alert\"\n className=\"mt-4 rounded-md border border-border-danger bg-surface-danger-muted px-4 py-3 text-sm text-text-danger\"\n >\n {error}\n </div>\n )}\n\n {loading ? (\n <output className=\"mt-6 block py-12 text-center text-text-secondary\">\n Loading licenses…\n </output>\n ) : entitlements.length > 0 ? (\n <div className=\"mt-6\">\n <h2 className=\"mb-3 flex items-center gap-2 text-sm font-semibold text-text-primary\">\n <KeyRound className=\"size-4\" />\n Your licenses ({entitlements.length})\n </h2>\n <ul className=\"space-y-3\">\n {entitlements.map((ent) => (\n <li\n key={ent.id}\n className=\"rounded-lg border border-border-seam bg-surface-raised p-4 transition-[box-shadow,border-color] hover:border-border-strong hover:shadow-[var(--shadow-pop)]\"\n >\n <div className=\"flex items-start gap-3\">\n {ent.product?.icon ? (\n <img\n src={ent.product.icon}\n alt=\"\"\n className=\"size-12 flex-shrink-0 rounded-md object-cover\"\n />\n ) : (\n <div className=\"flex size-12 flex-shrink-0 items-center justify-center rounded-md bg-surface-muted text-xs text-text-secondary\">\n {ent.product?.name.slice(0, 2).toUpperCase() ?? '?'}\n </div>\n )}\n <div className=\"min-w-0 flex-1\">\n <h3 className=\"text-base font-semibold text-text-primary\">\n {ent.product?.name ?? 'Unknown product'}\n </h3>\n {ent.expiresAt && (\n <p className=\"text-xs text-text-secondary\">\n Expires: {new Date(ent.expiresAt).toLocaleDateString()}\n </p>\n )}\n {ent.licenseKey && (\n <div className=\"mt-2 flex items-center gap-2\">\n <code className=\"flex-1 truncate rounded bg-surface-muted px-2 py-1 font-mono text-[11px] text-text-primary\">\n {ent.licenseKey}\n </code>\n <button\n type=\"button\"\n onClick={() => void copyKey(ent.licenseKey!)}\n className=\"rounded-md border border-border-default bg-surface-default p-1 text-text-primary hover:bg-surface-hover\"\n title=\"Copy license key\"\n >\n <Copy className=\"size-3.5\" />\n </button>\n </div>\n )}\n {copiedKey === ent.licenseKey && (\n <p className=\"mt-1 text-xs text-status-success-text\">Copied to clipboard</p>\n )}\n </div>\n <button\n type=\"button\"\n onClick={() => installInWorkspace(ent.productId, ent.licenseKey)}\n className=\"inline-flex items-center gap-1 rounded-md bg-action-primary-bg px-3 py-1.5 text-xs font-semibold text-action-primary-text hover:opacity-90\"\n >\n <Download className=\"size-3.5\" />\n Install to workspace\n </button>\n </div>\n </li>\n ))}\n </ul>\n </div>\n ) : null}\n\n {!loading && (\n <NextSteps\n className=\"mt-6\"\n storageKey=\"store-order-confirmation\"\n title=\"What's next?\"\n steps={[\n {\n id: 'install',\n label: 'Install what you bought',\n description: 'Add each purchased app to a workspace using its license.',\n icon: <Download className=\"size-4\" />,\n onClick: () => {\n const first = entitlements[0];\n if (first) installInWorkspace(first.productId, first.licenseKey);\n },\n done: entitlements.length === 0,\n },\n {\n id: 'orders',\n label: 'Keep this receipt',\n description: 'Find this order any time under My Orders.',\n icon: <Receipt className=\"size-4\" />,\n onClick: () => navigate(`${basePath}/orders`),\n },\n {\n id: 'shop',\n label: 'Discover more',\n description: 'Browse the marketplace for more apps and tools.',\n icon: <ArrowRight className=\"size-4\" />,\n onClick: () => navigate(`${basePath}/marketplace`),\n },\n ]}\n />\n )}\n\n <div className=\"mt-6 flex flex-col gap-2 sm:flex-row sm:justify-center\">\n <button\n type=\"button\"\n onClick={() => navigate(`${basePath}/orders`)}\n className=\"cursor-pointer inline-flex items-center justify-center gap-2 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-semibold text-action-primary-text\"\n >\n <Receipt className=\"size-4\" />\n View My Orders\n </button>\n <button\n type=\"button\"\n onClick={() => navigate(`${basePath}/marketplace`)}\n className=\"cursor-pointer inline-flex items-center justify-center gap-2 rounded-lg border border-border-default px-4 py-2 text-sm font-semibold text-text-primary hover:bg-surface-hover\"\n >\n Continue Shopping\n <ArrowRight className=\"size-4\" />\n </button>\n </div>\n </main>\n );\n};\n"],"mappings":"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"}
|