@base44/app-plugin-commerce 0.1.3 → 0.1.5

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 (86) hide show
  1. package/README.md +11 -11
  2. package/base44/agents/commerce/StoreAdmin.jsonc +7 -7
  3. package/base44/entities/commerce.Order.jsonc +1 -1
  4. package/base44/entities/commerce.PaymentGateway.jsonc +3 -3
  5. package/base44/entities/commerce.Product.jsonc +9 -45
  6. package/base44/entities/commerce.ProductAttribute.jsonc +7 -19
  7. package/base44/entities/commerce.ProductAttributeTerm.jsonc +4 -10
  8. package/base44/entities/commerce.ProductTag.jsonc +1 -8
  9. package/base44/entities/commerce.ProductVariation.jsonc +1 -1
  10. package/base44/entities/commerce.StoreSettings.jsonc +1 -1
  11. package/base44/functions/commerce/admin-products/entry.ts +73 -121
  12. package/base44/functions/commerce/admin-refunds/entry.ts +1 -1
  13. package/base44/functions/commerce/admin-reports/entry.ts +1 -1
  14. package/base44/functions/commerce/admin-tools/entry.ts +4 -3
  15. package/base44/functions/commerce/payments/entry.ts +0 -2
  16. package/base44/functions/commerce/seed-store/defaults.ts +13 -39
  17. package/base44/functions/commerce/seed-store/entry.ts +50 -166
  18. package/base44/functions/commerce/seed-store/sample-data.ts +28 -66
  19. package/base44/functions/commerce/seed-store/seed-catalog.ts +717 -0
  20. package/base44/functions/commerce/storefront-cart/entry.ts +0 -7
  21. package/base44/functions/commerce/storefront-catalog/entry.ts +12 -21
  22. package/base44/functions/commerce/storefront-checkout/entry.ts +5 -12
  23. package/base44/shared/commerce/catalog.ts +134 -0
  24. package/base44/shared/commerce/coupons.ts +4 -7
  25. package/base44/shared/commerce/email-templates.ts +15 -13
  26. package/base44/shared/commerce/emails.ts +4 -3
  27. package/base44/shared/commerce/payments.ts +8 -20
  28. package/base44/shared/commerce/products.ts +24 -0
  29. package/base44/shared/commerce/settings.ts +2 -3
  30. package/base44/shared/commerce/stock.ts +2 -4
  31. package/base44/shared/commerce/totals.ts +37 -42
  32. package/package.json +1 -1
  33. package/scripts/install.js +23 -7
  34. package/skills/commerce/SKILL.md +9 -7
  35. package/skills/commerce/docs/api-admin.md +20 -14
  36. package/skills/commerce/docs/api-storefront.md +39 -35
  37. package/skills/commerce/installation-guidelines.md +8 -7
  38. package/skills/commerce/post-installation.md +205 -84
  39. package/skills/commerce/references/admin-product-form.md +56 -0
  40. package/skills/commerce/references/emails.md +4 -3
  41. package/skills/commerce/references/guest-access-security.md +2 -2
  42. package/skills/commerce/references/limits-and-performance.md +1 -1
  43. package/skills/commerce/references/online-payments.md +8 -8
  44. package/skills/commerce/references/product-render.md +25 -23
  45. package/skills/commerce/references/reviews.md +20 -0
  46. package/skills/commerce/references/storefront-product-page.md +9 -9
  47. package/skills/commerce/references/webhooks.md +3 -1
  48. package/src/commerce/admin/README.md +4 -4
  49. package/src/commerce/admin/context/BasePathContext.jsx +5 -5
  50. package/src/commerce/admin/context/SettingsContext.jsx +4 -4
  51. package/src/commerce/admin/index.jsx +3 -3
  52. package/src/commerce/admin/layout/Sidebar.jsx +1 -9
  53. package/src/commerce/admin/lib/constants.js +0 -7
  54. package/src/commerce/admin/lib/paths.js +6 -6
  55. package/src/commerce/admin/lib/product-utils.js +18 -15
  56. package/src/commerce/admin/pages/orders/components/AddProductDialog.jsx +8 -8
  57. package/src/commerce/admin/pages/products/ProductEditor.jsx +10 -12
  58. package/src/commerce/admin/pages/products/ProductsList.jsx +3 -15
  59. package/src/commerce/admin/pages/products/components/AttributesSection.jsx +426 -0
  60. package/src/commerce/admin/pages/products/components/ProductDataPanel.jsx +33 -69
  61. package/src/commerce/admin/pages/products/components/TaxonomyPanel.jsx +1 -1
  62. package/src/commerce/admin/pages/products/components/tabs/LinkedTab.jsx +1 -14
  63. package/src/commerce/admin/pages/products/components/tabs/ModifiersTab.jsx +19 -0
  64. package/src/commerce/admin/pages/products/components/tabs/PriceInventoryTab.jsx +728 -0
  65. package/src/commerce/admin/pages/reports/Reports.jsx +3 -3
  66. package/src/commerce/admin/pages/settings/EmailsSettings.jsx +12 -14
  67. package/src/commerce/admin/pages/settings/GeneralSettings.jsx +6 -116
  68. package/src/commerce/admin/pages/settings/PaymentsSettings.jsx +101 -62
  69. package/src/commerce/admin/pages/settings/SettingsLayout.jsx +2 -6
  70. package/src/commerce/admin/pages/settings/TaxSettings.jsx +0 -1
  71. package/src/commerce/admin/pages/status/WebhookEditor.jsx +3 -3
  72. package/src/commerce/admin/pages/status/Webhooks.jsx +2 -2
  73. package/src/commerce/admin/routes.jsx +10 -19
  74. package/src/commerce/utils/index.js +2 -2
  75. package/src/commerce/utils/shipping-promos.js +9 -6
  76. package/src/commerce/utils/variants.js +21 -21
  77. package/src/commerce/admin/pages/products/AttributeTerms.jsx +0 -180
  78. package/src/commerce/admin/pages/products/Attributes.jsx +0 -183
  79. package/src/commerce/admin/pages/products/Tags.jsx +0 -150
  80. package/src/commerce/admin/pages/products/components/tabs/AdvancedTab.jsx +0 -48
  81. package/src/commerce/admin/pages/products/components/tabs/AttributesTab.jsx +0 -208
  82. package/src/commerce/admin/pages/products/components/tabs/ExternalTab.jsx +0 -41
  83. package/src/commerce/admin/pages/products/components/tabs/GeneralTab.jsx +0 -103
  84. package/src/commerce/admin/pages/products/components/tabs/InventoryTab.jsx +0 -93
  85. package/src/commerce/admin/pages/products/components/tabs/ShippingTab.jsx +0 -86
  86. package/src/commerce/admin/pages/products/components/tabs/VariationsTab.jsx +0 -377
