@base44/app-plugin-commerce 0.1.0

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 (173) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +117 -0
  3. package/base44/agents/commerce/StoreAdmin.jsonc +64 -0
  4. package/base44/entities/commerce.Cart.jsonc +73 -0
  5. package/base44/entities/commerce.Coupon.jsonc +113 -0
  6. package/base44/entities/commerce.Customer.jsonc +96 -0
  7. package/base44/entities/commerce.DownloadPermission.jsonc +54 -0
  8. package/base44/entities/commerce.EmailLog.jsonc +43 -0
  9. package/base44/entities/commerce.Order.jsonc +287 -0
  10. package/base44/entities/commerce.OrderNote.jsonc +31 -0
  11. package/base44/entities/commerce.OrderRefund.jsonc +64 -0
  12. package/base44/entities/commerce.PaymentGateway.jsonc +48 -0
  13. package/base44/entities/commerce.Product.jsonc +291 -0
  14. package/base44/entities/commerce.ProductAttribute.jsonc +39 -0
  15. package/base44/entities/commerce.ProductAttributeTerm.jsonc +38 -0
  16. package/base44/entities/commerce.ProductCategory.jsonc +51 -0
  17. package/base44/entities/commerce.ProductReview.jsonc +48 -0
  18. package/base44/entities/commerce.ProductTag.jsonc +30 -0
  19. package/base44/entities/commerce.ProductVariation.jsonc +167 -0
  20. package/base44/entities/commerce.ShippingClass.jsonc +30 -0
  21. package/base44/entities/commerce.ShippingZone.jsonc +41 -0
  22. package/base44/entities/commerce.ShippingZoneMethod.jsonc +84 -0
  23. package/base44/entities/commerce.StoreSettings.jsonc +23 -0
  24. package/base44/entities/commerce.TaxClass.jsonc +23 -0
  25. package/base44/entities/commerce.TaxRate.jsonc +68 -0
  26. package/base44/entities/commerce.Webhook.jsonc +57 -0
  27. package/base44/entities/commerce.WebhookDelivery.jsonc +45 -0
  28. package/base44/functions/commerce/admin-coupons/entry.ts +100 -0
  29. package/base44/functions/commerce/admin-customers/entry.ts +141 -0
  30. package/base44/functions/commerce/admin-orders/entry.ts +396 -0
  31. package/base44/functions/commerce/admin-orders/helpers.ts +246 -0
  32. package/base44/functions/commerce/admin-products/entry.ts +506 -0
  33. package/base44/functions/commerce/admin-refunds/entry.ts +158 -0
  34. package/base44/functions/commerce/admin-reports/entry.ts +283 -0
  35. package/base44/functions/commerce/admin-reviews/entry.ts +66 -0
  36. package/base44/functions/commerce/admin-tools/entry.ts +261 -0
  37. package/base44/functions/commerce/admin-webhooks/entry.ts +52 -0
  38. package/base44/functions/commerce/payment-webhook/entry.ts +135 -0
  39. package/base44/functions/commerce/payments/entry.ts +238 -0
  40. package/base44/functions/commerce/seed-store/defaults.ts +162 -0
  41. package/base44/functions/commerce/seed-store/entry.ts +310 -0
  42. package/base44/functions/commerce/seed-store/sample-data.ts +349 -0
  43. package/base44/functions/commerce/storefront-account/entry.ts +207 -0
  44. package/base44/functions/commerce/storefront-cart/cart-pricing.ts +258 -0
  45. package/base44/functions/commerce/storefront-cart/entry.ts +283 -0
  46. package/base44/functions/commerce/storefront-catalog/entry.ts +459 -0
  47. package/base44/functions/commerce/storefront-checkout/cart-pricing.ts +258 -0
  48. package/base44/functions/commerce/storefront-checkout/entry.ts +485 -0
  49. package/base44/shared/commerce/auth.ts +60 -0
  50. package/base44/shared/commerce/coupons.ts +257 -0
  51. package/base44/shared/commerce/data/continents.ts +75 -0
  52. package/base44/shared/commerce/data/countries.ts +307 -0
  53. package/base44/shared/commerce/data/currencies.ts +46 -0
  54. package/base44/shared/commerce/email-templates.ts +240 -0
  55. package/base44/shared/commerce/emails.ts +225 -0
  56. package/base44/shared/commerce/money.ts +66 -0
  57. package/base44/shared/commerce/orders.ts +251 -0
  58. package/base44/shared/commerce/payments.ts +495 -0
  59. package/base44/shared/commerce/reviews.ts +36 -0
  60. package/base44/shared/commerce/scan.ts +57 -0
  61. package/base44/shared/commerce/sequence.ts +35 -0
  62. package/base44/shared/commerce/settings.ts +57 -0
  63. package/base44/shared/commerce/shipping.ts +215 -0
  64. package/base44/shared/commerce/stock.ts +227 -0
  65. package/base44/shared/commerce/stripe.ts +463 -0
  66. package/base44/shared/commerce/tax.ts +136 -0
  67. package/base44/shared/commerce/totals.ts +314 -0
  68. package/base44/shared/commerce/webhooks.ts +116 -0
  69. package/package.json +37 -0
  70. package/scripts/install.js +156 -0
  71. package/skills/commerce/SKILL.md +62 -0
  72. package/skills/commerce/docs/api-admin.md +186 -0
  73. package/skills/commerce/docs/api-storefront.md +408 -0
  74. package/skills/commerce/installation-guidelines.md +91 -0
  75. package/skills/commerce/post-installation.md +157 -0
  76. package/skills/commerce/references/emails.md +13 -0
  77. package/skills/commerce/references/guest-access-security.md +18 -0
  78. package/skills/commerce/references/limits-and-performance.md +16 -0
  79. package/skills/commerce/references/media-and-downloads.md +4 -0
  80. package/skills/commerce/references/online-payments.md +201 -0
  81. package/skills/commerce/references/product-render.md +87 -0
  82. package/skills/commerce/references/scheduled-work.md +19 -0
  83. package/skills/commerce/references/storefront-product-page.md +83 -0
  84. package/skills/commerce/references/webhooks.md +8 -0
  85. package/src/commerce/admin/README.md +107 -0
  86. package/src/commerce/admin/bot/Markdown.jsx +138 -0
  87. package/src/commerce/admin/bot/StoreAdminBot.jsx +249 -0
  88. package/src/commerce/admin/bot/pipe-tables.js +116 -0
  89. package/src/commerce/admin/components/AddressForm.jsx +78 -0
  90. package/src/commerce/admin/components/ConfirmDialog.jsx +52 -0
  91. package/src/commerce/admin/components/CountrySelect.jsx +81 -0
  92. package/src/commerce/admin/components/DataTable.jsx +192 -0
  93. package/src/commerce/admin/components/DateRangePicker.jsx +91 -0
  94. package/src/commerce/admin/components/EmptyState.jsx +17 -0
  95. package/src/commerce/admin/components/MediaUploader.jsx +116 -0
  96. package/src/commerce/admin/components/MetaDataEditor.jsx +45 -0
  97. package/src/commerce/admin/components/MoneyInput.jsx +50 -0
  98. package/src/commerce/admin/components/PageHeader.jsx +29 -0
  99. package/src/commerce/admin/components/RichTextarea.jsx +21 -0
  100. package/src/commerce/admin/components/SearchSelect.jsx +142 -0
  101. package/src/commerce/admin/components/StatusBadge.jsx +17 -0
  102. package/src/commerce/admin/context/BasePathContext.jsx +26 -0
  103. package/src/commerce/admin/context/SettingsContext.jsx +207 -0
  104. package/src/commerce/admin/hooks/useAsync.js +46 -0
  105. package/src/commerce/admin/hooks/useDebounce.js +11 -0
  106. package/src/commerce/admin/hooks/useMoney.js +52 -0
  107. package/src/commerce/admin/hooks/usePagedList.js +83 -0
  108. package/src/commerce/admin/hooks/usePaymentProvider.js +27 -0
  109. package/src/commerce/admin/hooks/useRealtime.js +129 -0
  110. package/src/commerce/admin/index.jsx +34 -0
  111. package/src/commerce/admin/layout/AccessDenied.jsx +54 -0
  112. package/src/commerce/admin/layout/AdminLayout.jsx +33 -0
  113. package/src/commerce/admin/layout/AuthGuard.jsx +84 -0
  114. package/src/commerce/admin/layout/Sidebar.jsx +130 -0
  115. package/src/commerce/admin/layout/Topbar.jsx +94 -0
  116. package/src/commerce/admin/lib/api.js +55 -0
  117. package/src/commerce/admin/lib/constants.js +157 -0
  118. package/src/commerce/admin/lib/format.js +27 -0
  119. package/src/commerce/admin/lib/geo-data.js +125 -0
  120. package/src/commerce/admin/lib/order-utils.js +147 -0
  121. package/src/commerce/admin/lib/paths.js +35 -0
  122. package/src/commerce/admin/lib/product-utils.js +55 -0
  123. package/src/commerce/admin/pages/Dashboard.jsx +245 -0
  124. package/src/commerce/admin/pages/coupons/CouponEditor.jsx +565 -0
  125. package/src/commerce/admin/pages/coupons/CouponsList.jsx +172 -0
  126. package/src/commerce/admin/pages/customers/CustomerEditor.jsx +318 -0
  127. package/src/commerce/admin/pages/customers/CustomersList.jsx +169 -0
  128. package/src/commerce/admin/pages/orders/OrderEditor.jsx +952 -0
  129. package/src/commerce/admin/pages/orders/OrdersList.jsx +227 -0
  130. package/src/commerce/admin/pages/orders/components/AddProductDialog.jsx +149 -0
  131. package/src/commerce/admin/pages/orders/components/DownloadPermissionsPanel.jsx +119 -0
  132. package/src/commerce/admin/pages/orders/components/LineItemsTable.jsx +208 -0
  133. package/src/commerce/admin/pages/orders/components/OrderNotesPanel.jsx +123 -0
  134. package/src/commerce/admin/pages/orders/components/PaymentPanel.jsx +199 -0
  135. package/src/commerce/admin/pages/orders/components/RefundPanel.jsx +239 -0
  136. package/src/commerce/admin/pages/orders/components/TotalsBox.jsx +52 -0
  137. package/src/commerce/admin/pages/products/AttributeTerms.jsx +180 -0
  138. package/src/commerce/admin/pages/products/Attributes.jsx +183 -0
  139. package/src/commerce/admin/pages/products/Categories.jsx +236 -0
  140. package/src/commerce/admin/pages/products/ProductEditor.jsx +267 -0
  141. package/src/commerce/admin/pages/products/ProductsList.jsx +391 -0
  142. package/src/commerce/admin/pages/products/Reviews.jsx +255 -0
  143. package/src/commerce/admin/pages/products/Tags.jsx +150 -0
  144. package/src/commerce/admin/pages/products/components/ProductDataPanel.jsx +132 -0
  145. package/src/commerce/admin/pages/products/components/PublishBox.jsx +101 -0
  146. package/src/commerce/admin/pages/products/components/TaxonomyPanel.jsx +243 -0
  147. package/src/commerce/admin/pages/products/components/tabs/AdvancedTab.jsx +48 -0
  148. package/src/commerce/admin/pages/products/components/tabs/AttributesTab.jsx +208 -0
  149. package/src/commerce/admin/pages/products/components/tabs/DownloadsTab.jsx +91 -0
  150. package/src/commerce/admin/pages/products/components/tabs/ExternalTab.jsx +41 -0
  151. package/src/commerce/admin/pages/products/components/tabs/GeneralTab.jsx +103 -0
  152. package/src/commerce/admin/pages/products/components/tabs/InventoryTab.jsx +93 -0
  153. package/src/commerce/admin/pages/products/components/tabs/LinkedTab.jsx +102 -0
  154. package/src/commerce/admin/pages/products/components/tabs/ShippingTab.jsx +86 -0
  155. package/src/commerce/admin/pages/products/components/tabs/VariationsTab.jsx +377 -0
  156. package/src/commerce/admin/pages/reports/Reports.jsx +416 -0
  157. package/src/commerce/admin/pages/settings/EmailsSettings.jsx +240 -0
  158. package/src/commerce/admin/pages/settings/GeneralSettings.jsx +232 -0
  159. package/src/commerce/admin/pages/settings/InventorySettings.jsx +146 -0
  160. package/src/commerce/admin/pages/settings/PaymentsSettings.jsx +260 -0
  161. package/src/commerce/admin/pages/settings/ProductsSettings.jsx +118 -0
  162. package/src/commerce/admin/pages/settings/SettingsLayout.jsx +53 -0
  163. package/src/commerce/admin/pages/settings/ShippingSettings.jsx +304 -0
  164. package/src/commerce/admin/pages/settings/ShippingZoneEditor.jsx +514 -0
  165. package/src/commerce/admin/pages/settings/TaxRatesTable.jsx +231 -0
  166. package/src/commerce/admin/pages/settings/TaxSettings.jsx +281 -0
  167. package/src/commerce/admin/pages/settings/useGroupForm.jsx +76 -0
  168. package/src/commerce/admin/pages/status/WebhookEditor.jsx +296 -0
  169. package/src/commerce/admin/pages/status/Webhooks.jsx +53 -0
  170. package/src/commerce/admin/routes.jsx +151 -0
  171. package/src/commerce/utils/index.js +19 -0
  172. package/src/commerce/utils/shipping-promos.js +99 -0
  173. package/src/commerce/utils/variants.js +411 -0
