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 +31 -10
- package/dist/index.js +177 -105
- package/messages/en.json +11 -1
- package/messages/he.json +11 -1
- package/package.json +1 -1
- package/templates/nextjs/base/TRANSLATIONS.md +14 -7
- package/templates/nextjs/base/src/app/checkout/page.tsx +59 -3
- package/templates/nextjs/base/src/app/order-confirmation/page.tsx +21 -2
- package/templates/nextjs/base/src/components/account/order-history.tsx +25 -39
- package/templates/nextjs/base/src/components/account/order-status-timeline.tsx +30 -11
- package/templates/nextjs/base/src/core/hooks/use-cart-page.ts +71 -2
- package/templates/nextjs/base/src/core/lib/auth.ts +32 -39
- package/templates/nextjs/base/src/core/lib/brainerce.ts.ejs +5 -16
- package/templates/nextjs/base/src/core/providers/store-provider.tsx.ejs +3 -6
- package/templates/nextjs/base/src/ui/cart/cart-item.tsx +19 -1
- package/templates/nextjs/base/src/ui/cart/cart-view.tsx +42 -6
- package/templates/nextjs/base/src/ui/cart/reservation-countdown.tsx +52 -10
- package/templates/nextjs/designs/atelier/ui/cart/cart-drawer.tsx +21 -3
- package/templates/nextjs/designs/atelier/ui/cart/cart-item.tsx +19 -1
- package/templates/nextjs/designs/atelier/ui/cart/cart-view.tsx +44 -7
- package/templates/nextjs/designs/atelier/ui/cart/reservation-countdown.tsx +52 -10
- package/templates/nextjs/ui-canvas/cart/cart-item.tsx +15 -1
- package/templates/nextjs/ui-canvas/cart/cart-view.tsx +38 -4
- package/templates/nextjs/ui-canvas/cart/reservation-countdown.tsx +54 -11
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
|
|
48
|
-
|
|
|
49
|
-
| `--connection-id <id>` | Brainerce vibe-coded connection ID (`vc_*`)
|
|
50
|
-
| `--canvas`
|
|
51
|
-
| `--language <lang>`
|
|
52
|
-
| `--pkg-manager <pm>`
|
|
53
|
-
| `--framework <fw>`
|
|
54
|
-
| `--api-url <url>`
|
|
55
|
-
| `--no-git`
|
|
56
|
-
| `--no-install`
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
|
|
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
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
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
|
-
|
|
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:
|
|
677
|
+
headers: { Origin: origin }
|
|
643
678
|
});
|
|
644
679
|
} catch (err) {
|
|
645
680
|
if (err.name === "AbortError") {
|
|
646
|
-
throw new
|
|
681
|
+
throw new StoreInfoProbeError("OTHER", baseUrl, "request timed out after 10s");
|
|
647
682
|
}
|
|
648
|
-
throw new
|
|
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
|
|
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
|
|
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
|
|
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
|
|
670
|
-
|
|
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
|
-
|
|
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
|
|
752
|
+
const info = await probeStoreInfo(connectionId, baseUrl);
|
|
692
753
|
return { info, apiBaseUrl: baseUrl, fellBack: i > 0 };
|
|
693
754
|
} catch (err) {
|
|
694
|
-
|
|
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
|
-
|
|
702
|
-
|
|
703
|
-
|
|
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
|
-
|
|
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
|
|
867
|
-
|
|
868
|
-
|
|
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
|
|
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
|
|
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
|
|
919
|
-
|
|
920
|
-
|
|
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
|
@@ -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
|
|
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)
|
|
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.
|
|
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
|
-
//
|
|
174
|
-
|
|
175
|
-
|
|
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
|