@@ -28,7 +28,7 @@ import DataTable from "../../components/DataTable";
28
28
  import StatusBadge from "../../components/StatusBadge";
29
29
  import {
30
30
  COUPON_TYPES,
31
- PRODUCT_TYPES,
31
+ PRODUCT_STATUSES,
32
32
  REVIEW_STATUSES,
33
33
  } from "../../lib/constants";
34
34
 
@@ -365,9 +365,9 @@ function TotalsTab() {
365
365
  renderLabel={(r) => <StatusBadge status={r.key} className="text-[10px]" />}
366
366
  />
367
367
  <TotalsCard
368
- title="Products by type"
368
+ title="Products by status"
369
369
  loading={loading}
370
- rows={normalizeTotals(d["products-totals"], PRODUCT_TYPES)}
370
+ rows={normalizeTotals(d["products-totals"], PRODUCT_STATUSES)}
371
371
  />
372
372
  <TotalsCard title="Customers" loading={loading} rows={normalizeTotals(d["customers-totals"])} />
373
373
  <TotalsCard
@@ -24,18 +24,16 @@ import {
24
24
  import { Settings2 } from "lucide-react";
25
25
  import { EMAIL_TYPES } from "../../lib/constants";
26
26
  import { call } from "../../lib/api";
27
- import { useSettings } from "../../context/SettingsContext";
28
27
  import useGroupForm from "./useGroupForm";
29
28
 
30
29
  const DEFAULTS = {
31
- from_name: "", // blank: the backend inherits general.store_name
30
+ store_name: "",
32
31
  admin_recipients: [],
33
32
  };
34
33
 
35
34
  export default function EmailsSettings() {
36
35
  const { form, setField, SaveButton } = useGroupForm("emails", DEFAULTS);
37
- const { get } = useSettings();
38
- const storeName = get("general", "store_name", "");
36
+ const storeName = form.store_name || "";
39
37
  const [activeId, setActiveId] = useState(null);
40
38
  // Resolved by the backend — listing admin users needs service role.
41
39
  const [effective, setEffective] = useState(null);
@@ -46,8 +44,7 @@ export default function EmailsSettings() {
46
44
  .catch(() => setEffective(null));
47
45
  }, []);
48
46
 
49
- // Same shape as the "From" name field: the fallback is the placeholder, and the
50
- // hint bolds the value actually in use.
47
+ // The fallback is the placeholder, and the hint bolds the value actually in use.
51
48
  const adminUsers = (effective?.admin_users ?? []).join(", ");
52
49
 
53
50
  // Per-type config lives at the top level of the `emails` group, keyed by type
@@ -75,23 +72,24 @@ export default function EmailsSettings() {
75
72
  <div className="space-y-1.5">
76
73
  <CardTitle>Sender options</CardTitle>
77
74
  <CardDescription>
78
- The name transactional emails are sent from. The sending address is controlled by your Base44
79
- email configuration, not here.
75
+ How transactional emails are named, and who gets the admin copies. The sending
76
+ address is controlled by your Base44 email configuration, not here.
80
77
  </CardDescription>
81
78
  </div>
82
79
  <SaveButton />
83
80
  </CardHeader>
84
81
  <CardContent className="grid gap-4 sm:grid-cols-2">
85
82
  <div className="space-y-1.5 sm:col-span-2">
86
- <Label>“From” name</Label>
83
+ <Label>Store name</Label>
87
84
  <Input
88
- value={form.from_name || ""}
89
- placeholder={storeName || "Your store name"}
90
- onChange={(e) => setField("from_name", e.target.value)}
85
+ value={form.store_name || ""}
86
+ placeholder="Your store's name"
87
+ onChange={(e) => setField("store_name", e.target.value)}
91
88
  />
92
89
  <p className="text-xs text-muted-foreground">
93
- Leave blank to send as {storeName ? <strong>{storeName}</strong> : "your store name"} —
94
- the store name from Settings → General.
90
+ Used in the subject line and as the “From” name — e.g.{" "}
91
+ “[{storeName || "Your store"}]: New order #1002”, sent from {storeName || "Your store"}.
92
+ Leave it blank to send as your app's name.
95
93
  </p>
96
94
  </div>
97
95
  <div className="space-y-1.5 sm:col-span-2">
@@ -2,7 +2,6 @@ import React from "react";
2
2
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
3
3
  import { Input } from "@/components/ui/input";
4
4
  import { Label } from "@/components/ui/label";
5
- import { Checkbox } from "@/components/ui/checkbox";
6
5
  import {
7
6
  Select,
8
7
  SelectContent,
@@ -10,19 +9,11 @@ import {
10
9
  SelectTrigger,
11
10
  SelectValue,
12
11
  } from "@/components/ui/select";
13
- import CountrySelect from "../../components/CountrySelect";
14
- import { statesFor } from "../../lib/geo-data";
15
12
  import { CURRENCIES, CURRENCY_POSITIONS } from "../../lib/constants";
16
13
  import useGroupForm from "./useGroupForm";
17
14
 
18
15
  const DEFAULTS = {
19
- store_name: "",
20
- store_url: "",
21
16
  order_received_path: "/order-received",
22
- address: { address_1: "", address_2: "", city: "", state: "", postcode: "", country: "US" },
23
- enable_taxes: true,
24
- enable_coupons: true,
25
- calc_discounts_sequentially: false,
26
17
  currency: "USD",
27
18
  currency_position: "left",
28
19
  thousand_sep: ",",
@@ -32,41 +23,19 @@ const DEFAULTS = {
32
23
 
33
24
  export default function GeneralSettings() {
34
25
  const { form, setField, SaveButton } = useGroupForm("general", DEFAULTS);
35
- const address = form.address || DEFAULTS.address;
36
- const states = statesFor(address.country);
37
-
38
- const setAddress = (key, value) => setField("address", { ...address, [key]: value });
39
26
 
40
27
  return (
41
28
  <div className="max-w-3xl space-y-4">
42
29
  <Card>
43
30
  <CardHeader className="flex-row items-start justify-between gap-4 space-y-0">
44
31
  <div className="space-y-1.5">
45
- <CardTitle>Store details</CardTitle>
46
- <CardDescription>This is where your business is located. Tax rates and shipping rates use this address.</CardDescription>
32
+ <CardTitle>Checkout</CardTitle>
33
+ <CardDescription>Where customers land once they come back from paying.</CardDescription>
47
34
  </div>
48
35
  <SaveButton />
49
36
  </CardHeader>
50
- <CardContent className="grid gap-4 sm:grid-cols-2">
51
- <div className="space-y-1.5 sm:col-span-2">
52
- <Label>Store name</Label>
53
- <Input value={form.store_name || ""} onChange={(e) => setField("store_name", e.target.value)} />
54
- </div>
55
- <div className="space-y-1.5 sm:col-span-2">
56
- <Label>Storefront URL</Label>
57
- <Input
58
- type="url"
59
- inputMode="url"
60
- placeholder="https://shop.example.com"
61
- value={form.store_url || ""}
62
- onChange={(e) => setField("store_url", e.target.value)}
63
- />
64
- <p className="text-xs text-muted-foreground">
65
- Where customers shop. Used to build the page a customer returns to after paying — leave it
66
- empty and the app's own address is used.
67
- </p>
68
- </div>
69
- <div className="space-y-1.5 sm:col-span-2">
37
+ <CardContent>
38
+ <div className="space-y-1.5">
70
39
  <Label>Payment return path</Label>
71
40
  <Input
72
41
  placeholder="/order-received"
@@ -74,89 +43,10 @@ export default function GeneralSettings() {
74
43
  onChange={(e) => setField("order_received_path", e.target.value)}
75
44
  />
76
45
  <p className="text-xs text-muted-foreground">
77
- The storefront route customers come back to after paying. It must match the page you built —
78
- a wrong path sends paying customers to a 404.
46
+ The storefront route customers come back to after paying, on your storefront's own
47
+ address. It must match the page you built — a wrong path sends paying customers to a 404.
79
48
  </p>
80
49
  </div>
81
- <div className="space-y-1.5">
82
- <Label>Address line 1</Label>
83
- <Input value={address.address_1 || ""} onChange={(e) => setAddress("address_1", e.target.value)} />
84
- </div>
85
- <div className="space-y-1.5">
86
- <Label>Address line 2</Label>
87
- <Input value={address.address_2 || ""} onChange={(e) => setAddress("address_2", e.target.value)} />
88
- </div>
89
- <div className="space-y-1.5">
90
- <Label>City</Label>
91
- <Input value={address.city || ""} onChange={(e) => setAddress("city", e.target.value)} />
92
- </div>
93
- <div className="space-y-1.5">
94
- <Label>Postcode / ZIP</Label>
95
- <Input value={address.postcode || ""} onChange={(e) => setAddress("postcode", e.target.value)} />
96
- </div>
97
- <div className="space-y-1.5">
98
- <Label>Country</Label>
99
- <CountrySelect
100
- value={address.country}
101
- onChange={(code) => setField("address", { ...address, country: code, state: "" })}
102
- />
103
- </div>
104
- <div className="space-y-1.5">
105
- <Label>State / Province</Label>
106
- {states ? (
107
- <Select value={address.state || ""} onValueChange={(v) => setAddress("state", v)}>
108
- <SelectTrigger>
109
- <SelectValue placeholder="Select state…" />
110
- </SelectTrigger>
111
- <SelectContent>
112
- {states.map((st) => (
113
- <SelectItem key={st.code} value={st.code}>
114
- {st.name}
115
- </SelectItem>
116
- ))}
117
- </SelectContent>
118
- </Select>
119
- ) : (
120
- <Input value={address.state || ""} onChange={(e) => setAddress("state", e.target.value)} />
121
- )}
122
- </div>
123
- </CardContent>
124
- </Card>
125
-
126
- <Card>
127
- <CardHeader className="flex-row items-start justify-between gap-4 space-y-0">
128
- <div className="space-y-1.5">
129
- <CardTitle>Store options</CardTitle>
130
- <CardDescription>
131
- Where you ship is controlled by your shipping zones — see Settings → Shipping.
132
- </CardDescription>
133
- </div>
134
- <SaveButton />
135
- </CardHeader>
136
- <CardContent className="space-y-4">
137
- <div className="space-y-2">
138
- <label className="flex items-center gap-2 text-sm">
139
- <Checkbox
140
- checked={!!form.enable_taxes}
141
- onCheckedChange={(v) => setField("enable_taxes", !!v)}
142
- />
143
- Enable tax rates and calculations
144
- </label>
145
- <label className="flex items-center gap-2 text-sm">
146
- <Checkbox
147
- checked={!!form.enable_coupons}
148
- onCheckedChange={(v) => setField("enable_coupons", !!v)}
149
- />
150
- Enable the use of coupon codes
151
- </label>
152
- <label className="flex items-center gap-2 text-sm">
153
- <Checkbox
154
- checked={!!form.calc_discounts_sequentially}
155
- onCheckedChange={(v) => setField("calc_discounts_sequentially", !!v)}
156
- />
157
- Calculate coupon discounts sequentially
158
- </label>
159
- </div>
160
50
  </CardContent>
161
51
  </Card>
162
52
 
@@ -1,5 +1,5 @@
1
1
  import React, { useState } from "react";
2
- import { Card, CardContent } from "@/components/ui/card";
2
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
3
3
  import { Button } from "@/components/ui/button";
4
4
  import { Input } from "@/components/ui/input";
5
5
  import { Label } from "@/components/ui/label";
@@ -88,25 +88,92 @@ export default function PaymentsSettings() {
88
88
  }
89
89
 
90
90
  return (
91
- <div className="max-w-3xl space-y-3">
92
- {rows.map((g, i) => {
93
- const isOnline = g.slug === provider.gatewaySlug;
94
- return (
95
- <Card key={g.id}>
96
- <CardContent className="flex items-center gap-4 py-4">
97
- <div className="flex flex-col">
98
- <Button variant="ghost" size="icon" className="h-6 w-6" disabled={i === 0 || busy} onClick={() => move(i, -1)}>
99
- <ArrowUp className="h-3.5 w-3.5" />
100
- </Button>
101
- <Button variant="ghost" size="icon" className="h-6 w-6" disabled={i === rows.length - 1 || busy} onClick={() => move(i, 1)}>
102
- <ArrowDown className="h-3.5 w-3.5" />
103
- </Button>
104
- </div>
105
- <div className="min-w-0 flex-1">
106
- <div className="font-medium">{g.method_title || g.title || g.slug}</div>
107
- <div className="truncate text-sm text-muted-foreground">{g.method_description || g.description}</div>
91
+ <div className="max-w-3xl">
92
+ <Card>
93
+ <CardHeader className="pb-3">
94
+ <CardTitle>Checkout options</CardTitle>
95
+ <CardDescription>
96
+ Which payment options customers can pick at checkout, in this order, and the wording they
97
+ see. This is the storefront's menu only — switching an option on or off, or renaming it,
98
+ does not connect, disconnect or configure any payment provider.
99
+ </CardDescription>
100
+ </CardHeader>
101
+ <CardContent className="divide-y border-t p-0">
102
+ {rows.map((g, i) => {
103
+ const isOnline = g.slug === provider.gatewaySlug;
104
+ return (
105
+ <div key={g.id} className="space-y-3 px-6 py-4">
106
+ <div className="flex items-start gap-4">
107
+ <div className="flex flex-col pt-0.5">
108
+ <Button
109
+ variant="ghost"
110
+ size="icon"
111
+ className="h-6 w-6"
112
+ disabled={i === 0 || busy}
113
+ onClick={() => move(i, -1)}
114
+ >
115
+ <ArrowUp className="h-3.5 w-3.5" />
116
+ </Button>
117
+ <Button
118
+ variant="ghost"
119
+ size="icon"
120
+ className="h-6 w-6"
121
+ disabled={i === rows.length - 1 || busy}
122
+ onClick={() => move(i, 1)}
123
+ >
124
+ <ArrowDown className="h-3.5 w-3.5" />
125
+ </Button>
126
+ </div>
127
+
128
+ <div className="min-w-0 flex-1">
129
+ <div className="font-medium">{g.method_title || g.title || g.slug}</div>
130
+ <div className="text-sm text-muted-foreground">
131
+ {g.method_description || g.description}
132
+ </div>
133
+ </div>
134
+
135
+ <div className="flex shrink-0 items-center gap-2 pt-0.5">
136
+ <Switch
137
+ checked={!!g.enabled}
138
+ disabled={busy}
139
+ title={
140
+ isOnline && !provider.connected
141
+ ? "Stays hidden from customers until a payment provider is connected"
142
+ : undefined
143
+ }
144
+ onCheckedChange={(v) => toggle(g, v)}
145
+ />
146
+ {/* Every option's customer-facing wording is editable, the card
147
+ one included — and a button on every row keeps the toggles
148
+ in one column. */}
149
+ <Button
150
+ variant="outline"
151
+ size="sm"
152
+ onClick={() =>
153
+ setDialog({
154
+ gateway: {
155
+ ...g,
156
+ settings: {
157
+ ...(g.settings || {}),
158
+ account_details:
159
+ g.slug === "offline"
160
+ ? g.settings?.account_details || []
161
+ : g.settings?.account_details,
162
+ },
163
+ },
164
+ })
165
+ }
166
+ >
167
+ <Settings2 className="mr-2 h-4 w-4" />
168
+ Labels
169
+ </Button>
170
+ </div>
171
+ </div>
172
+
173
+ {/* Full width, below the row: inside it the tall notice drags the
174
+ arrows and the switch down to the vertical middle. */}
108
175
  {isOnline && !provider.loading && (
109
- <Alert className="mt-3">
176
+ <Alert>
110
177
  <Info className="h-4 w-4" />
111
178
  {provider.connected ? (
112
179
  <>
@@ -121,11 +188,11 @@ export default function PaymentsSettings() {
121
188
  <>
122
189
  <AlertTitle>No payment provider connected</AlertTitle>
123
190
  <AlertDescription>
124
- Card payments are ready to go but need a provider: connect the{" "}
125
- {provider.providerLabel || "payment"} connector in this app's integrations, and
126
- this option starts appearing at checkout — no code to write. Until then it stays
127
- hidden from customers, so nobody can pick it and pay nothing. Prefer a different
128
- provider? See{" "}
191
+ Card payments are ready to go but need a provider: set{" "}
192
+ {provider.providerLabel || "a payment provider"} up in this app's
193
+ integrations, and this option starts appearing at checkout — no code to
194
+ write. Until then it stays hidden from customers, so nobody can pick it and
195
+ pay nothing. Prefer a different provider? See{" "}
129
196
  <code className="text-xs">skills/commerce/references/online-payments.md</code>.
130
197
  </AlertDescription>
131
198
  </>
@@ -133,42 +200,10 @@ export default function PaymentsSettings() {
133
200
  </Alert>
134
201
  )}
135
202
  </div>
136
- <div className="flex items-center gap-2">
137
- <Switch
138
- checked={!!g.enabled}
139
- disabled={busy}
140
- title={
141
- isOnline && !provider.connected
142
- ? "Stays hidden from customers until a payment provider is connected"
143
- : undefined
144
- }
145
- onCheckedChange={(v) => toggle(g, v)}
146
- />
147
- {!isOnline && (
148
- <Button
149
- variant="outline"
150
- size="sm"
151
- onClick={() =>
152
- setDialog({
153
- gateway: {
154
- ...g,
155
- settings: {
156
- ...(g.settings || {}),
157
- account_details: g.slug === "bacs" ? (g.settings?.account_details || []) : g.settings?.account_details,
158
- },
159
- },
160
- })
161
- }
162
- >
163
- <Settings2 className="mr-2 h-4 w-4" />
164
- Manage
165
- </Button>
166
- )}
167
- </div>
168
- </CardContent>
169
- </Card>
170
- );
171
- })}
203
+ );
204
+ })}
205
+ </CardContent>
206
+ </Card>
172
207
 
173
208
  {dialog && (
174
209
  <Dialog open onOpenChange={(o) => !o && setDialog(null)}>
@@ -194,7 +229,7 @@ export default function PaymentsSettings() {
194
229
  />
195
230
  </div>
196
231
 
197
- {dialog.gateway.slug === "bacs" && (
232
+ {dialog.gateway.slug === "offline" && (
198
233
  <BankAccountsEditor
199
234
  accounts={dialog.gateway.settings?.account_details || []}
200
235
  onChange={(account_details) =>
@@ -225,7 +260,11 @@ function BankAccountsEditor({ accounts, onChange }) {
225
260
  return (
226
261
  <div className="space-y-3">
227
262
  <Label>Bank account details</Label>
228
- {accounts.length === 0 && <p className="text-xs text-muted-foreground">No accounts yet.</p>}
263
+ {accounts.length === 0 && (
264
+ <p className="text-xs text-muted-foreground">
265
+ None yet — fine for cash on delivery; add one for bank transfer.
266
+ </p>
267
+ )}
229
268
  {accounts.map((a, i) => (
230
269
  <div key={i} className="space-y-2 rounded-md border p-3">
231
270
  <div className="flex items-center justify-between">
@@ -1,6 +1,5 @@
1
1
  import React from "react";
2
2
  import { Outlet, useLocation, useNavigate } from "react-router-dom";
3
- import { useSettings } from "../../context/SettingsContext";
4
3
  import { useAdminHref } from "../../context/BasePathContext";
5
4
  import PageHeader from "../../components/PageHeader";
6
5
 
@@ -12,11 +11,11 @@ const TABS = [
12
11
  { seg: "shipping", label: "Shipping" },
13
12
  { seg: "payments", label: "Payments" },
14
13
  { seg: "emails", label: "Emails" },
14
+ { seg: "webhooks", label: "Webhooks" },
15
15
  ];
16
16
 
17
17
  /** Layout route for /settings/*: horizontal group tabs + nested page via <Outlet/>. */
18
18
  export default function SettingsLayout() {
19
- const { get } = useSettings();
20
19
  const href = useAdminHref();
21
20
  const navigate = useNavigate();
22
21
  const location = useLocation();
@@ -25,14 +24,11 @@ export default function SettingsLayout() {
25
24
  const afterSettings = location.pathname.split("/settings/")[1] || "general";
26
25
  const current = afterSettings.split("/")[0] || "general";
27
26
 
28
- const enableTaxes = get("general", "enable_taxes", true);
29
- const tabs = TABS.filter((t) => t.seg !== "tax" || enableTaxes);
30
-
31
27
  return (
32
28
  <div className="space-y-4">
33
29
  <PageHeader title="Settings" description="Configure how your store works." />
34
30
  <div className="flex flex-wrap gap-1 border-b">
35
- {tabs.map((t) => (
31
+ {TABS.map((t) => (
36
32
  <button
37
33
  key={t.seg}
38
34
  type="button"
@@ -75,7 +75,6 @@ function TaxOptions({ classes }) {
75
75
  <SelectContent>
76
76
  <SelectItem value="shipping">Customer shipping address</SelectItem>
77
77
  <SelectItem value="billing">Customer billing address</SelectItem>
78
- <SelectItem value="base">Shop base address</SelectItem>
79
78
  </SelectContent>
80
79
  </Select>
81
80
  </div>
@@ -86,7 +86,7 @@ export default function WebhookEditor() {
86
86
  if (isNew) {
87
87
  const created = await base44.entities["commerce.Webhook"].create(payload);
88
88
  toast.success("Webhook created");
89
- navigate(href(`webhooks/${created.id}`));
89
+ navigate(href(`settings/webhooks/${created.id}`));
90
90
  } else {
91
91
  await base44.entities["commerce.Webhook"].update(id, payload);
92
92
  toast.success("Webhook saved");
@@ -103,7 +103,7 @@ export default function WebhookEditor() {
103
103
  try {
104
104
  await base44.entities["commerce.Webhook"].delete(id);
105
105
  toast.success("Webhook deleted");
106
- navigate(href("webhooks"));
106
+ navigate(href("settings/webhooks"));
107
107
  } catch (err) {
108
108
  toast.error(err.message || "Failed to delete");
109
109
  setDeleting(false);
@@ -135,7 +135,7 @@ export default function WebhookEditor() {
135
135
  <div className="max-w-3xl space-y-4">
136
136
  <PageHeader
137
137
  title={isNew ? "Add webhook" : "Edit webhook"}
138
- backHref={href("webhooks")}
138
+ backHref={href("settings/webhooks")}
139
139
  actions={
140
140
  !isNew && (
141
141
  <>
@@ -35,7 +35,7 @@ export default function Webhooks() {
35
35
  title="Webhooks"
36
36
  description="Notify external services when store events occur."
37
37
  actions={
38
- <Button onClick={() => navigate(href("webhooks/new"))}>
38
+ <Button onClick={() => navigate(href("settings/webhooks/new"))}>
39
39
  <Plus className="mr-2 h-4 w-4" />
40
40
  Add webhook
41
41
  </Button>
@@ -45,7 +45,7 @@ export default function Webhooks() {
45
45
  columns={columns}
46
46
  rows={hooks.data || []}
47
47
  loading={hooks.loading}
48
- onRowClick={(w) => navigate(href(`webhooks/${w.id}`))}
48
+ onRowClick={(w) => navigate(href(`settings/webhooks/${w.id}`))}
49
49
  empty={{ icon: WebhookIcon, title: "No webhooks", description: "Create a webhook to POST event payloads to an external URL." }}
50
50
  />
51
51
  </div>
@@ -11,9 +11,6 @@ import OrderEditor from "./pages/orders/OrderEditor";
11
11
  import ProductsList from "./pages/products/ProductsList";
12
12
  import ProductEditor from "./pages/products/ProductEditor";
13
13
  import Categories from "./pages/products/Categories";
14
- import Tags from "./pages/products/Tags";
15
- import Attributes from "./pages/products/Attributes";
16
- import AttributeTerms from "./pages/products/AttributeTerms";
17
14
  import Reviews from "./pages/products/Reviews";
18
15
  import CouponsList from "./pages/coupons/CouponsList";
19
16
  import CouponEditor from "./pages/coupons/CouponEditor";
@@ -44,9 +41,6 @@ export const ADMIN_ROUTES = [
44
41
  { path: "products", label: "Products" },
45
42
  { path: "products/new", label: "Add product" },
46
43
  { path: "products/categories", label: "Categories" },
47
- { path: "products/tags", label: "Tags" },
48
- { path: "products/attributes", label: "Attributes" },
49
- { path: "products/attributes/:id/terms", label: "Terms" },
50
44
  { path: "products/reviews", label: "Reviews" },
51
45
  { path: "products/:id", label: "Edit product" },
52
46
  { path: "coupons", label: "Coupons" },
@@ -65,9 +59,9 @@ export const ADMIN_ROUTES = [
65
59
  { path: "settings/shipping/zones/:zoneId", label: "Shipping zone" },
66
60
  { path: "settings/payments", label: "Payments" },
67
61
  { path: "settings/emails", label: "Emails" },
68
- { path: "webhooks", label: "Webhooks" },
69
- { path: "webhooks/new", label: "Add webhook" },
70
- { path: "webhooks/:id", label: "Webhook" },
62
+ { path: "settings/webhooks", label: "Webhooks" },
63
+ { path: "settings/webhooks/new", label: "Add webhook" },
64
+ { path: "settings/webhooks/:id", label: "Webhook" },
71
65
  ];
72
66
 
73
67
  function NotFound() {
@@ -89,8 +83,9 @@ function NotFound() {
89
83
 
90
84
  /**
91
85
  * Catch-all. A URL that still contains the *mount pattern* rather than a real
92
- * path — `/admin/*` (the `<Route path="/admin/*">` pattern pasted into the
93
- * address bar or copied into a nav link), or a leftover `/admin/:id` — is a
86
+ * path — `/store-admin/*` (the `<Route path="/store-admin/*">` pattern pasted
87
+ * into the address bar or copied into a nav link), or a leftover
88
+ * `/store-admin/:id` — is a
94
89
  * wiring artifact, not a page the admin asked for: land on the dashboard
95
90
  * instead of showing a dead end. Anything else is a genuine 404.
96
91
  */
@@ -100,7 +95,7 @@ function UnmatchedRoute() {
100
95
  return isMountPatternPath(params["*"]) ? <Navigate to={href()} replace /> : <NotFound />;
101
96
  }
102
97
 
103
- /** All admin routes. Rendered inside AdminLayout; mount AdminApp at `/admin/*`. */
98
+ /** All admin routes. Rendered inside AdminLayout; mount AdminApp at `/store-admin/*`. */
104
99
  export default function AdminRoutes() {
105
100
  return (
106
101
  <Routes>
@@ -113,9 +108,6 @@ export default function AdminRoutes() {
113
108
  <Route path="products" element={<ProductsList />} />
114
109
  <Route path="products/new" element={<ProductEditor />} />
115
110
  <Route path="products/categories" element={<Categories />} />
116
- <Route path="products/tags" element={<Tags />} />
117
- <Route path="products/attributes" element={<Attributes />} />
118
- <Route path="products/attributes/:id/terms" element={<AttributeTerms />} />
119
111
  <Route path="products/reviews" element={<Reviews />} />
120
112
  <Route path="products/:id" element={<ProductEditor />} />
121
113
 
@@ -139,12 +131,11 @@ export default function AdminRoutes() {
139
131
  <Route path="shipping/zones/:zoneId" element={<ShippingZoneEditor />} />
140
132
  <Route path="payments" element={<PaymentsSettings />} />
141
133
  <Route path="emails" element={<EmailsSettings />} />
134
+ <Route path="webhooks" element={<Webhooks />} />
135
+ <Route path="webhooks/new" element={<WebhookEditor />} />
136
+ <Route path="webhooks/:id" element={<WebhookEditor />} />
142
137
  </Route>
143
138
 
144
- <Route path="webhooks" element={<Webhooks />} />
145
- <Route path="webhooks/new" element={<WebhookEditor />} />
146
- <Route path="webhooks/:id" element={<WebhookEditor />} />
147
-
148
139
  <Route path="*" element={<UnmatchedRoute />} />
149
140
  </Routes>
150
141
  );
@@ -9,8 +9,8 @@
9
9
  * import { resolveSelection, defaultSelection, selectOption } from "@/commerce/utils";
10
10
  *
11
11
  * - `variants.js` — variant selection: map attribute selections (Size, Color)
12
- * to a `ProductVariation` and back, per-option availability, variable-product
13
- * price ranges. See `skills/commerce/references/storefront-product-page.md`.
12
+ * to a `ProductVariation` and back, per-option availability, variant price
13
+ * ranges. See `skills/commerce/references/storefront-product-page.md`.
14
14
  * - `shipping-promos.js` — read the store's real free-shipping configuration so
15
15
  * "Free shipping over €150" copy states a configured rule, not an invented
16
16
  * number. See `skills/commerce/docs/api-storefront.md`.