@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.
@@ -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(secrets.get(name) ?? "");
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") || String(Deno.env.get("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 (`secrets.get("...")` from
38
- * `base44:runtime`) — never in an entity, never in the code, never from the
39
- * client. When one is missing, log which one and answer the caller with the
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.1",
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",
@@ -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 | 37K |
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