create-brainerce-store 1.71.0 → 1.72.0

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/README.md CHANGED
@@ -44,19 +44,40 @@ Full details: [TEMPLATE-ARCHITECTURE.md](./TEMPLATE-ARCHITECTURE.md).
44
44
 
45
45
  ## Flags
46
46
 
47
- | Flag | Description | Default |
48
- | --- | --- | --- |
49
- | `--connection-id <id>` | Brainerce vibe-coded connection ID (`vc_*`) | prompted |
50
- | `--canvas` | Bare unstyled `src/ui/` skeletons for AI-driven design | off |
51
- | `--language <lang>` | Store language (`en`, `he`) | fetched from store |
52
- | `--pkg-manager <pm>` | `npm`, `pnpm`, `yarn`, `bun` | auto-detected |
53
- | `--framework <fw>` | `nextjs` (Vite/Remix coming) | `nextjs` |
54
- | `--api-url <url>` | Brainerce API base URL (or `BRAINERCE_API_URL` env) | auto-discovered |
55
- | `--no-git` | Skip git initialization | git on |
56
- | `--no-install` | Skip dependency installation | install on |
47
+ | Flag | Description | Default |
48
+ | ---------------------- | ------------------------------------------------------ | ------------------ |
49
+ | `--connection-id <id>` | Brainerce vibe-coded connection ID (`vc_*`) | prompted |
50
+ | `--canvas` | Bare unstyled `src/ui/` skeletons for AI-driven design | off |
51
+ | `--language <lang>` | Store language (`en`, `he`) | fetched from store |
52
+ | `--pkg-manager <pm>` | `npm`, `pnpm`, `yarn`, `bun` | auto-detected |
53
+ | `--framework <fw>` | `nextjs` (Vite/Remix coming) | `nextjs` |
54
+ | `--api-url <url>` | Brainerce API base URL (or `BRAINERCE_API_URL` env) | auto-discovered |
55
+ | `--no-git` | Skip git initialization | git on |
56
+ | `--no-install` | Skip dependency installation | install on |
57
57
 
58
58
  Pass `.` as the project name to scaffold into the current directory.
59
59
 
60
+ ## Reading the connection
61
+
62
+ Before scaffolding, the CLI reads the channel from `/api/vc/<id>/info` so the
63
+ generated project gets the real store name, currency and language rather than
64
+ guesses. It walks production first, then staging, and stops at the first
65
+ environment that answers. When it has to fall back, it says so and names the
66
+ URL the project will be wired to.
67
+
68
+ That endpoint checks the request's `Origin` against the channel's **Domain**
69
+ and **Allowed Origins**, and the match counts the **port**. A Test channel with
70
+ an empty Domain box accepts anything; one with `localhost:3000` recorded will
71
+ refuse a bare `http://localhost`. The CLI therefore presents
72
+ `http://localhost:3000` first and plain `http://localhost` second, and reports
73
+ what every environment said rather than only the last one. A `403` from
74
+ production must never surface as "not found on staging".
75
+
76
+ A channel it cannot read is a hard stop, not a warning. Build-time
77
+ `NEXT_PUBLIC_*` values are inlined into the client bundle and cannot be
78
+ corrected at deploy time, so guessing `USD` / `en` here would ship a storefront
79
+ quoting the wrong currency.
80
+
60
81
  ## Requirements
61
82
 
62
83
  Node `^20.19.0 || ^22.13.0 || >=24.0.0`.
