@base44/app-plugin-commerce 0.3.3 → 0.3.4

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.3.3",
3
+ "version": "0.3.4",
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",
@@ -22,7 +22,7 @@ A fresh install has **no settings and no catalog**. One admin-only, idempotent c
22
22
 
23
23
  **`store_name` is required on a first seed** — the app's name as the platform shows it (`base44/config.jsonc` → `name` can be stale; ask if unsure). **`currency`** is an ISO code (`"EUR"`); formatting follows the viewer's locale, nothing else to set. Explicit values always win, first seed and re-runs alike.
24
24
 
25
- The working call — `name` is the only required product key; give each product the keys its own catalog entry actually has and leave the rest out. The **full key list** (sale windows, downloads, tax, backorders, dimensions…) lives in `api-admin.md` — open it only if the catalog needs one:
25
+ The working call — `name` is the only required product key; give each product the keys its own catalog entry actually has and leave the rest out. The **full key list** (sale windows, downloads, tax, backorders, upsells…) lives in `api-admin.md` — open it only if the catalog needs one:
26
26
 
27
27
  ```js
28
28
  try {
@@ -45,10 +45,14 @@ try {
45
45
  categories: ["Shoes"], // get-or-created by display name
46
46
  ribbons: ["Best Seller"], // flat labels, not a hierarchy
47
47
 
48
+ weight: 0.248, // real fields, in the store's units —
49
+ dimensions: { length: 31, width: 12, height: 11 }, // never a meta_data row
50
+
48
51
  // Descriptive spec rows (productSpecs). NOT axes, NOT ribbons; `_` hides.
52
+ // Qualities only — a weight or a size here is a string nothing can read.
49
53
  meta_data: [
50
54
  { key: "Material", value: "Recycled knit upper" },
51
- { key: "Weight", value: "248 g" },
55
+ { key: "Care", value: "Machine wash cold" },
52
56
  ],
53
57
 
54
58
  attributes: [ // the axes → one selector each
@@ -57,8 +61,9 @@ try {
57
61
  ],
58
62
  default_options: { Size: "42", Color: "Black" },
59
63
  variations: [ // omit entirely → all combos auto-generated
60
- { options: { Size: "41", Color: "Black" }, stock_quantity: 4 },
64
+ { options: { Size: "41", Color: "Black" }, stock_quantity: 4, weight: 0.242 },
61
65
  { options: { Size: "42", Color: "White" }, regular_price: 94, stock_quantity: 3,
66
+ weight: 0.254, // per variant, never inherited from the parent
62
67
  image: "https://…/sneaker-white.jpg" }, // per-variation image for a visual axis
63
68
  ],
64
69
  },
@@ -136,9 +141,4 @@ Online card payments are **off by default**: the seeded store takes the manual `
136
141
  - [ ] `coupons` seeded only if a coupon field exists ([`./02-storefront.md`](./02-storefront.md)).
137
142
  - [ ] Cards off, or on with the provider file copied whole.
138
143
 
139
- Record these lines in your working notes; do not re-read this file.
140
-
141
- - Product slugs come from the seed response's `catalog.products[]` — link pages by slug, never by a client-side map.
142
- - Payments: report at handover where they landed (default = offline on, card off) — the owner must never learn it from a customer.
143
- - Turning card payments on or off later is one more seed call: `{ payment_methods: ["offline", "card"] }`.
144
- - Seed-time `locations` is THE shipping path; patching `commerce.ShippingTaxLocation` is the day-2 route.
144
+ Then copy this file's `carry_forward` lines (in its front matter) into your working notes, and do not re-read this file.
@@ -55,6 +55,8 @@ A kit update re-copies `shared/commerce/` and restores the stub — re-run the c
55
55
 
