@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,232 @@
1
+ import React from "react";
2
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
3
+ import { Input } from "@/components/ui/input";
4
+ import { Label } from "@/components/ui/label";
5
+ import { Checkbox } from "@/components/ui/checkbox";
6
+ import {
7
+ Select,
8
+ SelectContent,
9
+ SelectItem,
10
+ SelectTrigger,
11
+ SelectValue,
12
+ } from "@/components/ui/select";
13
+ import CountrySelect from "../../components/CountrySelect";
14
+ import { statesFor } from "../../lib/geo-data";
15
+ import { CURRENCIES, CURRENCY_POSITIONS } from "../../lib/constants";
16
+ import useGroupForm from "./useGroupForm";
17
+
18
+ const DEFAULTS = {
19
+ store_name: "",
20
+ store_url: "",
21
+ 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
+ currency: "USD",
27
+ currency_position: "left",
28
+ thousand_sep: ",",
29
+ decimal_sep: ".",
30
+ num_decimals: 2,
31
+ };
32
+
33
+ export default function GeneralSettings() {
34
+ 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
+
40
+ return (
41
+ <div className="max-w-3xl space-y-4">
42
+ <Card>
43
+ <CardHeader className="flex-row items-start justify-between gap-4 space-y-0">
44
+ <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>
47
+ </div>
48
+ <SaveButton />
49
+ </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">
70
+ <Label>Payment return path</Label>
71
+ <Input
72
+ placeholder="/order-received"
73
+ value={form.order_received_path || ""}
74
+ onChange={(e) => setField("order_received_path", e.target.value)}
75
+ />
76
+ <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.
79
+ </p>
80
+ </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
+ </CardContent>
161
+ </Card>
162
+
163
+ <Card>
164
+ <CardHeader className="flex-row items-start justify-between gap-4 space-y-0">
165
+ <div className="space-y-1.5">
166
+ <CardTitle>Currency options</CardTitle>
167
+ <CardDescription>Affects how prices are displayed in the admin and by storefront APIs.</CardDescription>
168
+ </div>
169
+ <SaveButton />
170
+ </CardHeader>
171
+ <CardContent className="grid gap-4 sm:grid-cols-2">
172
+ <div className="space-y-1.5">
173
+ <Label>Currency</Label>
174
+ <Select value={form.currency} onValueChange={(v) => setField("currency", v)}>
175
+ <SelectTrigger>
176
+ <SelectValue />
177
+ </SelectTrigger>
178
+ <SelectContent>
179
+ {CURRENCIES.map((c) => (
180
+ <SelectItem key={c.code} value={c.code}>
181
+ {c.name} ({c.symbol})
182
+ </SelectItem>
183
+ ))}
184
+ </SelectContent>
185
+ </Select>
186
+ </div>
187
+ <div className="space-y-1.5">
188
+ <Label>Currency position</Label>
189
+ <Select value={form.currency_position} onValueChange={(v) => setField("currency_position", v)}>
190
+ <SelectTrigger>
191
+ <SelectValue />
192
+ </SelectTrigger>
193
+ <SelectContent>
194
+ {CURRENCY_POSITIONS.map((p) => (
195
+ <SelectItem key={p.value} value={p.value}>
196
+ {p.label}
197
+ </SelectItem>
198
+ ))}
199
+ </SelectContent>
200
+ </Select>
201
+ </div>
202
+ <div className="space-y-1.5">
203
+ <Label>Thousand separator</Label>
204
+ <Input
205
+ value={form.thousand_sep ?? ","}
206
+ maxLength={1}
207
+ onChange={(e) => setField("thousand_sep", e.target.value)}
208
+ />
209
+ </div>
210
+ <div className="space-y-1.5">
211
+ <Label>Decimal separator</Label>
212
+ <Input
213
+ value={form.decimal_sep ?? "."}
214
+ maxLength={1}
215
+ onChange={(e) => setField("decimal_sep", e.target.value)}
216
+ />
217
+ </div>
218
+ <div className="space-y-1.5">
219
+ <Label>Number of decimals</Label>
220
+ <Input
221
+ type="number"
222
+ min={0}
223
+ max={4}
224
+ value={form.num_decimals ?? 2}
225
+ onChange={(e) => setField("num_decimals", e.target.value === "" ? 2 : Number(e.target.value))}
226
+ />
227
+ </div>
228
+ </CardContent>
229
+ </Card>
230
+ </div>
231
+ );
232
+ }
@@ -0,0 +1,146 @@
1
+ import React from "react";
2
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
3
+ import { Input } from "@/components/ui/input";
4
+ import { Label } from "@/components/ui/label";
5
+ import { Switch } from "@/components/ui/switch";
6
+ import useGroupForm from "./useGroupForm";
7
+
8
+ const DEFAULTS = {
9
+ manage_stock: true,
10
+ hold_stock_minutes: 60,
11
+ low_stock_threshold: 2,
12
+ out_of_stock_threshold: 0,
13
+ notify_low_stock: true,
14
+ notify_out_of_stock: true,
15
+ notification_recipient: "",
16
+ hide_out_of_stock: false,
17
+ };
18
+
19
+ export default function InventorySettings() {
20
+ const { form, setField, SaveButton } = useGroupForm("inventory", DEFAULTS);
21
+ const off = !form.manage_stock;
22
+
23
+ const num = (key, fallback = 0) => (e) =>
24
+ setField(key, e.target.value === "" ? fallback : Number(e.target.value));
25
+
26
+ return (
27
+ <div className="max-w-3xl space-y-4">
28
+ <Card>
29
+ <CardHeader className="flex-row items-start justify-between gap-4 space-y-0">
30
+ <div className="space-y-1.5">
31
+ <CardTitle>Inventory</CardTitle>
32
+ <CardDescription>Stock management defaults for products that track inventory.</CardDescription>
33
+ </div>
34
+ <SaveButton />
35
+ </CardHeader>
36
+ <CardContent className="space-y-4">
37
+ <div className="flex items-start justify-between gap-4">
38
+ <div>
39
+ <Label>Manage stock</Label>
40
+ <p className="text-xs text-muted-foreground">Enable stock management store-wide.</p>
41
+ </div>
42
+ <Switch checked={!!form.manage_stock} onCheckedChange={(v) => setField("manage_stock", v)} />
43
+ </div>
44
+
45
+ <div className="space-y-1.5">
46
+ <Label className={off ? "opacity-50" : ""}>Hold stock (minutes)</Label>
47
+ <Input
48
+ type="number"
49
+ min={0}
50
+ disabled={off}
51
+ value={form.hold_stock_minutes ?? 60}
52
+ onChange={num("hold_stock_minutes", 60)}
53
+ />
54
+ <p className="text-xs text-muted-foreground">
55
+ Hold stock for unpaid orders for this many minutes. When the limit is reached, the
56
+ pending order is cancelled and its stock released. Release runs opportunistically —
57
+ see skills/commerce/references/scheduled-work.md.
58
+ </p>
59
+ </div>
60
+
61
+ <div className="grid gap-4 sm:grid-cols-2">
62
+ <div className="space-y-1.5">
63
+ <Label className={off ? "opacity-50" : ""}>Low stock threshold</Label>
64
+ <Input
65
+ type="number"
66
+ min={0}
67
+ disabled={off}
68
+ value={form.low_stock_threshold ?? 2}
69
+ onChange={num("low_stock_threshold", 2)}
70
+ />
71
+ </div>
72
+ <div className="space-y-1.5">
73
+ <Label className={off ? "opacity-50" : ""}>Out of stock threshold</Label>
74
+ <Input
75
+ type="number"
76
+ disabled={off}
77
+ value={form.out_of_stock_threshold ?? 0}
78
+ onChange={num("out_of_stock_threshold", 0)}
79
+ />
80
+ </div>
81
+ </div>
82
+ </CardContent>
83
+ </Card>
84
+
85
+ <Card>
86
+ <CardHeader className="flex-row items-start justify-between gap-4 space-y-0">
87
+ <div className="space-y-1.5">
88
+ <CardTitle>Notifications</CardTitle>
89
+ </div>
90
+ <SaveButton />
91
+ </CardHeader>
92
+ <CardContent className="space-y-4">
93
+ <div className="flex items-start justify-between gap-4">
94
+ <Label className={off ? "opacity-50" : ""}>Enable low stock notifications</Label>
95
+ <Switch
96
+ disabled={off}
97
+ checked={!!form.notify_low_stock}
98
+ onCheckedChange={(v) => setField("notify_low_stock", v)}
99
+ />
100
+ </div>
101
+ <div className="flex items-start justify-between gap-4">
102
+ <Label className={off ? "opacity-50" : ""}>Enable out of stock notifications</Label>
103
+ <Switch
104
+ disabled={off}
105
+ checked={!!form.notify_out_of_stock}
106
+ onCheckedChange={(v) => setField("notify_out_of_stock", v)}
107
+ />
108
+ </div>
109
+ <div className="space-y-1.5">
110
+ <Label className={off ? "opacity-50" : ""}>Notification recipient</Label>
111
+ <Input
112
+ type="email"
113
+ placeholder="Defaults to the admin recipients in Emails settings"
114
+ disabled={off}
115
+ value={form.notification_recipient || ""}
116
+ onChange={(e) => setField("notification_recipient", e.target.value)}
117
+ />
118
+ </div>
119
+ </CardContent>
120
+ </Card>
121
+
122
+ <Card>
123
+ <CardHeader className="flex-row items-start justify-between gap-4 space-y-0">
124
+ <div className="space-y-1.5">
125
+ <CardTitle>Visibility</CardTitle>
126
+ </div>
127
+ <SaveButton />
128
+ </CardHeader>
129
+ <CardContent>
130
+ <div className="flex items-start justify-between gap-4">
131
+ <div>
132
+ <Label>Hide out of stock items from the catalog</Label>
133
+ <p className="text-xs text-muted-foreground">
134
+ Storefront APIs will exclude out-of-stock products from listings.
135
+ </p>
136
+ </div>
137
+ <Switch
138
+ checked={!!form.hide_out_of_stock}
139
+ onCheckedChange={(v) => setField("hide_out_of_stock", v)}
140
+ />
141
+ </div>
142
+ </CardContent>
143
+ </Card>
144
+ </div>
145
+ );
146
+ }
@@ -0,0 +1,260 @@
1
+ import React, { useState } from "react";
2
+ import { Card, CardContent } from "@/components/ui/card";
3
+ import { Button } from "@/components/ui/button";
4
+ import { Input } from "@/components/ui/input";
5
+ import { Label } from "@/components/ui/label";
6
+ import { Textarea } from "@/components/ui/textarea";
7
+ import { Switch } from "@/components/ui/switch";
8
+ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
9
+ import {
10
+ Dialog,
11
+ DialogContent,
12
+ DialogFooter,
13
+ DialogHeader,
14
+ DialogTitle,
15
+ } from "@/components/ui/dialog";
16
+ import { ArrowDown, ArrowUp, Info, Loader2, Plus, Settings2, Trash2 } from "lucide-react";
17
+ import { toast } from "sonner";
18
+ import { base44 } from "../../lib/api";
19
+ import useAsync from "../../hooks/useAsync";
20
+ import usePaymentProvider from "../../hooks/usePaymentProvider";
21
+
22
+ const emptyBank = () => ({ account_name: "", account_number: "", bank_name: "", sort_code: "", iban: "", bic: "" });
23
+
24
+ export default function PaymentsSettings() {
25
+ const gateways = useAsync(() => base44.entities["commerce.PaymentGateway"].list("order", 100), []);
26
+ // Whether the store can take card payments is a live fact about the connected
27
+ // payment provider, not a constant — the notice below follows it.
28
+ const provider = usePaymentProvider();
29
+ const [dialog, setDialog] = useState(null); // { gateway (local copy) }
30
+ const [saving, setSaving] = useState(false);
31
+ const [busy, setBusy] = useState(false);
32
+
33
+ const rows = gateways.data || [];
34
+
35
+ const toggle = async (g, enabled) => {
36
+ try {
37
+ await base44.entities["commerce.PaymentGateway"].update(g.id, { enabled });
38
+ gateways.refetch();
39
+ } catch (err) {
40
+ toast.error(err.message || "Failed to update gateway");
41
+ }
42
+ };
43
+
44
+ const move = async (index, dir) => {
45
+ const target = index + dir;
46
+ if (target < 0 || target >= rows.length) return;
47
+ const a = rows[index];
48
+ const b = rows[target];
49
+ setBusy(true);
50
+ try {
51
+ await Promise.all([
52
+ base44.entities["commerce.PaymentGateway"].update(a.id, { order: b.order ?? target }),
53
+ base44.entities["commerce.PaymentGateway"].update(b.id, { order: a.order ?? index }),
54
+ ]);
55
+ gateways.refetch();
56
+ } catch (err) {
57
+ toast.error(err.message || "Failed to reorder");
58
+ } finally {
59
+ setBusy(false);
60
+ }
61
+ };
62
+
63
+ const saveGateway = async () => {
64
+ setSaving(true);
65
+ try {
66
+ const g = dialog.gateway;
67
+ await base44.entities["commerce.PaymentGateway"].update(g.id, {
68
+ title: g.title,
69
+ description: g.description,
70
+ settings: g.settings || {},
71
+ });
72
+ toast.success("Payment method saved");
73
+ setDialog(null);
74
+ gateways.refetch();
75
+ } catch (err) {
76
+ toast.error(err.message || "Failed to save");
77
+ } finally {
78
+ setSaving(false);
79
+ }
80
+ };
81
+
82
+ if (gateways.loading) {
83
+ return (
84
+ <div className="flex justify-center py-12">
85
+ <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
86
+ </div>
87
+ );
88
+ }
89
+
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>
108
+ {isOnline && !provider.loading && (
109
+ <Alert className="mt-3">
110
+ <Info className="h-4 w-4" />
111
+ {provider.connected ? (
112
+ <>
113
+ <AlertTitle>Payment provider connected</AlertTitle>
114
+ <AlertDescription>
115
+ Card payments are live: customers see this option at checkout, orders are
116
+ confirmed automatically once paid, and refunds can be sent back through the
117
+ provider{provider.providerLabel ? ` (${provider.providerLabel})` : ""}.
118
+ </AlertDescription>
119
+ </>
120
+ ) : (
121
+ <>
122
+ <AlertTitle>No payment provider connected</AlertTitle>
123
+ <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{" "}
129
+ <code className="text-xs">skills/commerce/references/online-payments.md</code>.
130
+ </AlertDescription>
131
+ </>
132
+ )}
133
+ </Alert>
134
+ )}
135
+ </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
+ })}
172
+
173
+ {dialog && (
174
+ <Dialog open onOpenChange={(o) => !o && setDialog(null)}>
175
+ <DialogContent className="max-h-[85vh] overflow-y-auto">
176
+ <DialogHeader>
177
+ <DialogTitle>{dialog.gateway.method_title || dialog.gateway.title}</DialogTitle>
178
+ </DialogHeader>
179
+ <div className="space-y-4">
180
+ <div className="space-y-1.5">
181
+ <Label>Title</Label>
182
+ <Input
183
+ value={dialog.gateway.title || ""}
184
+ onChange={(e) => setDialog({ ...dialog, gateway: { ...dialog.gateway, title: e.target.value } })}
185
+ />
186
+ <p className="text-xs text-muted-foreground">Shown to customers during checkout.</p>
187
+ </div>
188
+ <div className="space-y-1.5">
189
+ <Label>Description</Label>
190
+ <Textarea
191
+ rows={2}
192
+ value={dialog.gateway.description || ""}
193
+ onChange={(e) => setDialog({ ...dialog, gateway: { ...dialog.gateway, description: e.target.value } })}
194
+ />
195
+ </div>
196
+
197
+ {dialog.gateway.slug === "bacs" && (
198
+ <BankAccountsEditor
199
+ accounts={dialog.gateway.settings?.account_details || []}
200
+ onChange={(account_details) =>
201
+ setDialog({ ...dialog, gateway: { ...dialog.gateway, settings: { ...dialog.gateway.settings, account_details } } })
202
+ }
203
+ />
204
+ )}
205
+ </div>
206
+ <DialogFooter>
207
+ <Button variant="outline" onClick={() => setDialog(null)}>Cancel</Button>
208
+ <Button onClick={saveGateway} disabled={saving}>
209
+ {saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
210
+ Save
211
+ </Button>
212
+ </DialogFooter>
213
+ </DialogContent>
214
+ </Dialog>
215
+ )}
216
+ </div>
217
+ );
218
+ }
219
+
220
+ function BankAccountsEditor({ accounts, onChange }) {
221
+ const update = (i, key, val) => onChange(accounts.map((a, idx) => (idx === i ? { ...a, [key]: val } : a)));
222
+ const remove = (i) => onChange(accounts.filter((_, idx) => idx !== i));
223
+ const add = () => onChange([...accounts, emptyBank()]);
224
+
225
+ return (
226
+ <div className="space-y-3">
227
+ <Label>Bank account details</Label>
228
+ {accounts.length === 0 && <p className="text-xs text-muted-foreground">No accounts yet.</p>}
229
+ {accounts.map((a, i) => (
230
+ <div key={i} className="space-y-2 rounded-md border p-3">
231
+ <div className="flex items-center justify-between">
232
+ <span className="text-sm font-medium">Account {i + 1}</span>
233
+ <Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => remove(i)}>
234
+ <Trash2 className="h-3.5 w-3.5" />
235
+ </Button>
236
+ </div>
237
+ <div className="grid gap-2 sm:grid-cols-2">
238
+ {[
239
+ ["account_name", "Account name"],
240
+ ["account_number", "Account number"],
241
+ ["bank_name", "Bank name"],
242
+ ["sort_code", "Sort code"],
243
+ ["iban", "IBAN"],
244
+ ["bic", "BIC / Swift"],
245
+ ].map(([key, label]) => (
246
+ <div key={key} className="space-y-1">
247
+ <Label className="text-xs">{label}</Label>
248
+ <Input value={a[key] || ""} onChange={(e) => update(i, key, e.target.value)} />
249
+ </div>
250
+ ))}
251
+ </div>
252
+ </div>
253
+ ))}
254
+ <Button variant="outline" size="sm" onClick={add}>
255
+ <Plus className="mr-2 h-4 w-4" />
256
+ Add account
257
+ </Button>
258
+ </div>
259
+ );
260
+ }