create-brainerce-store 1.71.0 → 1.73.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.
Files changed (27) hide show
  1. package/README.md +31 -10
  2. package/dist/index.js +179 -107
  3. package/messages/en.json +14 -1
  4. package/messages/he.json +14 -1
  5. package/package.json +1 -1
  6. package/templates/nextjs/base/TRANSLATIONS.md +207 -200
  7. package/templates/nextjs/base/src/app/checkout/page.tsx +1074 -1018
  8. package/templates/nextjs/base/src/app/order-confirmation/page.tsx +21 -2
  9. package/templates/nextjs/base/src/components/account/order-history.tsx +371 -385
  10. package/templates/nextjs/base/src/components/account/order-status-timeline.tsx +85 -66
  11. package/templates/nextjs/base/src/core/hooks/use-cart-page.ts +127 -58
  12. package/templates/nextjs/base/src/core/lib/auth.ts +32 -39
  13. package/templates/nextjs/base/src/core/lib/brainerce.ts.ejs +5 -16
  14. package/templates/nextjs/base/src/core/providers/store-provider.tsx.ejs +3 -6
  15. package/templates/nextjs/base/src/ui/cart/cart-item.tsx +164 -146
  16. package/templates/nextjs/base/src/ui/cart/cart-view.tsx +176 -140
  17. package/templates/nextjs/base/src/ui/cart/reservation-countdown.tsx +137 -95
  18. package/templates/nextjs/base/src/ui/product/review-form.tsx +33 -11
  19. package/templates/nextjs/designs/atelier/ui/cart/cart-drawer.tsx +177 -163
  20. package/templates/nextjs/designs/atelier/ui/cart/cart-item.tsx +158 -140
  21. package/templates/nextjs/designs/atelier/ui/cart/cart-view.tsx +184 -147
  22. package/templates/nextjs/designs/atelier/ui/cart/reservation-countdown.tsx +131 -89
  23. package/templates/nextjs/designs/atelier/ui/product/review-form.tsx +30 -10
  24. package/templates/nextjs/ui-canvas/cart/cart-item.tsx +137 -123
  25. package/templates/nextjs/ui-canvas/cart/cart-view.tsx +140 -106
  26. package/templates/nextjs/ui-canvas/cart/reservation-countdown.tsx +124 -81
  27. package/templates/nextjs/ui-canvas/product/review-form.tsx +9 -1
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.73.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.1",
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,35 +968,21 @@ 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;
889
- const languageDefault = language || storeInfo?.language;
978
+ const projectNameSuggestion = channelSlug || storeSlug || void 0;
979
+ language = 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
- language: languageDefault,
985
+ language,
895
986
  framework,
896
987
  design,
897
988
  pkgManager,
@@ -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
@@ -154,6 +154,9 @@
154
154
  "submitReview": "Submit review",
155
155
  "addPhotos": "Add photos",
156
156
  "removePhoto": "Remove photo",
157
+ "choosePhotos": "Choose photos",
158
+ "photoFormats": "JPG, PNG, WebP or GIF, up to {mb}MB each.",
159
+ "photoLimitReached": "That is all {max} photos. Remove one to add another.",
157
160
  "photoUploading": "Uploading…",
158
161
  "photoTooLarge": "That photo is too large. Please pick a smaller one.",
159
162
  "photoUploadFailed": "Could not upload that photo. Please try another.",
@@ -188,7 +191,8 @@
188
191
  "addBundleItem": "Add & Save",
189
192
  "addingBundle": "Adding...",
190
193
  "selectOptions": "Select options",
191
- "outOfStock": "Out of stock"
194
+ "outOfStock": "Out of stock",
195
+ "unavailableItemsHint": "Remove the items marked out of stock or unavailable to continue to checkout."
192
196
  },