56
56
  Checkout (`place-order`, `card` gateway) creates the order `pending`, runs `createCardPayment`, stores its `reference` on the order and redirects to `url`; return URLs carry `order_id`/`order_key`/`payment=`. Confirmation is **two idempotent paths** (the second a no-op): the customer return (`/order-received` → `commerce/payments` `complete-return` → `checkCardPaymentPaid` → `processing` with all side-effects; `useOrderReturn` is that page in one hook) and the webhook (covers the closed tab). `create-link` mints a fresh page for any unpaid order the same way; `admin-refunds` `refund_payment: true` calls `refundCardPayment` **before** writing locally (unimplemented → `501`; record without the flag instead). Gateway enabled with no provider behind it → `503 no_card_payment_provider`; every other payment option is **manual** (on-hold, the option's description as payment instructions) and needs no code.
57
57
 
58
+ ⚑ **Card payment cannot complete in the preview pane.** Providers serve their payment page `frame-ancestors 'none'`, so the redirect dead-ends in the frame. `useCheckout`'s `placeOrder` refuses up front — `{ ok: false, error: { code: "card_payment_in_preview" } }`, nothing created — because `place-order` takes the order and its stock hold *before* minting the payment page, and an attempt that dies at the redirect would strand an unpayable order. Manual gateways are unaffected. **Test card checkout in the published store**; a card flow that appears to do nothing in preview and works published is this, not broken provider wiring.
59
+
58
60
  ## Implementation rules
59
61
 
60
62
  For a **custom** provider (the shipped files already obey all of these):
@@ -9,6 +9,22 @@ import {
9
9
  shippingSlice,
10
10
  } from "./address";
11
11
 
12
+ /**
13
+ * Card payment cannot complete in the app builder's preview pane. The
14
+ * provider's payment page is a full-page redirect and providers serve theirs
15
+ * `frame-ancestors 'none'`, so the redirect navigates the pane, the provider
16
+ * refuses to render in it, and checkout looks like it silently did nothing. A
17
+ * storefront runs framed in exactly one place, so "framed" is read as "preview".
18
+ */
19
+ function inPreview() {
20
+ if (typeof window === "undefined") return false;
21
+ try {
22
+ return window.self !== window.top;
23
+ } catch {
24
+ return true;
25
+ }
26
+ }
27
+
12
28
  const EMPTY_ADDRESS = Object.freeze({
13
29
  first_name: "",
14
30
  last_name: "",
@@ -70,6 +86,12 @@ function resolvePaymentMethod(gateways, picked) {
70
86
  * handle the result yourself (a manual result carries
71
87
  * `result.payment_instructions`).
72
88
  *
89
+ * **A card order will not place from the preview pane** — providers refuse to
90
+ * be framed, so the redirect can only dead-end there. `placeOrder` resolves
91
+ * `{ ok: false, error: { code: "card_payment_in_preview" } }` without
92
+ * creating anything; manual gateways are unaffected. Test card checkout in
93
+ * the published store.
94
+ *
73
95
  * Both navigations are **full page loads** (`window.location.assign`): the
74
96
  * provider hop leaves the app, and the receipt boots from the URL alone
75
97
  * (`order_id` + `order_key`), so a reload there is correct and shareable.
@@ -215,6 +237,18 @@ export function useCheckout(options = {}) {
215
237
  const placeOrder = useCallback(
216
238
  async (extra = {}) => {
217
239
  if (placing) return { ok: false, error: { code: "placing", message: "Order already being placed." } };
240
+ // Refuse a card checkout from the preview pane before the network, not
241
+ // after it: `place-order` creates the order and takes its stock hold
242
+ // *before* the payment page exists, so an attempt that can only die at
243
+ // the redirect would strand a real pending order nobody can pay.
244
+ if (paymentMethod === "card" && inPreview()) {
245
+ const error = {
246
+ code: "card_payment_in_preview",
247
+ message: "Card payment can't be completed in preview. Open the published store to pay.",
248
+ };
249
+ setOrderError(error);
250
+ return { ok: false, error };
251
+ }
218
252
  // Safe as `onClick={checkout.placeOrder}`: a DOM/React event is not
219
253
  // `extra`. Spreading one into the order payload would send a circular
220
254
  // SyntheticEvent as order fields — an explicit object still forwards.