@base44/app-plugin-commerce 0.8.1 → 0.8.2
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/base44/shared/commerce/card-payment.stripe.ts +7 -3
- package/base44/shared/commerce/card-payment.ts +5 -3
- package/package.json +1 -1
- package/skills/commerce/SKILL.md +1 -1
- package/skills/commerce/install/02-storefront.md +3 -1
- package/skills/commerce/install/03-data.md +2 -2
- package/src/commerce/storefront/pickers.jsx +5 -0
- package/src/commerce/utils/storefront.js +9 -0
|
@@ -39,7 +39,6 @@
|
|
|
39
39
|
* `card-payment.<provider>.ts` next to the stub. `references/online-payments.md`
|
|
40
40
|
* has the rules; this file is the worked model.
|
|
41
41
|
*/
|
|
42
|
-
import { secrets } from "base44:runtime";
|
|
43
42
|
import { HttpError } from "./auth.ts";
|
|
44
43
|
|
|
45
44
|
/** Stripe's REST API, called directly — no SDK to bundle in the function. */
|
|
@@ -63,8 +62,13 @@ export interface CardPaymentPage {
|
|
|
63
62
|
* checkout instead of failing to boot every commerce function that imports it.
|
|
64
63
|
*/
|
|
65
64
|
const secret = (name: string): string => {
|
|
65
|
+
// Deno.env, never `secrets` from "base44:runtime": that specifier resolves
|
|
66
|
+
// only in the Deno function runtime, and this file lives under shared/ where
|
|
67
|
+
// a client bundler can reach it — the static import fails the Vite build of
|
|
68
|
+
// the whole storefront. Base44 publishes app secrets into the function
|
|
69
|
+
// environment, so Deno.env.get reads the same values.
|
|
66
70
|
try {
|
|
67
|
-
return String(
|
|
71
|
+
return String(Deno.env.get(name) ?? "");
|
|
68
72
|
} catch {
|
|
69
73
|
return "";
|
|
70
74
|
}
|
|
@@ -100,7 +104,7 @@ const stripeKey = (): string => {
|
|
|
100
104
|
* as `base44_app_id`, which is how the platform attributes a Stripe payment
|
|
101
105
|
* back to this app — send it on every call that creates money movement.
|
|
102
106
|
*/
|
|
103
|
-
const base44AppId = (): string => secret("BASE44_APP_ID")
|
|
107
|
+
const base44AppId = (): string => secret("BASE44_APP_ID");
|
|
104
108
|
|
|
105
109
|
/**
|
|
106
110
|
* One Stripe REST call. A body makes it a POST (form-encoded, with an
|
|
@@ -34,9 +34,11 @@
|
|
|
34
34
|
* Until implemented, the Credit Card checkout option answers
|
|
35
35
|
* 503 `no_card_payment_provider`.
|
|
36
36
|
*
|
|
37
|
-
* Credentials belong in Base44 secrets
|
|
38
|
-
*
|
|
39
|
-
*
|
|
37
|
+
* Credentials belong in Base44 secrets, read with `Deno.env.get("...")` —
|
|
38
|
+
* never in an entity, never in the code, never from the client. Do NOT import
|
|
39
|
+
* `secrets` from `base44:runtime` here: that specifier resolves only in the
|
|
40
|
+
* Deno function runtime, and a static import of it in this shared file fails
|
|
41
|
+
* the client bundle of the whole storefront. When one is missing, log which one and answer the caller with the
|
|
40
42
|
* flat 503 below: the storefront must not learn the names of the app's
|
|
41
43
|
* secrets.
|
|
42
44
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@base44/app-plugin-commerce",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
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",
|
package/skills/commerce/SKILL.md
CHANGED
|
@@ -123,7 +123,7 @@ batch (above).
|
|
|
123
123
|
| Topic | Open when | Size |
|
|
124
124
|
|---|---|---|
|
|
125
125
|
| [`install/01-install.md`](./install/01-install.md) | installing — routes you to 02 and 03 | 6K |
|
|
126
|
-
| [`install/02-storefront.md`](./install/02-storefront.md) | building storefront pages |
|
|
126
|
+
| [`install/02-storefront.md`](./install/02-storefront.md) | building storefront pages | 38K |
|
|
127
127
|
| [`install/03-data.md`](./install/03-data.md) | seeding catalog, shipping rates/zones, payments; re-callable per slice | 11K |
|
|
128
128
|
| [`docs/entities.md`](./docs/entities.md) | any direct entity read/write ("which entity holds X") | 11K |
|
|
129
129
|
| [`references/catalog-rendering.md`](./references/catalog-rendering.md) | field shapes each catalog call returns, variant edge cases | 16K |
|
|
@@ -25,6 +25,8 @@ One split decides everything here: **the logic is premade, the UI never is.** Th
|
|
|
25
25
|
|
|
26
26
|
**One import path: `@/commerce/storefront`.** Each section opens with its page's exact import line — copy it verbatim, then delete unused names. Everything a page needs is re-exported there, so a React page never imports `@/commerce/utils` directly. `useStoreInfo` is the name most often left out.
|
|
27
27
|
|
|
28
|
+
⚑ **Only some of what lives there is importable.** Hooks and helpers (`useProduct`, `useCart`, `productImages`, …) are named exports; a few operations exist **only on the client** `useStorefront()` returns — `submitReview`, `getProductReviews`, `listProducts`, `getProduct`, `applyCoupon`, `chooseShippingMethod`, `completeReturn`. Importing one by name throws `does not provide an export named …` and blanks the whole app: `const c = useStorefront(); await c.submitReview(…)`.
|
|
29
|
+
|
|
28
30
|
## Setup — once
|
|
29
31
|
|
|
30
32
|
Nearly every store has shared chrome, so **start from a pathless layout route** — it also keeps the admin outside the storefront's provider:
|
|
@@ -317,7 +319,7 @@ function CheckoutForm() {
|
|
|
317
319
|
|
|
318
320
|
**`<AddressFields>` is the one shipped component — use it, never hand-roll the address form.** It owns what hand-rolled forms get wrong: the state/province field appears with the right options once a country is picked (shipping rates and taxes match on country *plus* state, so a form without it mis-prices US/CA/AU orders with no error anywhere), every field keeps its `autoComplete` token (what makes browser autofill work), required marks arm on first blur, and the server's "we don't ship there" lands on the country field. `which="shipping"` renders null until `shipToDifferent` is on — the deliver-elsewhere checkbox itself is yours, wired to `c.shipToDifferent` / `c.setShipToDifferent`.
|
|
319
321
|
|
|
320
|
-
It ships **no CSS** bar a `max-width:100%` cap on the selects (an unstyled checkout must not scroll sideways): every element carries `data-part` (`address-fields`, `field`, `label`, `control`, `required`, `error`) plus `data-key` (the field) and `data-span` (1 or 2 — the field's natural width in a two-column grid), so style it in your `index.css` via `[data-part]` selectors or pass `className`/`classes={{ field, label, control, error }}`. ⚑ **`data-part` sits on the element, not a wrapper** — `select[data-part="control"]`, never `[data-part="control"] input`: the descendant form matches nothing and ships the form unstyled. Props: `includeCompany` (false), `includePhone` (true), `omit={["…"]}`, `labels={{ postcode: "ZIP code" }}`, `selectPlaceholder`, and two escape hatches — `inputRender` swaps the control only (spread the handed `dom` props onto your input), `fieldRender` replaces the whole labeled block. `c.missingBillingFields` stays the live list of what is missing, for your own per-field marks.
|
|
322
|
+
⚑ **Pass `classes.control`** — an unstyled `<input>` still reads as deliberate; a `<select>` does not, so skipping this one prop leaves the checkout looking broken on exactly one field. It ships **no CSS** bar a `max-width:100%` cap on the selects (an unstyled checkout must not scroll sideways): every element carries `data-part` (`address-fields`, `field`, `label`, `control`, `required`, `error`) plus `data-key` (the field) and `data-span` (1 or 2 — the field's natural width in a two-column grid), so style it in your `index.css` via `[data-part]` selectors or pass `className`/`classes={{ field, label, control, error }}`. ⚑ **`data-part` sits on the element, not a wrapper** — `select[data-part="control"]`, never `[data-part="control"] input`: the descendant form matches nothing and ships the form unstyled. Props: `includeCompany` (false), `includePhone` (true), `omit={["…"]}`, `labels={{ postcode: "ZIP code" }}`, `selectPlaceholder`, and two escape hatches — `inputRender` swaps the control only (spread the handed `dom` props onto your input), `fieldRender` replaces the whole labeled block. `c.missingBillingFields` stays the live list of what is missing, for your own per-field marks.
|
|
321
323
|
|
|
322
324
|
⚑ **The `stage === "submitted"` guard goes above the empty-cart branch** — placing an order clears the cart before the browser navigates, and without the guard the page flashes an empty bag over a just-placed order.
|
|
323
325
|
|
|
@@ -5,7 +5,7 @@ skip_when: "commerce/seed-store has already returned success for this store and
|
|
|
5
5
|
forget_when: "The seed response is received and recorded (slugs + warnings), and the checklist at the bottom passes."
|
|
6
6
|
carry_forward:
|
|
7
7
|
- "Product slugs come from the seed response's catalog.products[] — link pages by slug, never by a client-side map."
|
|
8
|
-
- "Payments: report at handover where they landed (default = offline on, card off) — the owner must never learn it from a customer."
|
|
8
|
+
- "Payments: report at handover where they landed (default = offline on, card off) — the owner must never learn it from a customer. Shipping the Stripe file is not a Stripe connection; only the user can authorise that."
|
|
9
9
|
- "Turning card payments on or off later is one more seed call: { payment_methods: [\"offline\", \"card\"] }."
|
|
10
10
|
- "Seed-time `locations` is THE shipping path; patching commerce.ShippingTaxLocation is the day-2 route."
|
|
11
11
|
---
|
|
@@ -131,7 +131,7 @@ Online card payments are **off by default**: the seeded store takes the manual `
|
|
|
131
131
|
| Paid another way (transfer, COD, invoice, pickup) | nothing to do — the default is exactly this |
|
|
132
132
|
| Payments not mentioned | leave the default, **state it at handover** |
|
|
133
133
|
|
|
134
|
-
*Cards only:* **if it is Stripe the code is already written** — copy `base44/shared/commerce/card-payment.stripe.ts` over `base44/shared/commerce/card-payment.ts` **whole** (`fs.copyFileSync`; a partial edit breaks every function's deploy), then enable via `payment_methods`. Nothing needs filling in. Any other provider: [`../references/online-payments.md`](../references/online-payments.md).
|
|
134
|
+
*Cards only:* **if it is Stripe the code is already written** — copy `base44/shared/commerce/card-payment.stripe.ts` over `base44/shared/commerce/card-payment.ts` **whole** (`fs.copyFileSync`; a partial edit breaks every function's deploy), then enable via `payment_methods`. Nothing needs filling in. ⚑ **That is the code, not a connection** — the app is not connected to Stripe until the user authorises it and its secret key reaches the app's secrets, which only they can do. Say the card path is *ready* and ask them to connect Stripe; never report it as connected, and never ask for a key in chat. Any other provider: [`../references/online-payments.md`](../references/online-payments.md).
|
|
135
135
|
|
|
136
136
|
## Done — forget this file
|
|
137
137
|
|
|
@@ -125,6 +125,7 @@ export function ShippingMethodPicker({ checkout: checkoutProp, children }) {
|
|
|
125
125
|
* value the selected slug ("" while none)
|
|
126
126
|
* select (slug) => void
|
|
127
127
|
* selected the selected gateway entry, or null
|
|
128
|
+
* mustChoose more than one gateway → render them as a picker
|
|
128
129
|
* single exactly one gateway — already selected
|
|
129
130
|
* hint { code: "none_available", severity, serverMessage } | null —
|
|
130
131
|
* no gateways at all: say checkout is unavailable, in your words
|
|
@@ -160,6 +161,10 @@ export function PaymentMethodPicker({ checkout: checkoutProp, children }) {
|
|
|
160
161
|
value,
|
|
161
162
|
select,
|
|
162
163
|
selected: decorated.find((g) => g.selected) ?? null,
|
|
164
|
+
// Symmetrical with ShippingMethodPicker: a checkout that gates its radios
|
|
165
|
+
// on `mustChoose` renders NOTHING when the field is missing, and a payment
|
|
166
|
+
// picker that renders nothing is a checkout no one can complete.
|
|
167
|
+
mustChoose: gateways.length > 1,
|
|
163
168
|
single: single ?? (gateways.length === 1), // fallback: a hand-built checkout object
|
|
164
169
|
hint,
|
|
165
170
|
});
|
|
@@ -65,6 +65,15 @@ export function storefrontErrorMessage(e) {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
export function createStorefront(base44, { storageKey = "cart_token", storage } = {}) {
|
|
68
|
+
// Fail at construction, not four calls later. Without this the first symptom
|
|
69
|
+
// is "Cannot read properties of undefined (reading 'functions')" thrown from
|
|
70
|
+
// inside a cart call, which reads as a kit bug rather than a missing prop.
|
|
71
|
+
if (!base44 || typeof base44.functions?.invoke !== "function") {
|
|
72
|
+
throw new Error(
|
|
73
|
+
"StorefrontProvider requires the base44 client: <StorefrontProvider base44={base44}> " +
|
|
74
|
+
'(import { base44 } from "@/api/base44Client").',
|
|
75
|
+
);
|
|
76
|
+
}
|
|
68
77
|
const bag = storage ?? (typeof localStorage !== "undefined" ? localStorage : null);
|
|
69
78
|
|
|
70
79
|
// Every function returns the envelope { success, data }; with the SDK the
|