@base44/app-plugin-commerce 0.2.1 → 0.2.3

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 (59) hide show
  1. package/README.md +6 -6
  2. package/base44/agents/commerce/StoreAdmin.jsonc +1 -1
  3. package/base44/entities/commerce.OrderRefund.jsonc +1 -1
  4. package/base44/entities/commerce.PaymentGateway.jsonc +1 -1
  5. package/base44/entities/commerce.Webhook.jsonc +1 -1
  6. package/base44/functions/commerce/admin-products/entry.ts +1 -1
  7. package/base44/functions/commerce/admin-reports/entry.ts +1 -1
  8. package/base44/functions/commerce/payments/entry.ts +2 -2
  9. package/base44/functions/commerce/seed-store/defaults.ts +1 -1
  10. package/base44/functions/commerce/seed-store/seed-catalog.ts +64 -6
  11. package/base44/functions/commerce/storefront-catalog/entry.ts +1 -1
  12. package/base44/functions/commerce/storefront-checkout/entry.ts +1 -1
  13. package/base44/shared/commerce/card-payment.stripe.ts +29 -9
  14. package/base44/shared/commerce/card-payment.ts +1 -1
  15. package/base44/shared/commerce/payments.ts +2 -2
  16. package/base44/shared/commerce/scan.ts +1 -1
  17. package/base44/shared/commerce/sequence.ts +2 -2
  18. package/package.json +1 -1
  19. package/scripts/install.js +1 -1
  20. package/skills/commerce/SKILL.md +36 -26
  21. package/skills/commerce/docs/api-storefront.md +6 -6
  22. package/skills/commerce/install/01-install.md +2 -2
  23. package/skills/commerce/install/02-storefront.md +381 -94
  24. package/skills/commerce/install/03-data.md +62 -19
  25. package/skills/commerce/references/catalog-rendering.md +6 -6
  26. package/skills/commerce/references/online-payments.md +5 -6
  27. package/skills/commerce/references/reviews.md +5 -5
  28. package/skills/commerce/references/shipping-and-tax.md +2 -0
  29. package/src/commerce/admin/README.md +6 -3
  30. package/src/commerce/admin/layout/AuthGuard.jsx +1 -1
  31. package/src/commerce/admin/pages/products/Reviews.jsx +1 -1
  32. package/src/commerce/admin/pages/settings/InventorySettings.jsx +1 -1
  33. package/src/commerce/admin/pages/settings/PaymentsSettings.jsx +1 -1
  34. package/src/commerce/storefront/index.js +45 -33
  35. package/src/commerce/storefront/useAddressForm.js +41 -8
  36. package/src/commerce/storefront/useCartLine.js +37 -0
  37. package/src/commerce/storefront/useCheckout.jsx +18 -6
  38. package/src/commerce/storefront/useOrderReturn.js +36 -10
  39. package/src/commerce/storefront/useProduct.js +72 -0
  40. package/src/commerce/storefront/useProductGallery.js +4 -0
  41. package/src/commerce/utils/index.js +9 -6
  42. package/src/commerce/utils/shipping-promos.js +2 -2
  43. package/src/commerce/utils/specs.js +26 -0
  44. package/src/commerce/utils/variants.js +49 -2
  45. package/src/commerce/storefront/blocks/AddToCartBlock.jsx +0 -86
  46. package/src/commerce/storefront/blocks/AddressFieldsBlock.jsx +0 -96
  47. package/src/commerce/storefront/blocks/BreadcrumbsBlock.jsx +0 -52
  48. package/src/commerce/storefront/blocks/CartLinesBlock.jsx +0 -98
  49. package/src/commerce/storefront/blocks/CheckoutBlock.jsx +0 -247
  50. package/src/commerce/storefront/blocks/CouponFieldBlock.jsx +0 -84
  51. package/src/commerce/storefront/blocks/OrderReceivedBlock.jsx +0 -129
  52. package/src/commerce/storefront/blocks/ProductGalleryBlock.jsx +0 -66
  53. package/src/commerce/storefront/blocks/ProductSpecsBlock.jsx +0 -33
  54. package/src/commerce/storefront/blocks/ProductStripBlock.jsx +0 -55
  55. package/src/commerce/storefront/blocks/QuantityStepper.jsx +0 -62
  56. package/src/commerce/storefront/blocks/ReviewsBlock.jsx +0 -191
  57. package/src/commerce/storefront/blocks/TotalsBlock.jsx +0 -42
  58. package/src/commerce/storefront/blocks/VariantSelectorBlock.jsx +0 -81
  59. package/src/commerce/storefront/blocks/index.js +0 -44