@@ -0,0 +1,83 @@
1
+ # Variant selection on the product page
2
+
3
+ The one interaction agents reliably get wrong: turning a variable product into controls a customer can buy from. Everything else about the page — what to show, and which fields each call returns — is [`product-render.md`](./product-render.md); request/response shapes are [`docs/api-storefront.md`](../docs/api-storefront.md).
4
+
5
+ The logic ships: **`src/commerce/utils/variants.js`** (framework-free, no I/O, full JSDoc on every export — read it for signatures). Import it and spend your effort on the markup.
6
+
7
+ ---
8
+
9
+ ## 1. The model, and the rule
10
+
11
+ `get-product` returns `{ product, variations }`. `product.attributes[]` entries with `variation: true` are the **axes**, in `position` order; each `variations[]` record is one combination. `visible: true` + `variation: false` attributes are descriptive — a spec table, **never** a selector.
12
+
13
+ **One control per axis. Never one option per variation.** Do not flatten `variations[]` into `Red / S`, `Red / M`, `Blue / S`, … — that is `n × m` noise, it hides the product's structure, and it stops scaling immediately.
14
+
15
+ ```
16
+ Size ( ○ S )( ● M )( ○ L ) ← one control per attribute
17
+ Color ( ● Red )( ○ Blue )
18
+ → resolved variation: Red / M · $21.99 · 10 in stock
19
+ ```
20
+
21
+ Pick the control by option count: swatches for colours, pills for a few short options, a `Select` for many or long labels. The axis label is `attribute.name`.
22
+
23
+ ## 2. One call gives you the whole view
24
+
25
+ ```jsx
26
+ import { resolveSelection, defaultSelection, selectOption } from "@/commerce/utils";
27
+
28
+ const [selection, setSelection] = useState(() => defaultSelection(product, variations));
29
+ const view = resolveSelection(product, variations, selection);
30
+ const pick = (axisKey, option) =>
31
+ setSelection((prev) => selectOption(product, variations, prev, axisKey, option));
32
+ ```
33
+
34
+ Bind the UI to `view` — not to `product.*` — so a selection actually changes the page:
35
+
36
+ | `view` field | Binds to |
37
+ |---|---|
38
+ | `axes` | the controls: `[{ key, name, attribute_id, options[] }]`, dead options already dropped |
39
+ | `display` | image, price, `regular_price`, `on_sale`, SKU, stock, weight/dimensions, description — variation-first with parent fallback. `display.image` is `{src, name, alt}`, or **`null` when the product has no images** — the one case needing a placeholder in the UI |
40
+ | `availability[axisKey][option]` | per-option state: `"available"` / `"out_of_stock"` / `"unavailable"` — §3 |
41
+ | `priceRange` | `{ min, max, on_sale, count }` while the selection is incomplete — §3 |
42
+ | `complete` / `missingAxes` | the button label: `Select a ${view.missingAxes[0]?.name}` |
43
+ | `purchasable` | gate **Add to cart** on this (resolved *and* buyable) |
44
+ | `addToCart` | `{ product_id, variation_id }` to spread into the cart call, or `null` |
45
+
46
+ `view.variation` and `view.candidates` are there when you need the record itself or the variations still reachable. For a `simple` product `resolveSelection` returns no axes, `complete: true` and a ready `addToCart` — the same code path; it branches on `product.type`, so `variations` can be `[]`.
47
+
48
+ A **variable product with no usable variations** — `type: "variable"` but nothing in `variations[]`, the state an attribute record alone leaves you in — is deliberately *not* given a parent fallback, because `add-item` would reject it. You get axes whose `options` arrays are empty, `purchasable: false` and `addToCart: null`. Render it as unavailable rather than painting selector groups with no buttons, and guard the "Select a …" label: with no `variation: true` attributes at all, `missingAxes` is empty and `view.missingAxes[0]?.name` is `undefined`.
49
+
50
+ The other exports cover what the view model doesn't: `selectionForVariation` (variation → selection, to hydrate controls from a cart line or `?variation=` link), `selectionToParams`/`selectionFromParams` (URL round-trip), `priceRange`, and the individual pieces if you build your own view model. Signatures and edge cases are in the JSDoc; two behaviors are easy to miss when hand-rolling: non-publishable variations never leak an option into the UI, and an empty `option` on an axis means **"any"**.
51
+
52
+ ## 3. The three UI decisions the helpers can't make for you
53
+
54
+ - **Unavailable vs out of stock.** `"unavailable"` (no such combination) → **disable, don't hide**; options that vanish and reappear as the customer clicks are disorienting. `"out_of_stock"` → keep it visible and label it. `onbackorder` counts as available and purchasable — label it ("Ships in 2–3 weeks"), don't disable it.
55
+ - **Incomplete selection.** Show `view.priceRange` (`$21.99 – $23.99`, or `From $21.99`) — never `$0`, and never the parent `price`, which for a variable product is not its price ([`product-render.md` §3](./product-render.md#3-product-types-in-each-view)). Keep **Add to cart** disabled with a hint at what's missing.
56
+ - **Initial state and linkability.** Start from `defaultSelection` (honours the merchant's `default_attributes`, preselects single-option axes). Mirror the selection into the URL with `selectionToParams` and hydrate with `selectionFromParams`, so a chosen variant is shareable and survives a refresh.
57
+
58
+ Route every click through `selectOption` and dead ends take care of themselves: it keeps the just-picked option and clears only conflicting axes (Red/L → click Blue → `{ Color: Blue }`, size re-open) instead of leaving the customer on a combination that resolves to nothing.
59
+
60
+ ## 4. Add to cart
61
+
62
+ ```js
63
+ await base44.functions.invoke("commerce/storefront-cart", {
64
+ action: "add-item",
65
+ cart_token,
66
+ ...view.addToCart, // { product_id, variation_id } — variation_id is REQUIRED for type "variable"
67
+ quantity,
68
+ });
69
+ ```
70
+
71
+ Handle `400 variation_required` (empty `variation_id` on a variable product — the page has a bug), the stock codes (`out_of_stock`, `insufficient_stock`, …), and `404 variation_not_found` (catalog changed underneath the page: refetch `get-product`). `product.sold_individually` caps the quantity control at 1.
72
+
73
+ ## 5. Checklist
74
+
75
+ - [ ] One control per axis in `view.axes` — no combination list anywhere in the UI.
76
+ - [ ] Non-variation visible attributes render as specs, not selectors.
77
+ - [ ] Image, price, SKU and availability all come from `view.display`, with a placeholder for `display.image === null`.
78
+ - [ ] Incomplete selection shows `view.priceRange` — never `$0`, never the parent `price`.
79
+ - [ ] `"unavailable"` disabled, `"out_of_stock"` labelled; every click routed through `selectOption`.
80
+ - [ ] Initial state from `defaultSelection`; selection mirrored into the URL.
81
+ - [ ] **Add to cart** gated on `view.purchasable`, sending `view.addToCart`.
82
+ - [ ] A variable product with empty `axes[].options` shows as unavailable — no empty selector groups, no "Select a undefined".
83
+ - [ ] Tags, ratings, upsells and the rest per [`product-render.md`](./product-render.md).
@@ -0,0 +1,8 @@
1
+ # Webhooks
2
+
3
+ `webhooks.ts` `dispatch()` fires on `order.*`, `product.*`, `customer.*`, `coupon.*` (created/updated/deleted, plus order/product `restored`). Each active `commerce.Webhook` matching the topic receives an HTTP POST with headers `X-Commerce-Webhook-Topic/-Resource/-Event/-ID/-Delivery-ID/-Signature`. The signature is **base64 HMAC-SHA256** of the body, keyed by the webhook's `secret` (Web Crypto).
4
+
5
+ - Every attempt is logged as a `commerce.WebhookDelivery` (request/response bodies truncated to 32 KB). Prune with `commerce/admin-tools` `prune-webhook-deliveries`.
6
+ - `failure_count` increments on non-2xx/timeout and resets on success; a webhook auto-disables after **5** consecutive failures.
7
+ - The `secret` is stored on the (admin-only-RLS) `commerce.Webhook` entity. For higher assurance, move it to Base44 secrets and read it in `dispatch()`.
8
+ - Verify deliveries on the receiver by recomputing the HMAC over the raw body with your secret.
@@ -0,0 +1,107 @@
1
+ # Store Admin (`src/commerce/admin/`)
2
+
3
+ React admin UI for the Base44 commerce template. Copy this
4
+ folder into a Base44 app built on the default template (Vite + React +
5
+ Tailwind + shadcn/ui + React Router) to get a full store back office.
6
+
7
+ > Install docs: [`skills/commerce/installation-guidelines.md`](../../../skills/commerce/installation-guidelines.md) · mounting & role setup: [`skills/commerce/post-installation.md`](../../../skills/commerce/post-installation.md) · architecture & operations: the commerce skill, [`skills/commerce/SKILL.md`](../../../skills/commerce/SKILL.md)
8
+ > API references: [`skills/commerce/docs/api-admin.md`](../../../skills/commerce/docs/api-admin.md), [`skills/commerce/docs/api-storefront.md`](../../../skills/commerce/docs/api-storefront.md)
9
+
10
+ ## Mounting
11
+
12
+ 1. Deploy the backend first (`base44/` entities + functions — see the root README).
13
+ 2. Copy this folder to `src/commerce/admin/` in your app.
14
+ 3. Confirm the deps — `sonner`, `recharts` and `react-markdown` all come with
15
+ the default template and are all this folder needs. Install any that are
16
+ missing:
17
+
18
+ ```bash
19
+ npm i sonner recharts
20
+ ```
21
+
22
+ 4. Mount the app in your router:
23
+
24
+ ```jsx
25
+ import AdminApp from "@/commerce/admin";
26
+
27
+ <Route path="/admin/*" element={<AdminApp />} />
28
+ // mounted elsewhere? → <AdminApp basePath="/backoffice" />
29
+ ```
30
+
31
+ 5. Make sure your user has the **admin role** (Base44 dashboard → Users, or
32
+ `base44.users.inviteUser(email, "admin")`). Signed-in users without the
33
+ admin role get an access-denied screen — do not weaken this check.
34
+ 6. Open `/admin`. If the store has never been seeded (no `general` StoreSettings
35
+ group) the first-run screen appears and seeds store defaults, optionally with
36
+ sample data when the store has no products yet. If `commerce/seed-store` was
37
+ already run during installation — including when an agent generated a real
38
+ catalog — the store counts as ready and this screen never shows; see
39
+ `skills/commerce/post-installation.md` §2.
40
+
41
+ ## External touchpoints
42
+
43
+ The folder is self-contained; it only imports from:
44
+
45
+ - `@/api/base44Client` — the app's SDK client (**named `base44` export**, as in
46
+ Base44's default app template; if your app uses a default export instead,
47
+ adjust the single import in `lib/api.js`)
48
+ - `@/components/ui/*` — the host app's shadcn/ui kit
49
+ - `react-router-dom`, `lucide-react`, `sonner`, `recharts`, `react-markdown`
50
+ (GFM tables in bot replies are rendered by `bot/pipe-tables.js`, so no markdown
51
+ plugin is needed)
52
+
53
+ The StoreAdmin bot panel (`bot/`) additionally requires the `commerce/StoreAdmin`
54
+ agent (`base44/agents/commerce/StoreAdmin.jsonc`), which uses the `commerce/*`
55
+ backend functions directly as its tools.
56
+
57
+ ### Required shadcn/ui primitives
58
+
59
+ alert, alert-dialog, badge, button, calendar, card, checkbox, command, dialog,
60
+ dropdown-menu, input, label, popover, radio-group, scroll-area, select,
61
+ separator, sheet, skeleton, switch, table, tabs, textarea, tooltip
62
+
63
+ The default Base44 template ships all of these. If one is missing:
64
+
65
+ ```bash
66
+ npx shadcn@latest add <component>
67
+ ```
68
+
69
+ ## Layout of this folder
70
+
71
+ ```
72
+ index.jsx AdminApp: providers → auth guard → layout → routes
73
+ routes.jsx Route table + <AdminRoutes/>
74
+ layout/ AdminLayout, Sidebar, Topbar, AuthGuard (admin-role gate), AccessDenied
75
+ bot/ StoreAdminBot (chat panel over the commerce/StoreAdmin agent), Markdown (GFM renderer)
76
+ context/ SettingsContext (store settings + first-run seeding), BasePathContext
77
+ hooks/ useAsync, usePagedList, useRealtime (live updates), useMoney, useDebounce,
78
+ usePaymentProvider (is an online payment provider connected?)
79
+ lib/ api (function calls), constants, format, geo-data, order/product utils
80
+ components/ DataTable, SearchSelect, MoneyInput, DateRangePicker, AddressForm, …
81
+ pages/ All admin pages (orders, products, coupons, customers, reports, settings, webhooks)
82
+ ```
83
+
84
+ ## Live-updating views
85
+
86
+ The dashboard, the orders list and the reports tabs refresh themselves — a new
87
+ order or a status change shows up without the admin reloading the page.
88
+
89
+ `hooks/useRealtime.js` does this in two layers: the SDK's realtime entity
90
+ subscriptions (`base44.entities["commerce.Order"].subscribe(...)`) as the primary
91
+ push channel, plus a slow polling fallback that **turns itself off for good the
92
+ first time a push event arrives**. The fallback is there because most commerce
93
+ writes happen inside backend functions under the service role; if those don't
94
+ emit socket events, push alone would never fire.
95
+
96
+ To make another page live, pair it with the quiet refetch on the data hooks so
97
+ the refresh doesn't flash skeletons over content:
98
+
99
+ ```jsx
100
+ const list = usePagedList(fetcher, { deps: [] });
101
+ useRealtime(["commerce.Product"], () => list.refetchQuiet());
102
+ ```
103
+
104
+ Use a slower `fallbackPollMs` (e.g. `{ fallbackPollMs: 60000 }`) for anything
105
+ backed by a scan-heavy report action, and `0` to disable polling entirely.
106
+ Refreshes are debounced, are skipped entirely while the tab is hidden, and run
107
+ once when the tab is focused again.
@@ -0,0 +1,138 @@
1
+ import React from "react";
2
+ import { Link } from "react-router-dom";
3
+ import ReactMarkdown from "react-markdown";
4
+ import { splitPipeTables } from "./pipe-tables";
5
+ import { useAdminHref } from "../context/BasePathContext";
6
+
7
+ /**
8
+ * Markdown renderer for StoreAdmin bot messages.
9
+ *
10
+ * `react-markdown` ships with the default Base44 template, so this component
11
+ * adds **no dependency of its own**: GFM pipe tables — which the agent is
12
+ * instructed to use for every list and report, and which CommonMark would show
13
+ * as literal pipes — are split out by `pipe-tables.js` and rendered here, while
14
+ * all other markdown goes through react-markdown. Tables scroll horizontally
15
+ * inside their own container.
16
+ */
17
+ /**
18
+ * `admin:` links are the agent's way to send the operator to a page it cannot
19
+ * act on itself (`[Settings → Tax](admin:settings/tax)`). Resolved through
20
+ * useAdminHref so they follow the actual mount point, and routed in-app rather
21
+ * than opening a tab. Everything else stays an external link.
22
+ */
23
+ const ADMIN_LINK = /^admin:/i;
24
+
25
+ function MessageLink({ href = "", children, ...props }) {
26
+ const adminHref = useAdminHref();
27
+ if (ADMIN_LINK.test(href)) {
28
+ return (
29
+ <Link to={adminHref(href.replace(ADMIN_LINK, ""))} className="text-primary underline underline-offset-2">
30
+ {children}
31
+ </Link>
32
+ );
33
+ }
34
+ return (
35
+ <a
36
+ href={href}
37
+ className="text-primary underline underline-offset-2"
38
+ target="_blank"
39
+ rel="noreferrer"
40
+ {...props}
41
+ >
42
+ {children}
43
+ </a>
44
+ );
45
+ }
46
+
47
+ const components = {
48
+ p: ({ node, ...props }) => <p className="mb-2 leading-relaxed last:mb-0" {...props} />,
49
+ ul: ({ node, ...props }) => <ul className="mb-2 list-disc space-y-0.5 pl-5 last:mb-0" {...props} />,
50
+ ol: ({ node, ...props }) => <ol className="mb-2 list-decimal space-y-0.5 pl-5 last:mb-0" {...props} />,
51
+ h1: ({ node, ...props }) => <h1 className="mb-1.5 mt-2 text-sm font-semibold first:mt-0" {...props} />,
52
+ h2: ({ node, ...props }) => <h2 className="mb-1.5 mt-2 text-sm font-semibold first:mt-0" {...props} />,
53
+ h3: ({ node, ...props }) => <h3 className="mb-1 mt-2 text-[13px] font-semibold first:mt-0" {...props} />,
54
+ h4: ({ node, ...props }) => <h4 className="mb-1 mt-2 text-[13px] font-semibold first:mt-0" {...props} />,
55
+ a: ({ node, ...props }) => <MessageLink {...props} />,
56
+ code: ({ node, inline, className, ...props }) =>
57
+ inline ? (
58
+ <code className="rounded bg-muted px-1 py-0.5 font-mono text-[11px]" {...props} />
59
+ ) : (
60
+ <code className={`font-mono text-[11px] ${className || ""}`} {...props} />
61
+ ),
62
+ pre: ({ node, ...props }) => (
63
+ <pre className="my-2 overflow-x-auto rounded-md bg-muted p-2.5" {...props} />
64
+ ),
65
+ blockquote: ({ node, ...props }) => (
66
+ <blockquote className="my-2 border-l-2 pl-3 text-muted-foreground" {...props} />
67
+ ),
68
+ hr: ({ node, ...props }) => <hr className="my-3" {...props} />,
69
+ };
70
+
71
+ /** Inline variant for table cells: no block wrapper around the content. */
72
+ const inlineComponents = {
73
+ ...components,
74
+ p: ({ node, ...props }) => <span {...props} />,
75
+ };
76
+
77
+ /** Cells are usually plain text — only parse the ones that carry inline markup. */
78
+ const INLINE_MARKUP = /[*_`[\]<>]/;
79
+
80
+ function Cell({ text }) {
81
+ if (!INLINE_MARKUP.test(text)) return text;
82
+ return <ReactMarkdown components={inlineComponents}>{text}</ReactMarkdown>;
83
+ }
84
+
85
+ const alignClass = (align) =>
86
+ align === "right" ? "text-right" : align === "center" ? "text-center" : "text-left";
87
+
88
+ function PipeTable({ head, align, rows }) {
89
+ return (
90
+ <div className="my-2 w-full overflow-x-auto rounded-md border">
91
+ <table className="w-full border-collapse text-xs">
92
+ <thead className="bg-muted/60">
93
+ <tr>
94
+ {head.map((cell, i) => (
95
+ <th
96
+ key={i}
97
+ className={`whitespace-nowrap border-b px-2.5 py-1.5 font-semibold ${alignClass(align[i])}`}
98
+ >
99
+ <Cell text={cell} />
100
+ </th>
101
+ ))}
102
+ </tr>
103
+ </thead>
104
+ <tbody>
105
+ {rows.map((row, r) => (
106
+ <tr key={r}>
107
+ {row.map((cell, c) => (
108
+ <td
109
+ key={c}
110
+ className={`border-b px-2.5 py-1.5 align-top last:border-b-0 [tr:last-child>&]:border-b-0 ${alignClass(align[c])}`}
111
+ >
112
+ <Cell text={cell} />
113
+ </td>
114
+ ))}
115
+ </tr>
116
+ ))}
117
+ </tbody>
118
+ </table>
119
+ </div>
120
+ );
121
+ }
122
+
123
+ export default function Markdown({ children }) {
124
+ const segments = splitPipeTables(children);
125
+ return (
126
+ <div className="text-[13px] [&>*:first-child]:mt-0 [&>*:last-child]:mb-0">
127
+ {segments.map((segment, i) =>
128
+ segment.type === "table" ? (
129
+ <PipeTable key={i} head={segment.head} align={segment.align} rows={segment.rows} />
130
+ ) : (
131
+ <ReactMarkdown key={i} components={components}>
132
+ {segment.value}
133
+ </ReactMarkdown>
134
+ )
135
+ )}
136
+ </div>
137
+ );
138
+ }
@@ -0,0 +1,249 @@
1
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@/components/ui/sheet";
3
+ import { Button } from "@/components/ui/button";
4
+ import { Textarea } from "@/components/ui/textarea";
5
+ import { Badge } from "@/components/ui/badge";
6
+ import { Loader2, RotateCcw, Send, Sparkles, Wrench, XCircle } from "lucide-react";
7
+ import { toast } from "sonner";
8
+ import { base44 } from "../lib/api";
9
+ import Markdown from "./Markdown";
10
+
11
+ /** Agent identifier — base44/agents/commerce/StoreAdmin.jsonc, namespaced by its folder. */
12
+ const AGENT_NAME = "commerce/StoreAdmin";
13
+ /** sessionStorage key so the conversation survives panel close / page reload. */
14
+ const CONVERSATION_KEY = "commerce.StoreAdmin.conversation_id";
15
+
16
+ const SUGGESTIONS = [
17
+ "Show today's sales summary",
18
+ "List the 10 most recent orders",
19
+ "Which products are low on stock?",
20
+ ];
21
+
22
+ /** One chat message bubble (user right, assistant left with markdown + tool chips). */
23
+ function Message({ message }) {
24
+ const isUser = message.role === "user";
25
+ const content =
26
+ typeof message.content === "string"
27
+ ? message.content
28
+ : message.content
29
+ ? JSON.stringify(message.content, null, 2)
30
+ : "";
31
+ const toolCalls = (message.tool_calls || []).filter((t) => t.name);
32
+
33
+ return (
34
+ <div className={`flex ${isUser ? "justify-end" : "justify-start"}`}>
35
+ <div
36
+ className={`max-w-[92%] rounded-lg px-3 py-2 ${
37
+ isUser ? "bg-primary text-primary-foreground" : "bg-muted"
38
+ }`}
39
+ >
40
+ {toolCalls.length > 0 && (
41
+ <div className="mb-1.5 flex flex-wrap gap-1">
42
+ {toolCalls.map((t) => (
43
+ <Badge key={t.id} variant="outline" className="gap-1 bg-background/60 px-1.5 py-0 text-[10px] font-normal">
44
+ {t.status === "running" ? (
45
+ <Loader2 className="h-2.5 w-2.5 animate-spin" />
46
+ ) : t.status === "error" ? (
47
+ <XCircle className="h-2.5 w-2.5 text-destructive" />
48
+ ) : (
49
+ <Wrench className="h-2.5 w-2.5" />
50
+ )}
51
+ {t.name.replace(/^commerce[-/]/, "")}
52
+ </Badge>
53
+ ))}
54
+ </div>
55
+ )}
56
+ {isUser ? (
57
+ <div className="whitespace-pre-wrap text-[13px]">{content}</div>
58
+ ) : content ? (
59
+ <Markdown>{content}</Markdown>
60
+ ) : null}
61
+ </div>
62
+ </div>
63
+ );
64
+ }
65
+
66
+ /**
67
+ * StoreAdmin bot — chat side panel over the `commerce/StoreAdmin` agent
68
+ * (base44/agents/commerce/StoreAdmin.jsonc).
69
+ *
70
+ * Launched from the admin sidebar. Creates the conversation lazily on the
71
+ * first message, then live-updates through the agents WebSocket subscription.
72
+ * Assistant responses render as GitHub-flavored markdown (incl. tables).
73
+ */
74
+ export default function StoreAdminBot({ open, onOpenChange }) {
75
+ const [conversationId, setConversationId] = useState(
76
+ () => sessionStorage.getItem(CONVERSATION_KEY) || null
77
+ );
78
+ const [messages, setMessages] = useState([]);
79
+ const [input, setInput] = useState("");
80
+ const [sending, setSending] = useState(false);
81
+ const scrollRef = useRef(null);
82
+ const conversationRef = useRef(null);
83
+
84
+ const visibleMessages = useMemo(
85
+ () => messages.filter((m) => !m.hidden && m.role !== "system"),
86
+ [messages]
87
+ );
88
+
89
+ // Agent is busy while the last visible message is the user's, or a tool call is running.
90
+ const busy = useMemo(() => {
91
+ const last = visibleMessages[visibleMessages.length - 1];
92
+ if (!last) return false;
93
+ if (last.role === "user") return true;
94
+ return (last.tool_calls || []).some((t) => t.status === "running");
95
+ }, [visibleMessages]);
96
+
97
+ // Load + subscribe whenever the panel is open and a conversation exists.
98
+ useEffect(() => {
99
+ if (!open || !conversationId) return undefined;
100
+ let cancelled = false;
101
+ base44.agents
102
+ .getConversation(conversationId)
103
+ .then((conv) => {
104
+ if (cancelled || !conv) return;
105
+ conversationRef.current = conv;
106
+ setMessages(conv.messages || []);
107
+ })
108
+ .catch(() => {
109
+ // Stale id (e.g. conversation deleted) — start fresh.
110
+ sessionStorage.removeItem(CONVERSATION_KEY);
111
+ setConversationId(null);
112
+ setMessages([]);
113
+ });
114
+ const unsubscribe = base44.agents.subscribeToConversation(conversationId, (conv) => {
115
+ if (cancelled) return;
116
+ conversationRef.current = conv;
117
+ setMessages(conv.messages || []);
118
+ });
119
+ return () => {
120
+ cancelled = true;
121
+ unsubscribe?.();
122
+ };
123
+ }, [open, conversationId]);
124
+
125
+ // Keep the newest message in view.
126
+ useEffect(() => {
127
+ const el = scrollRef.current;
128
+ if (el) el.scrollTop = el.scrollHeight;
129
+ }, [visibleMessages, busy, open]);
130
+
131
+ const send = useCallback(
132
+ async (text) => {
133
+ const trimmed = (text ?? input).trim();
134
+ if (!trimmed || sending) return;
135
+ setSending(true);
136
+ try {
137
+ let conv = conversationRef.current;
138
+ if (!conv || conv.id !== conversationId || !conversationId) {
139
+ conv = await base44.agents.createConversation({
140
+ agent_name: AGENT_NAME,
141
+ metadata: { source: "commerce-admin" },
142
+ });
143
+ conversationRef.current = conv;
144
+ sessionStorage.setItem(CONVERSATION_KEY, conv.id);
145
+ setConversationId(conv.id);
146
+ }
147
+ setInput("");
148
+ // Optimistic echo; the subscription replaces it with the stored message.
149
+ setMessages((prev) => [
150
+ ...prev,
151
+ { id: `local-${Date.now()}`, role: "user", content: trimmed },
152
+ ]);
153
+ await base44.agents.addMessage(conv, { role: "user", content: trimmed });
154
+ } catch (err) {
155
+ toast.error(err?.response?.data?.message || err.message || "Failed to reach StoreAdmin");
156
+ } finally {
157
+ setSending(false);
158
+ }
159
+ },
160
+ [input, sending, conversationId]
161
+ );
162
+
163
+ const reset = useCallback(() => {
164
+ sessionStorage.removeItem(CONVERSATION_KEY);
165
+ conversationRef.current = null;
166
+ setConversationId(null);
167
+ setMessages([]);
168
+ }, []);
169
+
170
+ const onKeyDown = (e) => {
171
+ if (e.key === "Enter" && !e.shiftKey) {
172
+ e.preventDefault();
173
+ send();
174
+ }
175
+ };
176
+
177
+ return (
178
+ <Sheet open={open} onOpenChange={onOpenChange}>
179
+ <SheetContent side="right" className="flex w-full flex-col gap-0 p-0 sm:max-w-md">
180
+ <SheetHeader className="border-b px-4 py-3">
181
+ <div className="flex items-center justify-between">
182
+ <SheetTitle className="flex items-center gap-2 text-base">
183
+ <Sparkles className="h-4 w-4 text-primary" />
184
+ StoreAdmin
185
+ </SheetTitle>
186
+ <Button
187
+ variant="ghost"
188
+ size="sm"
189
+ className="mr-6 h-7 gap-1.5 px-2 text-xs text-muted-foreground"
190
+ onClick={reset}
191
+ disabled={!conversationId && messages.length === 0}
192
+ >
193
+ <RotateCcw className="h-3 w-3" />
194
+ New chat
195
+ </Button>
196
+ </div>
197
+ </SheetHeader>
198
+
199
+ <div ref={scrollRef} className="flex-1 space-y-3 overflow-y-auto px-4 py-3">
200
+ {visibleMessages.length === 0 && (
201
+ <div className="mt-6 space-y-3 text-center">
202
+ <p className="text-sm text-muted-foreground">
203
+ Ask me anything about your store — I can look things up and make changes for you.
204
+ </p>
205
+ <div className="flex flex-col items-stretch gap-1.5">
206
+ {SUGGESTIONS.map((s) => (
207
+ <Button
208
+ key={s}
209
+ variant="outline"
210
+ size="sm"
211
+ className="justify-start text-xs font-normal text-muted-foreground"
212
+ onClick={() => send(s)}
213
+ >
214
+ {s}
215
+ </Button>
216
+ ))}
217
+ </div>
218
+ </div>
219
+ )}
220
+ {visibleMessages.map((m) => (
221
+ <Message key={m.id} message={m} />
222
+ ))}
223
+ {busy && (
224
+ <div className="flex items-center gap-2 px-1 text-xs text-muted-foreground">
225
+ <Loader2 className="h-3 w-3 animate-spin" />
226
+ Working…
227
+ </div>
228
+ )}
229
+ </div>
230
+
231
+ <div className="border-t p-3">
232
+ <div className="flex items-end gap-2">
233
+ <Textarea
234
+ value={input}
235
+ onChange={(e) => setInput(e.target.value)}
236
+ onKeyDown={onKeyDown}
237
+ placeholder="e.g. Show pending orders as a table"
238
+ rows={1}
239
+ className="max-h-32 min-h-9 resize-none text-[13px]"
240
+ />
241
+ <Button size="icon" className="h-9 w-9 shrink-0" onClick={() => send()} disabled={sending || !input.trim()}>
242
+ {sending ? <Loader2 className="h-4 w-4 animate-spin" /> : <Send className="h-4 w-4" />}
243
+ </Button>
244
+ </div>
245
+ </div>
246
+ </SheetContent>
247
+ </Sheet>
248
+ );
249
+ }