package/dist/index.js CHANGED
@@ -31,7 +31,7 @@ var require_package = __commonJS({
31
31
  "package.json"(exports2, module2) {
32
32
  module2.exports = {
33
33
  name: "create-brainerce-store",
34
- version: "1.71.0",
34
+ version: "1.72.0",
35
35
  description: "Scaffold a production-ready e-commerce storefront connected to Brainerce",
36
36
  bin: {
37
37
  "create-brainerce-store": "dist/index.js"
@@ -226,7 +226,16 @@ var BRAINERCE_RUNTIME_DEPS = Object.freeze({
226
226
  // getMyProductReview() to decide whether to render a picker at all, and the
227
227
  // reviews list renders `review.images`. None of those exist on an older SDK,
228
228
  // so the scaffold fails type-check outright.
229
- brainerce: "^1.63.0",
229
+ // 2.0 corrects OrderStatus to the twelve canonical UPPERCASE values. The API
230
+ // has always returned them that way; the 1.x union declared six lowercase
231
+ // ones, so the scaffolded order timeline and order history looked their
232
+ // status maps up with a value that could never match and rendered every
233
+ // order as "Pending". Both components are now keyed on the real twelve, as
234
+ // `Record<OrderStatus, ...>`, so a 1.x SDK fails type-check on excess and
235
+ // missing keys. 2.0 also stops forgotPassword sending `resetUrl`, which the
236
+ // API rejects with 400 under forbidNonWhitelisted, so a scaffold on 1.x has
237
+ // a broken password reset in the browser.
238
+ brainerce: "^2.0.0",
230
239
  "isomorphic-dompurify": "^3.8.0"
231
240
  });
232
241
 
@@ -300,16 +309,18 @@ function detectInvokingPackageManager() {
300
309
  // src/cli.ts
301
310
  async function runInteractive(defaults) {
302
311
  const questions = [];
303
- questions.push({
304
- type: "text",
305
- name: "projectName",
306
- message: "Project name:",
307
- initial: defaults.projectName || "",
308
- validate: (value) => {
309
- const error = validateProjectName(value);
310
- return error || true;
311
- }
312
- });
312
+ if (!defaults.projectName) {
313
+ questions.push({
314
+ type: "text",
315
+ name: "projectName",
316
+ message: "Project name:",
317
+ initial: defaults.projectNameSuggestion || "",
318
+ validate: (value) => {
319
+ const error = validateProjectName(value);
320
+ return error || true;
321
+ }
322
+ });
323
+ }
313
324
  if (!defaults.connectionId) {
314
325
  questions.push({
315
326
  type: "text",
@@ -334,37 +345,42 @@ async function runInteractive(defaults) {
334
345
  initial: 0
335
346
  });
336
347
  }
337
- questions.push({
338
- type: "select",
339
- name: "pkgManager",
340
- message: "Package manager:",
341
- choices: [
342
- {
343
- title: `pnpm${detectInvokingPackageManager() === "pnpm" ? " (detected)" : ""}`,
344
- value: "pnpm"
345
- },
346
- {
347
- title: `npm${detectInvokingPackageManager() === "npm" ? " (detected)" : ""}`,
348
- value: "npm"
349
- },
350
- {
351
- title: `yarn${detectInvokingPackageManager() === "yarn" ? " (detected)" : ""}`,
352
- value: "yarn"
353
- },
354
- {
355
- title: `bun${detectInvokingPackageManager() === "bun" ? " (detected)" : ""}`,
356
- value: "bun"
357
- }
358
- ],
359
- initial: ["pnpm", "npm", "yarn", "bun"].indexOf(detectInvokingPackageManager())
360
- });
348
+ if (!defaults.pkgManager) {
349
+ questions.push({
350
+ type: "select",
351
+ name: "pkgManager",
352
+ message: "Package manager:",
353
+ choices: [
354
+ {
355
+ title: `pnpm${detectInvokingPackageManager() === "pnpm" ? " (detected)" : ""}`,
356
+ value: "pnpm"
357
+ },
358
+ {
359
+ title: `npm${detectInvokingPackageManager() === "npm" ? " (detected)" : ""}`,
360
+ value: "npm"
361
+ },
362
+ {
363
+ title: `yarn${detectInvokingPackageManager() === "yarn" ? " (detected)" : ""}`,
364
+ value: "yarn"
365
+ },
366
+ {
367
+ title: `bun${detectInvokingPackageManager() === "bun" ? " (detected)" : ""}`,
368
+ value: "bun"
369
+ }
370
+ ],
371
+ initial: ["pnpm", "npm", "yarn", "bun"].indexOf(detectInvokingPackageManager())
372
+ });
373
+ }
361
374
  const response = await (0, import_prompts.default)(questions, {
362
375
  onCancel: () => {
363
376
  throw new Error("PROMPT_CANCELLED");
364
377
  }
365
378
  });
366
379
  return {
367
- projectName: defaults.projectName || response.projectName,
380
+ // The typed answer wins. This read used to be the other way round, so a
381
+ // name typed at the prompt was silently replaced by the slug offered as
382
+ // its placeholder.
383
+ projectName: response.projectName || defaults.projectName || "",
368
384
  connectionId: defaults.connectionId || response.connectionId,
369
385
  language: response.language || defaults.language || "en",
370
386
  framework: defaults.framework || "nextjs",
@@ -625,13 +641,32 @@ var KNOWN_API_URLS = {
625
641
  production: "https://api.brainerce.com",
626
642
  staging: "https://api-staging.brainerce.com"
627
643
  };
628
- var ConnectionNotFoundError = class extends Error {
629
- constructor(connectionId, baseUrl) {
630
- super(`Connection "${connectionId}" not found at ${baseUrl}`);
631
- this.code = "NOT_FOUND";
644
+ var PROBE_ORIGINS = ["http://localhost:3000", "http://localhost"];
645
+ var StoreInfoProbeError = class extends Error {
646
+ constructor(kind, baseUrl, message, originRetryable = false) {
647
+ super(message);
648
+ this.kind = kind;
649
+ this.baseUrl = baseUrl;
650
+ this.originRetryable = originRetryable;
651
+ this.name = "StoreInfoProbeError";
632
652
  }
633
653
  };
634
- async function fetchStoreInfo(connectionId, baseUrl = KNOWN_API_URLS.production) {
654
+ function envLabel(baseUrl) {
655
+ if (baseUrl === KNOWN_API_URLS.production) return "production";
656
+ if (baseUrl === KNOWN_API_URLS.staging) return "staging";
657
+ return baseUrl;
658
+ }
659
+ async function readErrorMessage(res) {
660
+ try {
661
+ const body = await res.json();
662
+ if (typeof body.message === "string" && body.message.trim() !== "") {
663
+ return body.message.trim();
664
+ }
665
+ } catch {
666
+ }
667
+ return `HTTP ${res.status}`;
668
+ }
669
+ async function fetchStoreInfo(connectionId, baseUrl = KNOWN_API_URLS.production, origin = PROBE_ORIGINS[0]) {
635
670
  const url = `${baseUrl}/api/vc/${connectionId}/info`;
636
671
  const controller = new AbortController();
637
672
  const timeout = setTimeout(() => controller.abort(), 1e4);
@@ -639,35 +674,46 @@ async function fetchStoreInfo(connectionId, baseUrl = KNOWN_API_URLS.production)
639
674
  try {
640
675
  res = await fetch(url, {
641
676
  signal: controller.signal,
642
- headers: { Origin: "http://localhost" }
677
+ headers: { Origin: origin }
643
678
  });
644
679
  } catch (err) {
645
680
  if (err.name === "AbortError") {
646
- throw new Error(`Request to ${baseUrl} timed out`);
681
+ throw new StoreInfoProbeError("OTHER", baseUrl, "request timed out after 10s");
647
682
  }
648
- throw new Error(`Failed to connect to ${baseUrl}: ${err.message}`);
683
+ throw new StoreInfoProbeError(
684
+ "OTHER",
685
+ baseUrl,
686
+ `could not connect (${err.message})`
687
+ );
649
688
  } finally {
650
689
  clearTimeout(timeout);
651
690
  }
652
691
  if (res.status === 404) {
653
- throw new ConnectionNotFoundError(connectionId, baseUrl);
692
+ throw new StoreInfoProbeError("NOT_FOUND", baseUrl, "404 connection not found");
693
+ }
694
+ if (res.status === 403) {
695
+ const message = await readErrorMessage(res);
696
+ const originRetryable = message === "Origin not allowed for TEST channel";
697
+ throw new StoreInfoProbeError("ORIGIN_REFUSED", baseUrl, `403 ${message}`, originRetryable);
654
698
  }
655
699
  if (!res.ok) {
656
- throw new Error(`${baseUrl} returned status ${res.status}`);
700
+ throw new StoreInfoProbeError("OTHER", baseUrl, `returned status ${res.status}`);
657
701
  }
658
702
  let json;
659
703
  try {
660
704
  json = await res.json();
661
705
  } catch {
662
- throw new Error(`Invalid response from ${baseUrl}`);
706
+ throw new StoreInfoProbeError("OTHER", baseUrl, "invalid (non-JSON) response");
663
707
  }
664
708
  const storeName = json.storeName || json.name || "My Store";
665
709
  const displayName = json.channelName || storeName;
666
710
  const currency = json.currency;
667
711
  const language = json.language;
668
712
  if (!currency || !language) {
669
- throw new Error(
670
- `Malformed /info response from ${baseUrl} (missing ${!currency ? "currency" : "language"}).`
713
+ throw new StoreInfoProbeError(
714
+ "OTHER",
715
+ baseUrl,
716
+ `malformed /info response (missing ${!currency ? "currency" : "language"})`
671
717
  );
672
718
  }
673
719
  return {
@@ -678,32 +724,63 @@ async function fetchStoreInfo(connectionId, baseUrl = KNOWN_API_URLS.production)
678
724
  ...json.i18n ? { i18n: json.i18n } : {}
679
725
  };
680
726
  }
727
+ function asProbeError(err, baseUrl) {
728
+ return err instanceof StoreInfoProbeError ? err : new StoreInfoProbeError("OTHER", baseUrl, err?.message || String(err));
729
+ }
730
+ async function probeStoreInfo(connectionId, baseUrl) {
731
+ let lastError;
732
+ for (const origin of PROBE_ORIGINS) {
733
+ try {
734
+ return await fetchStoreInfo(connectionId, baseUrl, origin);
735
+ } catch (err) {
736
+ const probeError = asProbeError(err, baseUrl);
737
+ lastError = probeError;
738
+ if (!probeError.originRetryable) throw probeError;
739
+ }
740
+ }
741
+ throw lastError ?? new StoreInfoProbeError("OTHER", baseUrl, "no origins to try");
742
+ }
681
743
  async function resolveStoreInfo(connectionId, candidateUrls) {
682
744
  const urls = candidateUrls.filter((u, i) => u && candidateUrls.indexOf(u) === i);
683
745
  if (urls.length === 0) {
684
746
  throw new Error("No API URLs to try");
685
747
  }
686
- let lastError;
687
- let allNotFound = true;
748
+ const failures = [];
688
749
  for (let i = 0; i < urls.length; i++) {
689
750
  const baseUrl = urls[i];
690
751
  try {
691
- const info = await fetchStoreInfo(connectionId, baseUrl);
752
+ const info = await probeStoreInfo(connectionId, baseUrl);
692
753
  return { info, apiBaseUrl: baseUrl, fellBack: i > 0 };
693
754
  } catch (err) {
694
- lastError = err;
695
- const isNotFound = err.code === "NOT_FOUND";
696
- if (!isNotFound) {
697
- allNotFound = false;
698
- }
755
+ failures.push(asProbeError(err, baseUrl));
699
756
  }
700
757
  }
701
- if (allNotFound) {
702
- throw new Error(
703
- `Connection "${connectionId}" not found in any known environment (${urls.join(", ")}). Check your dashboard.`
704
- );
758
+ throw new Error(describeFailures(connectionId, failures));
759
+ }
760
+ function describeFailures(connectionId, failures) {
761
+ if (failures.every((f) => f.kind === "NOT_FOUND")) {
762
+ return `Connection "${connectionId}" was not found in any known environment (${failures.map((f) => envLabel(f.baseUrl)).join(", ")}). Check the connection ID in your dashboard.`;
705
763
  }
706
- throw lastError || new Error("Failed to resolve store info");
764
+ const lines = failures.map((f) => {
765
+ const label = envLabel(f.baseUrl);
766
+ const where = label === f.baseUrl ? f.baseUrl : `${label} (${f.baseUrl})`;
767
+ return ` ${where}: ${f.message}`;
768
+ });
769
+ let report = `Connection "${connectionId}" could not be read:
770
+ ${lines.join("\n")}`;
771
+ const testRefusal = failures.find((f) => f.kind === "ORIGIN_REFUSED" && f.originRetryable);
772
+ if (testRefusal) {
773
+ return report + `
774
+
775
+ The channel exists on ${envLabel(testRefusal.baseUrl)} but refused this machine's origin. Its Domain / Allowed Origins cover neither ${PROBE_ORIGINS.join(" nor ")}. On a TEST channel you can clear the Domain field, which opens it to any origin, or add the origin your dev server runs on to Allowed Origins.`;
776
+ }
777
+ const liveRefusal = failures.find((f) => f.kind === "ORIGIN_REFUSED");
778
+ if (liveRefusal) {
779
+ report += `
780
+
781
+ ${envLabel(liveRefusal.baseUrl)} refused the request outright, which is what a LIVE (domain-locked) channel does: it answers its own registered domain and never localhost. Scaffold against a TEST channel instead.`;
782
+ }
783
+ return report;
707
784
  }
708
785
 
709
786
  // src/utils/logger.ts
@@ -787,6 +864,34 @@ async function checkForUpdate(name, current) {
787
864
  return null;
788
865
  }
789
866
  }
867
+ async function resolveStoreInfoOrExit(connectionId, candidateApiUrls) {
868
+ const spinner = createSpinner("Fetching store info...");
869
+ spinner.start();
870
+ try {
871
+ const resolved = await resolveStoreInfo(connectionId, candidateApiUrls);
872
+ const info = resolved.info;
873
+ const i18nStatus = info.i18n?.enabled && info.i18n.supportedLocales.length > 1 ? ` | i18n: ${info.i18n.supportedLocales.join(", ")}` : "";
874
+ const channelLabel = info.name !== info.storeName ? `"${info.name}" (channel in "${info.storeName}")` : `"${info.name}"`;
875
+ spinner.succeed(
876
+ `Channel: ${channelLabel} | ${info.currency} | ${info.language}${i18nStatus} [${envLabel(resolved.apiBaseUrl)}]`
877
+ );
878
+ if (resolved.fellBack) {
879
+ logger.warn(
880
+ `Resolved on ${envLabel(resolved.apiBaseUrl)}, not production. This project will be wired to ${resolved.apiBaseUrl}.`
881
+ );
882
+ }
883
+ return { info, apiBaseUrl: resolved.apiBaseUrl };
884
+ } catch (err) {
885
+ spinner.fail("Could not fetch store info");
886
+ logger.error(
887
+ err instanceof Error ? err.message : "Could not reach the Brainerce API to read this connection."
888
+ );
889
+ logger.info(
890
+ "Re-run after verifying:\n \u2022 the connection ID matches a real sales channel in your dashboard\n \u2022 the machine running create-brainerce-store can reach the Brainerce API\n \u2022 if you are offline / behind a firewall, scaffold from a machine that can reach the API"
891
+ );
892
+ process.exit(1);
893
+ }
894
+ }
790
895
  var program = new import_commander.Command();
791
896
  program.name("create-brainerce-store").description("Scaffold a production-ready e-commerce storefront connected to Brainerce").version(pkg.version).argument("[project-name]", "Name for the project directory").option("--connection-id <id>", "Brainerce vibe-coded connection ID (vc_*)").option(
792
897
  "--api-url <url>",
@@ -863,33 +968,19 @@ program.name("create-brainerce-store").description("Scaffold a production-ready
863
968
  logger.error(connError2);
864
969
  process.exit(1);
865
970
  }
866
- const prefetchSpinner = createSpinner("Fetching store info...");
867
- prefetchSpinner.start();
868
- try {
869
- const resolved = await resolveStoreInfo(connectionId, candidateApiUrls);
870
- storeInfo = resolved.info;
871
- resolvedApiUrl = resolved.apiBaseUrl;
872
- const envLabel = resolved.apiBaseUrl === KNOWN_API_URLS.staging ? " [staging]" : resolved.apiBaseUrl === KNOWN_API_URLS.production ? "" : ` [${resolved.apiBaseUrl}]`;
873
- const i18nStatus = storeInfo.i18n?.enabled && storeInfo.i18n.supportedLocales.length > 1 ? ` | i18n: ${storeInfo.i18n.supportedLocales.join(", ")}` : "";
874
- const channelLabel = storeInfo.name !== storeInfo.storeName ? `"${storeInfo.name}" (channel in "${storeInfo.storeName}")` : `"${storeInfo.name}"`;
875
- prefetchSpinner.succeed(
876
- `Channel: ${channelLabel} | ${storeInfo.currency} | ${storeInfo.language}${i18nStatus}${envLabel}`
877
- );
878
- } catch (err) {
879
- prefetchSpinner.fail("Could not fetch store info");
880
- logger.warn(
881
- err instanceof Error ? err.message : "Using defaults. Make sure the connection ID is correct and the Brainerce API is reachable."
882
- );
883
- }
971
+ const resolved = await resolveStoreInfoOrExit(connectionId, candidateApiUrls);
972
+ storeInfo = resolved.info;
973
+ resolvedApiUrl = resolved.apiBaseUrl;
884
974
  }
885
975
  const slugify = (s) => s.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().trim().replace(/[^a-z0-9._-]+/g, "-").replace(/^[.-]+|[.-]+$/g, "").slice(0, 50);
886
976
  const channelSlug = storeInfo ? slugify(storeInfo.name) : "";
887
977
  const storeSlug = storeInfo ? slugify(storeInfo.storeName) : "";
888
- const projectNameDefault = projectName || channelSlug || storeSlug || void 0;
978
+ const projectNameSuggestion = channelSlug || storeSlug || void 0;
889
979
  const languageDefault = language || storeInfo?.language;
890
980
  if (!projectName || !connectionId || !language) {
891
981
  const answers = await runInteractive({
892
- projectName: projectNameDefault,
982
+ projectName,
983
+ projectNameSuggestion,
893
984
  connectionId,
894
985
  language: languageDefault,
895
986
  framework,
@@ -915,28 +1006,9 @@ program.name("create-brainerce-store").description("Scaffold a production-ready
915
1006
  process.exit(1);
916
1007
  }
917
1008
  if (!storeInfo) {
918
- const spinner = createSpinner("Fetching store info...");
919
- spinner.start();
920
- try {
921
- const resolved = await resolveStoreInfo(connectionId, candidateApiUrls);
922
- storeInfo = resolved.info;
923
- resolvedApiUrl = resolved.apiBaseUrl;
924
- const envLabel = resolved.apiBaseUrl === KNOWN_API_URLS.staging ? " [staging]" : resolved.apiBaseUrl === KNOWN_API_URLS.production ? "" : ` [${resolved.apiBaseUrl}]`;
925
- const i18nStatus = storeInfo.i18n?.enabled && storeInfo.i18n.supportedLocales.length > 1 ? ` | i18n: ${storeInfo.i18n.supportedLocales.join(", ")}` : "";
926
- const channelLabel = storeInfo.name !== storeInfo.storeName ? `"${storeInfo.name}" (channel in "${storeInfo.storeName}")` : `"${storeInfo.name}"`;
927
- spinner.succeed(
928
- `Channel: ${channelLabel} | ${storeInfo.currency} | ${storeInfo.language}${i18nStatus}${envLabel}`
929
- );
930
- } catch (err) {
931
- spinner.fail("Could not fetch store info");
932
- logger.error(
933
- err instanceof Error ? err.message : "Could not reach the Brainerce API to read this connection."
934
- );
935
- logger.info(
936
- "Re-run after verifying:\n \u2022 the connection ID matches a real sales channel in your dashboard\n \u2022 the machine running create-brainerce-store can reach the Brainerce API\n \u2022 if you are offline / behind a firewall, scaffold from a machine that can reach the API"
937
- );
938
- process.exit(1);
939
- }
1009
+ const resolved = await resolveStoreInfoOrExit(connectionId, candidateApiUrls);
1010
+ storeInfo = resolved.info;
1011
+ resolvedApiUrl = resolved.apiBaseUrl;
940
1012
  }
941
1013
  if (!pkgManager) {
942
1014
  pkgManager = detectInvokingPackageManager();
package/messages/en.json CHANGED
@@ -188,7 +188,8 @@
188
188
  "addBundleItem": "Add & Save",
189
189
  "addingBundle": "Adding...",
190
190
  "selectOptions": "Select options",
191
- "outOfStock": "Out of stock"
191
+ "outOfStock": "Out of stock",
192
+ "unavailableItemsHint": "Remove the items marked out of stock or unavailable to continue to checkout."
192
193
  },
193
194
  "checkout": {
194
195
  "pageTitle": "Checkout",
@@ -419,12 +420,18 @@
419
420
  "memberSince": "Member since",
420
421
  "orderPrefix": "Order",
421
422
  "productFallback": "Product",
423
+ "statusDraft": "Draft",
422
424
  "statusPending": "Pending",
423
425
  "statusProcessing": "Processing",
426
+ "statusOnHold": "On hold",
427
+ "statusPaid": "Paid",
424
428
  "statusShipped": "Shipped",
425
429
  "statusDelivered": "Delivered",
430
+ "statusCompleted": "Completed",
431
+ "statusFulfilled": "Fulfilled",
426
432
  "statusCancelled": "Cancelled",
427
433
  "statusRefunded": "Refunded",
434
+ "statusPartiallyRefunded": "Partially refunded",
428
435
  "editProfile": "Edit Profile",
429
436
  "firstName": "First Name",
430
437
  "lastName": "Last Name",
@@ -501,6 +508,9 @@
501
508
  },
502
509
  "reservation": {
503
510
  "expired": "Reservation expired. Items may no longer be available.",
511
+ "expiredHint": "We refreshed your cart. Check it before you continue.",
512
+ "expiredCheckout": "Your reserved stock was released, so payment is on hold. Go back to your cart and review it.",
513
+ "backToCart": "Back to cart",
504
514
  "hurry": "Hurry!",
505
515
  "reservedFor": "Items reserved for"
506
516
  },
package/messages/he.json CHANGED
@@ -188,7 +188,8 @@
188
188
  "addBundleItem": "הוסף וחסוך",
189
189
  "addingBundle": "מוסיף...",
190
190
  "selectOptions": "בחר אפשרויות",
191
- "outOfStock": "אזל מהמלאי"
191
+ "outOfStock": "אזל מהמלאי",
192
+ "unavailableItemsHint": "הסירו את המוצרים שמסומנים כאזלו מהמלאי או כלא זמינים כדי להמשיך לתשלום."
192
193
  },
193
194
  "checkout": {
194
195
  "pageTitle": "תשלום",
@@ -419,12 +420,18 @@
419
420
  "memberSince": "חבר מאז",
420
421
  "orderPrefix": "הזמנה",
421
422
  "productFallback": "מוצר",
423
+ "statusDraft": "טיוטה",
422
424
  "statusPending": "ממתין",
423
425
  "statusProcessing": "בטיפול",
426
+ "statusOnHold": "מושהה",
427
+ "statusPaid": "שולם",
424
428
  "statusShipped": "נשלח",
425
429
  "statusDelivered": "נמסר",
430
+ "statusCompleted": "הושלם",
431
+ "statusFulfilled": "סופק",
426
432
  "statusCancelled": "בוטל",
427
433
  "statusRefunded": "הוחזר",
434
+ "statusPartiallyRefunded": "הוחזר חלקית",
428
435
  "editProfile": "עריכת פרופיל",
429
436
  "firstName": "שם פרטי",
430
437
  "lastName": "שם משפחה",
@@ -501,6 +508,9 @@
501
508
  },
502
509
  "reservation": {
503
510
  "expired": "השריון פג. ייתכן שהמוצרים כבר לא זמינים.",
511
+ "expiredHint": "רעננו עבורכם את העגלה. כדאי לבדוק אותה לפני שממשיכים.",
512
+ "expiredCheckout": "השריון על המלאי שוחרר, ולכן התשלום מושהה. חזרו לעגלה ובדקו אותה.",
513
+ "backToCart": "חזרה לעגלה",
504
514
  "hurry": "מהרו!",
505
515
  "reservedFor": "המוצרים שמורים למשך"
506
516
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-brainerce-store",
3
- "version": "1.71.0",
3
+ "version": "1.72.0",
4
4
  "description": "Scaffold a production-ready e-commerce storefront connected to Brainerce",
5
5
  "bin": {
6
6
  "create-brainerce-store": "dist/index.js"
@@ -157,22 +157,29 @@ return {
157
157
 
158
158
  ## Promotional surfaces: bundles, bumps, discount banners
159
159
 
160
- The same overlay applies to every promotional surface you don't need locale-aware code:
160
+ The same overlay applies to every promotional surface, so you don't need locale-aware code:
161
161
 
162
162
  ```tsx
163
- // Cart bundles (cross-sell) locale-aware automatically:
163
+ // Cart bundles (cross-sell), locale-aware automatically:
164
164
  const cart = await client.getCart(cartId);
165
165
  cart.bundles[0].name; // "ארוחת צהריים" (bundle's own label)
166
166
  cart.bundles[0].offeredProducts[0].name; // "פיצה גבינה" (each offered product)
167
167
 
168
- // Order bumps at checkout:
169
- const { bumps } = await client.getOrderBumps(checkoutId);
168
+ // Order bumps at checkout. `getCheckoutBumps` takes a CHECKOUT id, not a cart id:
169
+ const { bumps } = await client.getCheckoutBumps(checkoutId);
170
170
  bumps[0].title; // translated bump headline (or merchant override)
171
171
  bumps[0].bumpProduct.name; // translated product name
172
172
 
173
- // Discount-rule banners (rendered from rule.name + rule.displayConfig):
174
- const rules = await client.getActiveDiscountRules();
175
- rules[0].name; // translated rule name
173
+ // Adding or removing a bump takes the CART id plus the bump config id
174
+ // (bumps[i].id). Pass a variantId when bumps[i].requiresVariantSelection:
175
+ await client.addOrderBump(cartId, bumps[0].id, selectedVariantId);
176
+ await client.removeOrderBump(cartId, bumps[0].id);
177
+
178
+ // Discount-rule banners. The API returns ready-to-render banner text, not the
179
+ // rule object: DiscountBanner is { ruleId, text, type }, so there is no `name`
180
+ // and no `displayConfig` to compose yourself.
181
+ const banners = await client.getDiscountBanners();
182
+ banners[0].text; // translated banner copy
176
183
  ```
177
184
 
178
185
  ## How merchants populate translations