@@ -1,55 +0,0 @@
1
- import React from "react";
2
- import { useProductList } from "../useProductList";
3
-
4
- /**
5
- * A row of products — a featured rail, "new in", upsells beside a product, a
6
- * few picks under an article.
7
- *
8
- * <ProductStripBlock params={{ featured: true, per_page: 4 }}
9
- * renderCard={(p) => <MyCard key={p.id} product={p} />} />
10
- *
11
- * **`renderCard` is required and has no default**: the product card is the most
12
- * identity-defining component in a storefront, so the kit never ships one. What
13
- * this block owns is everything around it — the query, the loading state, and
14
- * the fact that any filter may legitimately match nothing (nobody has starred a
15
- * product, nothing is on sale), in which case the strip renders nothing at all
16
- * rather than an empty row with a heading.
17
- *
18
- * Give it `products` instead of `params` to render rows you already have (a
19
- * product page's `upsells`/`crossSells`).
20
- *
21
- * @param {{renderCard: (product: object) => React.ReactNode, params?: object,
22
- * products?: Array<object>, title?: string, className?: string,
23
- * listClassName?: string, slots?: {loading?: React.ReactNode}}} props
24
- */
25
- export function ProductStripBlock({
26
- renderCard,
27
- params,
28
- products: given,
29
- title,
30
- className = "",
31
- listClassName = "grid grid-cols-2 gap-6 md:grid-cols-4",
32
- slots = {},
33
- }) {
34
- const enabled = !given;
35
- const list = useProductList(params ?? {}, { perPage: params?.per_page ?? 4 });
36
- const rows = given ?? (enabled ? list.products : []);
37
- const loading = enabled && list.loading;
38
-
39
- if (loading) return slots.loading ?? null;
40
- // A filter that matched nothing is normal — render nothing, not a bare heading.
41
- if (!rows.length) return null;
42
-
43
- return (
44
- <section data-commerce="product-strip" className={className}>
45
- {title && <h2 className="mb-4 text-xs uppercase tracking-wide text-muted-foreground">{title}</h2>}
46
- <ul className={listClassName}>
47
- {rows.map((p) => (
48
- <li key={p.id} data-commerce="product-card">
49
- {renderCard(p)}
50
- </li>
51
- ))}
52
- </ul>
53
- </section>
54
- );
55
- }
@@ -1,62 +0,0 @@
1
- import React from "react";
2
- import { useCartLine } from "../useCartLine";
3
-
4
- /**
5
- * A cart line's quantity control: − / value / + plus Remove, with stock
6
- * rejections rendered inline and rapid clicks coalesced.
7
- *
8
- * <QuantityStepper line={line} />
9
- *
10
- * @param {{line: object, className?: string, showRemove?: boolean,
11
- * removeLabel?: string}} props `line` comes from `useCart().lines`
12
- */
13
- export function QuantityStepper({ line, className = "", showRemove = true, removeLabel = "Remove" }) {
14
- const l = useCartLine(line);
15
- if (!line) return null;
16
-
17
- return (
18
- <div data-commerce="quantity" className={className}>
19
- <div className="flex items-center gap-1">
20
- <button
21
- type="button"
22
- data-commerce="quantity-decrease"
23
- onClick={l.decrease}
24
- disabled={!l.canDecrease || l.pending}
25
- aria-label="Decrease quantity"
26
- className="h-8 w-8 border border-border disabled:opacity-40"
27
- >
28
-
29
- </button>
30
- <span data-commerce="quantity-value" aria-live="polite" className="w-10 text-center text-sm">
31
- {l.quantity}
32
- </span>
33
- <button
34
- type="button"
35
- data-commerce="quantity-increase"
36
- onClick={l.increase}
37
- disabled={!l.canIncrease || l.pending}
38
- aria-label="Increase quantity"
39
- className="h-8 w-8 border border-border disabled:opacity-40"
40
- >
41
- +
42
- </button>
43
- {showRemove && (
44
- <button
45
- type="button"
46
- data-commerce="line-remove"
47
- onClick={l.remove}
48
- disabled={l.pending}
49
- className="ml-3 text-xs uppercase tracking-wide text-muted-foreground underline disabled:opacity-40"
50
- >
51
- {removeLabel}
52
- </button>
53
- )}
54
- </div>
55
- {l.error && (
56
- <p data-commerce="quantity-error" role="alert" className="mt-1 text-xs text-destructive">
57
- {l.error.message}
58
- </p>
59
- )}
60
- </div>
61
- );
62
- }
@@ -1,191 +0,0 @@
1
- import React from "react";
2
- import { useProductReviews } from "../useProductReviews";
3
-
4
- /**
5
- * The reviews section: the list, the aggregate rating, and the submit form —
6
- * including the store's policy and the right confirmation copy.
7
- *
8
- * <ReviewsBlock product={product} /> // anyone with an email
9
- * <ReviewsBlock product={product} policy="login" user={user} />
10
- * <ReviewsBlock product={product} policy="verified_buyers" user={user} />
11
- *
12
- * The backend ships complete; what this adds is the whole UI contract that used
13
- * to be hand-rolled: field-level validation matching the server's error codes,
14
- * the email field hidden for a signed-in visitor (the session's email wins
15
- * server-side anyway), paging, a refresh after an auto-approved submission, and
16
- * confirmation text **taken from the response** — a store with auto-approval on
17
- * is told "published", not "awaiting approval".
18
- *
19
- * @param {{product: object, policy?: "open"|"login"|"verified_buyers",
20
- * user?: object|null, requireRating?: boolean, className?: string,
21
- * title?: string, showForm?: boolean,
22
- * slots?: {blocked?: React.ReactNode}}} props
23
- */
24
- export function ReviewsBlock({
25
- product,
26
- policy = "open",
27
- user = null,
28
- requireRating = false,
29
- className = "",
30
- title = "Reviews",
31
- showForm = true,
32
- slots = {},
33
- }) {
34
- const r = useProductReviews(product, { policy, user, requireRating });
35
-
36
- return (
37
- <section data-commerce="reviews" className={className}>
38
- <header className="flex items-baseline justify-between gap-4">
39
- <h2 className="text-xs uppercase tracking-wide text-muted-foreground">{title}</h2>
40
- {r.ratingCount > 0 && (
41
- <p data-commerce="reviews-summary" className="text-sm">
42
- {Number(r.averageRating).toFixed(1)} / 5
43
- <span className="text-muted-foreground"> · {r.ratingCount} review{r.ratingCount === 1 ? "" : "s"}</span>
44
- </p>
45
- )}
46
- </header>
47
-
48
- {r.loading ? (
49
- <p className="mt-4 text-sm text-muted-foreground">Loading reviews…</p>
50
- ) : r.items.length === 0 ? (
51
- <p data-commerce="reviews-empty" className="mt-4 text-sm text-muted-foreground">
52
- No reviews yet.
53
- </p>
54
- ) : (
55
- <ul className="mt-4 divide-y divide-border">
56
- {r.items.map((review) => (
57
- <li key={review.id} data-commerce="review" className="py-4">
58
- <p className="text-sm">
59
- <span className="font-medium">{review.reviewer}</span>
60
- {review.rating ? <span className="text-muted-foreground"> · {review.rating}/5</span> : null}
61
- {review.verified && (
62
- <span data-commerce="review-verified" className="text-muted-foreground"> · Verified buyer</span>
63
- )}
64
- </p>
65
- <p className="mt-1 whitespace-pre-line text-sm">{review.review}</p>
66
- </li>
67
- ))}
68
- </ul>
69
- )}
70
-
71
- {r.hasNext && (
72
- <button
73
- type="button"
74
- data-commerce="reviews-more"
75
- onClick={r.loadMore}
76
- className="mt-4 border border-border px-4 py-2 text-sm"
77
- >
78
- Load more reviews
79
- </button>
80
- )}
81
-
82
- {showForm && (
83
- <div className="mt-8">
84
- {r.submitted ? (
85
- <p data-commerce="review-submitted" role="status" className="text-sm">{r.message}</p>
86
- ) : r.reviewBlockedReason ? (
87
- (slots.blocked ?? (
88
- <p data-commerce="review-blocked" className="text-sm text-muted-foreground">
89
- {r.reviewBlockedReason === "login_required"
90
- ? "Sign in to write a review."
91
- : "Only verified buyers can review this product."}
92
- </p>
93
- ))
94
- ) : (
95
- <ReviewForm r={r} />
96
- )}
97
- </div>
98
- )}
99
- </section>
100
- );
101
- }
102
-
103
- function ReviewForm({ r }) {
104
- const field = (key, label, node) => (
105
- <label className="block">
106
- <span className="block text-xs text-muted-foreground">{label}</span>
107
- {node}
108
- {r.fieldErrors[key] && (
109
- <span role="alert" className="mt-1 block text-xs text-destructive">{r.fieldErrors[key]}</span>
110
- )}
111
- </label>
112
- );
113
-
114
- return (
115
- <form
116
- data-commerce="review-form"
117
- onSubmit={(e) => {
118
- e.preventDefault();
119
- r.submit();
120
- }}
121
- className="space-y-3"
122
- >
123
- <h3 className="text-xs uppercase tracking-wide text-muted-foreground">Write a review</h3>
124
-
125
- <div className="grid gap-3 sm:grid-cols-2">
126
- {field(
127
- "reviewer",
128
- "Your name",
129
- <input
130
- data-commerce="review-name"
131
- value={r.form.reviewer}
132
- onChange={(e) => r.setField("reviewer", e.target.value)}
133
- className="mt-1 w-full border border-border bg-transparent px-3 py-2 text-sm"
134
- />,
135
- )}
136
- {r.requiresEmail &&
137
- field(
138
- "email",
139
- "Email",
140
- <input
141
- data-commerce="review-email"
142
- type="email"
143
- value={r.form.email}
144
- onChange={(e) => r.setField("email", e.target.value)}
145
- className="mt-1 w-full border border-border bg-transparent px-3 py-2 text-sm"
146
- />,
147
- )}
148
- </div>
149
-
150
- {field(
151
- "rating",
152
- "Rating",
153
- <select
154
- data-commerce="review-rating"
155
- value={r.form.rating}
156
- onChange={(e) => r.setField("rating", Number(e.target.value))}
157
- className="mt-1 w-full border border-border bg-transparent px-3 py-2 text-sm sm:w-32"
158
- >
159
- {[5, 4, 3, 2, 1].map((n) => (
160
- <option key={n} value={n}>{n} / 5</option>
161
- ))}
162
- </select>,
163
- )}
164
-
165
- {field(
166
- "review",
167
- "Your review",
168
- <textarea
169
- data-commerce="review-text"
170
- rows={4}
171
- value={r.form.review}
172
- onChange={(e) => r.setField("review", e.target.value)}
173
- className="mt-1 w-full border border-border bg-transparent px-3 py-2 text-sm"
174
- />,
175
- )}
176
-
177
- {r.fieldErrors.form && (
178
- <p role="alert" className="text-xs text-destructive">{r.fieldErrors.form}</p>
179
- )}
180
-
181
- <button
182
- type="submit"
183
- data-commerce="review-submit"
184
- disabled={!r.valid || r.submitting}
185
- className="border border-border px-4 py-2 text-sm disabled:opacity-50"
186
- >
187
- {r.submitting ? "Submitting…" : "Submit review"}
188
- </button>
189
- </form>
190
- );
191
- }
@@ -1,42 +0,0 @@
1
- import React from "react";
2
- import { useTotalsLines } from "../useTotalsLines";
3
-
4
- /**
5
- * The order summary — subtotal, discount, shipping, tax, total.
6
- *
7
- * <TotalsBlock /> // the shared cart
8
- * <TotalsBlock order={order} /> // a placed order
9
- *
10
- * Renders every line the store actually has, which is the point: a hand-written
11
- * summary omits discount and tax (they are usually zero) and then stops adding
12
- * up the day a coupon or a tax rate exists.
13
- *
14
- * @param {{order?: object, className?: string, showZero?: boolean,
15
- * labels?: Record<string, string>, renderLine?: (line: object) => React.ReactNode}} props
16
- */
17
- export function TotalsBlock({ order, className = "", showZero = false, labels, renderLine }) {
18
- const lines = useTotalsLines(order);
19
- const visible = lines.filter((l) => showZero || !l.hidden);
20
- if (!visible.length) return null;
21
-
22
- return (
23
- <div data-commerce="totals" className={`space-y-2 ${className}`}>
24
- {visible.map((line) => {
25
- const l = labels?.[line.key] ? { ...line, label: labels[line.key] } : line;
26
- if (renderLine) return <React.Fragment key={l.key}>{renderLine(l)}</React.Fragment>;
27
- return (
28
- <div
29
- key={l.key}
30
- data-commerce={`totals-${l.key}`}
31
- className={`flex items-baseline justify-between gap-4 ${
32
- l.emphasis ? "border-t border-border pt-2 text-base font-medium" : "text-sm"
33
- }`}
34
- >
35
- <span className={l.emphasis ? "" : "text-muted-foreground"}>{l.label}</span>
36
- <span data-commerce-value={l.amount}>{l.formatted}</span>
37
- </div>
38
- );
39
- })}
40
- </div>
41
- );
42
- }
@@ -1,81 +0,0 @@
1
- import React from "react";
2
- import { OPTION_OUT_OF_STOCK, OPTION_UNAVAILABLE } from "@/commerce/utils";
3
-
4
- /**
5
- * One control per attribute axis — Size, Color — with availability shown rather
6
- * than hidden.
7
- *
8
- * const { view, pick } = useProduct(slug);
9
- * <VariantSelectorBlock view={view} onPick={pick} />
10
- *
11
- * The rule it encodes: **a selector per axis, never a list of variations**, and
12
- * an option that isn't buyable is rendered *disabled*, not removed — a customer
13
- * who can't see that a size exists assumes the store doesn't carry it. Pass
14
- * `renderOption` for swatches or any other option UI.
15
- *
16
- * @param {{view: object, onPick: (axisKey: string, option: string) => void,
17
- * className?: string,
18
- * renderOption?: (o: {axis: object, option: string, selected: boolean,
19
- * state: string, disabled: boolean, pick: () => void}) => React.ReactNode}} props
20
- */
21
- export function VariantSelectorBlock({ view, onPick, className = "", renderOption }) {
22
- if (!view?.isVariable || !view.axes?.length) return null;
23
-
24
- return (
25
- <div data-commerce="variant-selector" className={`space-y-4 ${className}`}>
26
- {view.axes.map((axis) => (
27
- <fieldset key={axis.key} data-commerce="variant-axis" data-commerce-axis={axis.name}>
28
- <legend className="mb-2 text-xs uppercase tracking-wide text-muted-foreground">
29
- {axis.name}
30
- {view.selection?.[axis.key] ? `: ${view.selection[axis.key]}` : ""}
31
- </legend>
32
- <div className="flex flex-wrap gap-2">
33
- {axis.options.map((option) => {
34
- const state = view.availability?.[axis.key]?.[option] ?? "available";
35
- const selected = view.selection?.[axis.key] === option;
36
- const disabled = state === OPTION_UNAVAILABLE;
37
- const pick = () => onPick?.(axis.key, option);
38
-
39
- if (renderOption) {
40
- return (
41
- <React.Fragment key={option}>
42
- {renderOption({ axis, option, selected, state, disabled, pick })}
43
- </React.Fragment>
44
- );
45
- }
46
- return (
47
- <button
48
- key={option}
49
- type="button"
50
- data-commerce="variant-option"
51
- data-commerce-state={state}
52
- aria-pressed={selected}
53
- disabled={disabled}
54
- onClick={pick}
55
- title={
56
- state === OPTION_OUT_OF_STOCK
57
- ? "Out of stock"
58
- : disabled
59
- ? "Not available with your other choices"
60
- : undefined
61
- }
62
- className={`border px-3 py-1.5 text-sm ${
63
- selected ? "border-foreground" : "border-border"
64
- } ${state === OPTION_OUT_OF_STOCK ? "line-through opacity-60" : ""} disabled:opacity-40`}
65
- >
66
- {option}
67
- </button>
68
- );
69
- })}
70
- </div>
71
- </fieldset>
72
- ))}
73
-
74
- {view.missingAxes?.length > 0 && (
75
- <p data-commerce="variant-hint" className="text-xs text-muted-foreground">
76
- Choose {view.missingAxes.map((a) => a.name.toLowerCase()).join(" and ")} to continue.
77
- </p>
78
- )}
79
- </div>
80
- );
81
- }
@@ -1,44 +0,0 @@
1
- /**
2
- * Storefront blocks — working default markup for the parts of a shopfront that
3
- * are the same in every store.
4
- *
5
- * The split is deliberate:
6
- *
7
- * - **Identity surfaces are yours**: home, collection/grid, the product page's
8
- * layout, the product card, the theme. That is where "make it look like X"
9
- * lives, and no markup ships for it.
10
- * - **Commodity surfaces ship as blocks**: checkout, cart lines, totals, the
11
- * coupon field, reviews, order-received, and the product page's internals
12
- * (variant selector, gallery, specs, breadcrumbs, strips). Every store's
13
- * version of these is functionally identical, and hand-writing them is where
14
- * storefront bugs cluster.
15
- *
16
- * Blocks are thin compositions of the hooks in this package, styled by
17
- * inheritance: semantic markup, theme tokens (`border-border`, `text-muted-
18
- * foreground`, `bg-primary`), and a stable `data-commerce="…"` attribute on
19
- * every meaningful element — so your CSS can restyle any part of them, and
20
- * tests can target them. Each takes `className` and, where there is more than
21
- * one region, `slots`. When a design needs different *structure*, drop to the
22
- * hooks the block uses and rewrite that one region.
23
- *
24
- * A whole checkout page:
25
- *
26
- * import { CheckoutBlock } from "@/commerce/storefront";
27
- * export default function Checkout() {
28
- * return <main className="mx-auto max-w-3xl px-6 py-16"><CheckoutBlock /></main>;
29
- * }
30
- */
31
- export { CartLinesBlock } from "./CartLinesBlock";
32
- export { CheckoutBlock } from "./CheckoutBlock";
33
- export { AddressFieldsBlock } from "./AddressFieldsBlock";
34
- export { CouponFieldBlock } from "./CouponFieldBlock";
35
- export { TotalsBlock } from "./TotalsBlock";
36
- export { QuantityStepper } from "./QuantityStepper";
37
- export { OrderReceivedBlock } from "./OrderReceivedBlock";
38
- export { ReviewsBlock } from "./ReviewsBlock";
39
- export { VariantSelectorBlock } from "./VariantSelectorBlock";
40
- export { AddToCartBlock } from "./AddToCartBlock";
41
- export { ProductGalleryBlock } from "./ProductGalleryBlock";
42
- export { ProductSpecsBlock } from "./ProductSpecsBlock";
43
- export { BreadcrumbsBlock } from "./BreadcrumbsBlock";
44
- export { ProductStripBlock } from "./ProductStripBlock";