@base44/app-plugin-commerce 0.1.19 → 0.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@base44/app-plugin-commerce",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.20",
|
|
4
4
|
"description": "Base44 Commerce plugin — entities, backend functions, shared commerce engine, admin UI and the commerce skill, shipped as copyable source",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"base44",
|
|
@@ -220,7 +220,7 @@ Shipping cost on the cart page: a store with exactly **one shipping location** s
|
|
|
220
220
|
|
|
221
221
|
- **address form state** (`billing`/`updateBilling`, an optional separate `shipping`/`updateShipping` behind `setShipToDifferent`), with `missingBillingFields` tracking what `place-order` would reject;
|
|
222
222
|
- **automatic shipping/tax recalculation**: the moment the address is complete enough to price (default: country + city), the hook debounces and calls `set-shipping-address`, repricing every shipping option, its cost and the taxes — half-typed addresses are never sent, an unchanged address is never re-sent, and an address the store doesn't ship to surfaces as `addressError` to show **on the address fields**;
|
|
223
|
-
- **the shipping choice** (`shippingStatus`, `shippingMethods`, `chosenShippingMethod`, `chooseShippingMethod`) and **the payment choice** (`paymentMethods` from store info — their only source; a
|
|
223
|
+
- **the shipping choice** (`shippingStatus`, `shippingMethods`, `chosenShippingMethod`, `chooseShippingMethod`) and **the payment choice** (`paymentMethods` from store info — their only source); on both, **a single option is a selected option** — one shipping rate arrives already applied as `chosenShippingMethod`, one enabled gateway is `selectedGateway` from the first render that has store info, and both re-resolve when the data changes (`singleShippingMethod` / `singlePaymentMethod` flag the case);
|
|
224
224
|
- **the gate**: `canPlaceOrder` + named `blockers`, and `placeOrder` with the online-payment redirect handled.
|
|
225
225
|
|
|
226
226
|
Share one instance across the page's components with `CheckoutProvider` and build each step as your own markup:
|
|
@@ -264,16 +264,17 @@ const { billing, updateBilling, missingBillingFields,
|
|
|
264
264
|
The two store-data choices — **never hardcode either** — come pre-branched through the headless pickers (they render nothing themselves; the render prop is the whole UI):
|
|
265
265
|
|
|
266
266
|
```jsx
|
|
267
|
-
<ShippingMethodPicker>{({ status, methods, chosen, choose, mustChoose, syncing }) => (
|
|
267
|
+
<ShippingMethodPicker>{({ status, methods, chosen, choose, mustChoose, single, syncing }) => (
|
|
268
268
|
// renders null for you on virtual carts (status "not_needed")
|
|
269
269
|
// status "missing_address" → say options appear once the address is entered
|
|
270
270
|
// mustChoose → render methods [{ id, title, cost }] as a picker → choose(m.id)
|
|
271
|
+
// single → already selected; skip the picker, still show chosen.title + cost
|
|
271
272
|
// otherwise → display chosen.title + chosen.cost (never a raw id); dim while syncing
|
|
272
273
|
)}</ShippingMethodPicker>
|
|
273
274
|
|
|
274
275
|
<PaymentMethodPicker>{({ gateways, value, select, selected, single }) => (
|
|
275
276
|
// several → picker labeled with the admin's title/description → select(g.slug)
|
|
276
|
-
// single →
|
|
277
|
+
// single → already selected; skip the picker but still show selected.title
|
|
277
278
|
// none → checkout cannot complete — say so instead of rendering a dead button
|
|
278
279
|
)}</PaymentMethodPicker>
|
|
279
280
|
```
|
|
@@ -27,15 +27,19 @@ function resolveCheckout(name, prop, ctx) {
|
|
|
27
27
|
* status, // "missing_address" | "choice_required" | "chosen" | "auto_selected"
|
|
28
28
|
* methods, // [{ id, title, cost }] — what this address is offered
|
|
29
29
|
* chosen, // the chosen/auto-selected entry (title + cost), or null
|
|
30
|
+
* selected, // alias of `chosen` — the same name the payment picker uses
|
|
30
31
|
* choose, // (id) => Promise — call with a method's id on pick
|
|
31
32
|
* mustChoose, // status === "choice_required" → render methods as a picker
|
|
33
|
+
* single, // exactly one method offered — already chosen; skip the
|
|
34
|
+
* // picker but still show `chosen.title` and its cost
|
|
32
35
|
* syncing, // an address edit is being repriced — show a subtle busy state
|
|
33
36
|
* addressError, // { code, message } | null — "we don't ship there" belongs
|
|
34
37
|
* } // on the address fields
|
|
35
38
|
*
|
|
36
39
|
* Render rules the child should follow: `missing_address` → say the options
|
|
37
40
|
* appear once the address is entered; `mustChoose` → a picker of `methods`;
|
|
38
|
-
* otherwise display `chosen.title` + its cost (never the raw id).
|
|
41
|
+
* otherwise display `chosen.title` + its cost (never the raw id). `single` and
|
|
42
|
+
* `mustChoose` are never both true, and `single` guarantees `chosen`.
|
|
39
43
|
*/
|
|
40
44
|
export function ShippingMethodPicker({ checkout: checkoutProp, children }) {
|
|
41
45
|
const checkout = resolveCheckout("ShippingMethodPicker", checkoutProp, useCheckoutContextOptional());
|
|
@@ -43,6 +47,7 @@ export function ShippingMethodPicker({ checkout: checkoutProp, children }) {
|
|
|
43
47
|
shippingStatus: status,
|
|
44
48
|
shippingMethods: methods,
|
|
45
49
|
chosenShippingMethod: chosen,
|
|
50
|
+
singleShippingMethod: single,
|
|
46
51
|
chooseShippingMethod: choose,
|
|
47
52
|
shippingSyncing: syncing,
|
|
48
53
|
addressError,
|
|
@@ -52,8 +57,10 @@ export function ShippingMethodPicker({ checkout: checkoutProp, children }) {
|
|
|
52
57
|
status,
|
|
53
58
|
methods,
|
|
54
59
|
chosen,
|
|
60
|
+
selected: chosen,
|
|
55
61
|
choose,
|
|
56
62
|
mustChoose: status === "choice_required",
|
|
63
|
+
single: single ?? (methods.length === 1), // fallback: a hand-built checkout object
|
|
57
64
|
syncing,
|
|
58
65
|
addressError,
|
|
59
66
|
});
|
|
@@ -69,22 +76,30 @@ export function ShippingMethodPicker({ checkout: checkoutProp, children }) {
|
|
|
69
76
|
* value, // the selected slug ("" while none)
|
|
70
77
|
* select, // (slug) => void
|
|
71
78
|
* selected, // the selected gateway entry, or null
|
|
72
|
-
* single, // exactly one gateway —
|
|
79
|
+
* single, // exactly one gateway — already selected; skip the picker but
|
|
73
80
|
* } // still show its title so the customer knows how they pay
|
|
74
81
|
*
|
|
75
82
|
* Render rules: several gateways → a picker labeled with the admin's
|
|
76
83
|
* title/description; `single` → just display it; zero gateways → say checkout
|
|
77
|
-
* is unavailable instead of rendering a dead place-order button.
|
|
84
|
+
* is unavailable instead of rendering a dead place-order button. `single`
|
|
85
|
+
* guarantees `value` and `selected` — never render the one-gateway branch as
|
|
86
|
+
* "nothing selected yet" — and both re-resolve when store info changes.
|
|
78
87
|
*/
|
|
79
88
|
export function PaymentMethodPicker({ checkout: checkoutProp, children }) {
|
|
80
89
|
const checkout = resolveCheckout("PaymentMethodPicker", checkoutProp, useCheckoutContextOptional());
|
|
81
|
-
const {
|
|
90
|
+
const {
|
|
91
|
+
paymentMethods: gateways,
|
|
92
|
+
paymentMethod: value,
|
|
93
|
+
setPaymentMethod: select,
|
|
94
|
+
selectedGateway: selected,
|
|
95
|
+
singlePaymentMethod: single,
|
|
96
|
+
} = checkout;
|
|
82
97
|
if (!gateways) return null;
|
|
83
98
|
return children({
|
|
84
99
|
gateways,
|
|
85
100
|
value,
|
|
86
101
|
select,
|
|
87
102
|
selected,
|
|
88
|
-
single: gateways.length === 1,
|
|
103
|
+
single: single ?? (gateways.length === 1), // fallback: a hand-built checkout object
|
|
89
104
|
});
|
|
90
105
|
}
|
|
@@ -21,6 +21,18 @@ const EMPTY_ADDRESS = Object.freeze({
|
|
|
21
21
|
phone: "",
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* The gateway a checkout is actually paying with: the customer's pick while it
|
|
26
|
+
* is still an enabled gateway, otherwise the only gateway there is — one option
|
|
27
|
+
* is not a choice. Derived on every render from the current store info, so it
|
|
28
|
+
* follows the data instead of trailing it by an effect.
|
|
29
|
+
*/
|
|
30
|
+
function resolvePaymentMethod(gateways, picked) {
|
|
31
|
+
if (!gateways?.length) return "";
|
|
32
|
+
if (picked && gateways.some((g) => g.slug === picked)) return picked;
|
|
33
|
+
return gateways.length === 1 ? gateways[0].slug : "";
|
|
34
|
+
}
|
|
35
|
+
|
|
24
36
|
/**
|
|
25
37
|
* useCheckout — the guided checkout state machine. It owns the parts every
|
|
26
38
|
* checkout must get right, so the page you build is only markup around it:
|
|
@@ -38,8 +50,15 @@ const EMPTY_ADDRESS = Object.freeze({
|
|
|
38
50
|
* `chosenShippingMethod`), `chosen`, `choice_required` (render
|
|
39
51
|
* `shippingMethods` and call `chooseShippingMethod(id)`), `missing_address`
|
|
40
52
|
* (collect the address), `not_needed` (virtual cart — render nothing).
|
|
53
|
+
* `singleShippingMethod` flags the one-option case, and `chosenShippingMethod`
|
|
54
|
+
* is filled for it — a single option is never left unselected.
|
|
41
55
|
* - **Payment choice.** `paymentMethods` come from store info (their ONLY
|
|
42
|
-
* source); a store with exactly one enabled gateway gets it
|
|
56
|
+
* source); a store with exactly one enabled gateway gets it selected
|
|
57
|
+
* (`singlePaymentMethod`, with `selectedGateway` filled) from the first
|
|
58
|
+
* render that has store info. The selection is derived from the current
|
|
59
|
+
* gateway list, not remembered: when store info changes, a gateway that is
|
|
60
|
+
* no longer enabled is dropped and a list that is down to one gateway
|
|
61
|
+
* selects it — the customer's own pick survives as long as it stays enabled.
|
|
43
62
|
* - **The gate.** `canPlaceOrder` + `blockers` say exactly what still stands
|
|
44
63
|
* between the customer and the order — drive the button's disabled state
|
|
45
64
|
* and the "what's missing" hints from them instead of re-deriving.
|
|
@@ -70,7 +89,7 @@ export function useCheckout(options = {}) {
|
|
|
70
89
|
const [billing, setBilling] = useState({ ...EMPTY_ADDRESS, email: "" });
|
|
71
90
|
const [shipping, setShipping] = useState({ ...EMPTY_ADDRESS });
|
|
72
91
|
const [shipToDifferent, setShipToDifferent] = useState(false);
|
|
73
|
-
const [
|
|
92
|
+
const [pickedPaymentMethod, setPaymentMethod] = useState("");
|
|
74
93
|
const [addressError, setAddressError] = useState(null);
|
|
75
94
|
const [syncing, setSyncing] = useState(false);
|
|
76
95
|
const [syncedKey, setSyncedKey] = useState(undefined);
|
|
@@ -124,24 +143,30 @@ export function useCheckout(options = {}) {
|
|
|
124
143
|
// ── shipping choice (from the shared cart view) ──────────────────────────
|
|
125
144
|
const shippingStatus = cart?.shipping_status ?? null;
|
|
126
145
|
const shippingMethods = cart?.available_shipping_methods ?? [];
|
|
146
|
+
const singleShippingMethod = shippingMethods.length === 1;
|
|
147
|
+
// The backend auto-selects when it offers exactly one rate (`auto_selected`)
|
|
148
|
+
// and echoes it back on the cart, so the lookup normally finds it. The
|
|
149
|
+
// fallback covers the seam where a freshly repriced address offers one rate
|
|
150
|
+
// the cart's stored id hasn't caught up with: a single option is selected,
|
|
151
|
+
// and must read as selected, from the moment it is offered.
|
|
127
152
|
const chosenShippingMethod =
|
|
128
|
-
shippingMethods.find((m) => m.id === cart?.chosen_shipping_method) ??
|
|
153
|
+
shippingMethods.find((m) => m.id === cart?.chosen_shipping_method) ??
|
|
154
|
+
(singleShippingMethod ? shippingMethods[0] : null);
|
|
129
155
|
const chooseShippingMethod = useCallback(
|
|
130
156
|
(methodId) => runCart(() => client.chooseShippingMethod(methodId)),
|
|
131
157
|
[client, runCart],
|
|
132
158
|
);
|
|
133
159
|
|
|
134
160
|
// ── payment choice (gateways live on store info ONLY) ────────────────────
|
|
161
|
+
// The pick is state; the *method* is derived, so a lone gateway is selected
|
|
162
|
+
// on the very first render that has store info (never a frame of "nothing
|
|
163
|
+
// selected", never a wasted effect pass) and every one of these re-resolves
|
|
164
|
+
// the moment the gateway list changes — a gateway the admin just disabled
|
|
165
|
+
// drops out, and if that leaves exactly one, it takes over immediately.
|
|
135
166
|
const paymentMethods = info?.payment_gateways ?? null;
|
|
136
|
-
|
|
137
|
-
if (!paymentMethods) return;
|
|
138
|
-
if (paymentMethod && !paymentMethods.some((g) => g.slug === paymentMethod)) {
|
|
139
|
-
setPaymentMethod("");
|
|
140
|
-
} else if (!paymentMethod && paymentMethods.length === 1) {
|
|
141
|
-
setPaymentMethod(paymentMethods[0].slug); // one option is not a choice
|
|
142
|
-
}
|
|
143
|
-
}, [paymentMethods, paymentMethod]);
|
|
167
|
+
const paymentMethod = resolvePaymentMethod(paymentMethods, pickedPaymentMethod);
|
|
144
168
|
const selectedGateway = paymentMethods?.find((g) => g.slug === paymentMethod) ?? null;
|
|
169
|
+
const singlePaymentMethod = paymentMethods?.length === 1;
|
|
145
170
|
|
|
146
171
|
// ── the gate ─────────────────────────────────────────────────────────────
|
|
147
172
|
const missingBilling = missingBillingFields(billing, requiredBillingFields);
|
|
@@ -220,12 +245,14 @@ export function useCheckout(options = {}) {
|
|
|
220
245
|
shippingStatus,
|
|
221
246
|
shippingMethods,
|
|
222
247
|
chosenShippingMethod,
|
|
248
|
+
singleShippingMethod,
|
|
223
249
|
chooseShippingMethod,
|
|
224
250
|
// payment choice
|
|
225
251
|
paymentMethods,
|
|
226
252
|
paymentMethod,
|
|
227
253
|
setPaymentMethod,
|
|
228
254
|
selectedGateway,
|
|
255
|
+
singlePaymentMethod,
|
|
229
256
|
// the gate + the order
|
|
230
257
|
blockers,
|
|
231
258
|
canPlaceOrder,
|