193
197
  "checkout": {
194
198
  "pageTitle": "Checkout",
@@ -419,12 +423,18 @@
419
423
  "memberSince": "Member since",
420
424
  "orderPrefix": "Order",
421
425
  "productFallback": "Product",
426
+ "statusDraft": "Draft",
422
427
  "statusPending": "Pending",
423
428
  "statusProcessing": "Processing",
429
+ "statusOnHold": "On hold",
430
+ "statusPaid": "Paid",
424
431
  "statusShipped": "Shipped",
425
432
  "statusDelivered": "Delivered",
433
+ "statusCompleted": "Completed",
434
+ "statusFulfilled": "Fulfilled",
426
435
  "statusCancelled": "Cancelled",
427
436
  "statusRefunded": "Refunded",
437
+ "statusPartiallyRefunded": "Partially refunded",
428
438
  "editProfile": "Edit Profile",
429
439
  "firstName": "First Name",
430
440
  "lastName": "Last Name",
@@ -501,6 +511,9 @@
501
511
  },
502
512
  "reservation": {
503
513
  "expired": "Reservation expired. Items may no longer be available.",
514
+ "expiredHint": "We refreshed your cart. Check it before you continue.",
515
+ "expiredCheckout": "Your reserved stock was released, so payment is on hold. Go back to your cart and review it.",
516
+ "backToCart": "Back to cart",
504
517
  "hurry": "Hurry!",
505
518
  "reservedFor": "Items reserved for"
506
519
  },
package/messages/he.json CHANGED
@@ -154,6 +154,9 @@
154
154
  "submitReview": "שליחת ביקורת",
155
155
  "addPhotos": "הוספת תמונות",
156
156
  "removePhoto": "הסרת תמונה",
157
+ "choosePhotos": "בחירת תמונות",
158
+ "photoFormats": "JPG, PNG, WebP או GIF, עד {mb}MB לכל תמונה.",
159
+ "photoLimitReached": "אלה כל {max} התמונות. הסר אחת כדי להוסיף אחרת.",
157
160
  "photoUploading": "מעלה...",
158
161
  "photoTooLarge": "התמונה הזו גדולה מדי. בחרו תמונה קטנה יותר.",
159
162
  "photoUploadFailed": "לא הצלחנו להעלות את התמונה. נסו תמונה אחרת.",
@@ -188,7 +191,8 @@
188
191
  "addBundleItem": "הוסף וחסוך",
189
192
  "addingBundle": "מוסיף...",
190
193
  "selectOptions": "בחר אפשרויות",
191
- "outOfStock": "אזל מהמלאי"
194
+ "outOfStock": "אזל מהמלאי",
195
+ "unavailableItemsHint": "הסירו את המוצרים שמסומנים כאזלו מהמלאי או כלא זמינים כדי להמשיך לתשלום."
192
196
  },
193
197
  "checkout": {
194
198
  "pageTitle": "תשלום",
@@ -419,12 +423,18 @@
419
423
  "memberSince": "חבר מאז",
420
424
  "orderPrefix": "הזמנה",
421
425
  "productFallback": "מוצר",
426
+ "statusDraft": "טיוטה",
422
427
  "statusPending": "ממתין",
423
428
  "statusProcessing": "בטיפול",
429
+ "statusOnHold": "מושהה",
430
+ "statusPaid": "שולם",
424
431
  "statusShipped": "נשלח",
425
432
  "statusDelivered": "נמסר",
433
+ "statusCompleted": "הושלם",
434
+ "statusFulfilled": "סופק",
426
435
  "statusCancelled": "בוטל",
427
436
  "statusRefunded": "הוחזר",
437
+ "statusPartiallyRefunded": "הוחזר חלקית",
428
438
  "editProfile": "עריכת פרופיל",
429
439
  "firstName": "שם פרטי",
430
440
  "lastName": "שם משפחה",
@@ -501,6 +511,9 @@
501
511
  },
502
512
  "reservation": {
503
513
  "expired": "השריון פג. ייתכן שהמוצרים כבר לא זמינים.",
514
+ "expiredHint": "רעננו עבורכם את העגלה. כדאי לבדוק אותה לפני שממשיכים.",
515
+ "expiredCheckout": "השריון על המלאי שוחרר, ולכן התשלום מושהה. חזרו לעגלה ובדקו אותה.",
516
+ "backToCart": "חזרה לעגלה",
504
517
  "hurry": "מהרו!",
505
518
  "reservedFor": "המוצרים שמורים למשך"
506
519
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-brainerce-store",
3
- "version": "1.71.0",
3
+ "version": "1.73.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"