create-brainerce-store 1.79.0 → 1.81.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 (34) hide show
  1. package/dist/index.js +168 -10
  2. package/messages/en.json +13 -4
  3. package/messages/he.json +13 -4
  4. package/package.json +10 -1
  5. package/templates/nextjs/base/.eslintrc.json +2 -0
  6. package/templates/nextjs/base/AI-GUIDE.md +32 -2
  7. package/templates/nextjs/base/package.json.ejs +53 -52
  8. package/templates/nextjs/base/scripts/connect.mjs +149 -0
  9. package/templates/nextjs/base/src/app/checkout/page.tsx +18 -0
  10. package/templates/nextjs/base/src/components/account/profile-section.tsx +7 -1
  11. package/templates/nextjs/base/src/components/auth/register-form.tsx +18 -1
  12. package/templates/nextjs/base/src/components/tracking-bootstrap.tsx +1 -1
  13. package/templates/nextjs/base/src/core/hooks/use-product-page.ts +22 -0
  14. package/templates/nextjs/base/src/core/lib/add-to-cart-error.ts +49 -0
  15. package/templates/nextjs/base/src/ui/cart/cart-bundle-offer.tsx +18 -0
  16. package/templates/nextjs/base/src/ui/cart/cart-item.tsx +49 -0
  17. package/templates/nextjs/base/src/ui/cart/cart-upgrade-banner.tsx +96 -2
  18. package/templates/nextjs/base/src/ui/layout/newsletter-signup.tsx +59 -12
  19. package/templates/nextjs/base/src/ui/product/frequently-bought-together.tsx +17 -0
  20. package/templates/nextjs/base/src/ui/product/product-card.tsx +17 -0
  21. package/templates/nextjs/base/src/ui/product/product-client-section.tsx +13 -0
  22. package/templates/nextjs/designs/atelier/ui/cart/cart-bundle-offer.tsx +18 -0
  23. package/templates/nextjs/designs/atelier/ui/cart/cart-item.tsx +49 -0
  24. package/templates/nextjs/designs/atelier/ui/cart/cart-upgrade-banner.tsx +101 -5
  25. package/templates/nextjs/designs/atelier/ui/product/frequently-bought-together.tsx +17 -0
  26. package/templates/nextjs/designs/atelier/ui/product/product-card.tsx +17 -0
  27. package/templates/nextjs/designs/atelier/ui/product/product-client-section.tsx +13 -0
  28. package/templates/nextjs/ui-canvas/cart/cart-bundle-offer.tsx +16 -0
  29. package/templates/nextjs/ui-canvas/cart/cart-item.tsx +45 -0
  30. package/templates/nextjs/ui-canvas/cart/cart-upgrade-banner.tsx +95 -2
  31. package/templates/nextjs/ui-canvas/layout/newsletter-signup.tsx +50 -8
  32. package/templates/nextjs/ui-canvas/product/frequently-bought-together.tsx +15 -0
  33. package/templates/nextjs/ui-canvas/product/product-card.tsx +15 -0
  34. package/templates/nextjs/ui-canvas/product/product-client-section.tsx +13 -0
package/dist/index.js CHANGED
@@ -31,8 +31,17 @@ var require_package = __commonJS({
31
31
  "package.json"(exports2, module2) {
32
32
  module2.exports = {
33
33
  name: "create-brainerce-store",
34
- version: "1.79.0",
34
+ version: "1.81.0",
35
35
  description: "Scaffold a production-ready e-commerce storefront connected to Brainerce",
36
+ homepage: "https://brainerce.com",
37
+ repository: {
38
+ type: "git",
39
+ url: "https://github.com/brainerce/brainerce.git",
40
+ directory: "packages/create-brainerce-store"
41
+ },
42
+ bugs: {
43
+ url: "https://github.com/brainerce/brainerce/issues"
44
+ },
36
45
  bin: {
37
46
  "create-brainerce-store": "dist/index.js"
38
47
  },
@@ -315,7 +324,13 @@ var BRAINERCE_RUNTIME_DEPS = Object.freeze({
315
324
  // off an SDK-typed `Product`, so on an older SDK a scaffolded store fails
316
325
  // type-check at creation. Verified against the published 2.4.0 tarball: zero
317
326
  // files mention either field.
318
- brainerce: "^2.5.0",
327
+ //
328
+ // 2.7.0 is the first SDK with `marketing.getBenefit()` and
329
+ // `PublicNewsletterBenefitOffer`, and the scaffolded newsletter signup reads
330
+ // both to show the merchant's welcome offer beside the field. On an older
331
+ // SDK a scaffolded store fails type-check at creation, exactly like the KIT
332
+ // floor above — so this floor is load-bearing, not cosmetic.
333
+ brainerce: "^2.7.0",
319
334
  "isomorphic-dompurify": "^3.8.0"
320
335
  });
321
336
 
@@ -899,6 +914,105 @@ var logger = {
899
914
  }
900
915
  };
901
916
 
917
+ // src/device-flow.ts
918
+ var import_node_child_process = require("child_process");
919
+ function isOpenableUrl(value, apiBaseUrl) {
920
+ let url;
921
+ try {
922
+ url = new URL(value);
923
+ } catch {
924
+ return false;
925
+ }
926
+ if (url.protocol !== "https:" && url.protocol !== "http:") return false;
927
+ const apiHost = (() => {
928
+ try {
929
+ return new URL(apiBaseUrl).hostname;
930
+ } catch {
931
+ return "";
932
+ }
933
+ })();
934
+ const base = apiHost.replace(/^api(-staging)?\./, "");
935
+ return url.hostname === base || url.hostname.endsWith(`.${base}`);
936
+ }
937
+ function openBrowser(url) {
938
+ const platform = process.platform;
939
+ const [cmd, args] = platform === "win32" ? ["cmd", ["/c", "start", "", url]] : platform === "darwin" ? ["open", [url]] : ["xdg-open", [url]];
940
+ try {
941
+ const child = (0, import_node_child_process.spawn)(cmd, args, { stdio: "ignore", detached: true });
942
+ child.on("error", () => {
943
+ });
944
+ child.unref();
945
+ return true;
946
+ } catch {
947
+ return false;
948
+ }
949
+ }
950
+ async function runDeviceFlow(apiBaseUrl, options = {}) {
951
+ let start;
952
+ try {
953
+ const res = await fetch(`${apiBaseUrl}/api/device-auth/start`, {
954
+ method: "POST",
955
+ headers: { "Content-Type": "application/json" },
956
+ body: JSON.stringify({
957
+ clientName: options.clientName || process.env.BRAINERCE_CLIENT_NAME || "create-brainerce-store"
958
+ })
959
+ });
960
+ if (!res.ok) {
961
+ logger.error(
962
+ `Could not start the browser approval (HTTP ${res.status}).
963
+ Create a sales channel in the dashboard instead, then re-run with --connection-id vc_xxx.`
964
+ );
965
+ return null;
966
+ }
967
+ start = await res.json();
968
+ } catch (error) {
969
+ logger.error(
970
+ `Could not reach Brainerce to start the approval: ${error instanceof Error ? error.message : String(error)}`
971
+ );
972
+ return null;
973
+ }
974
+ const opened = isOpenableUrl(start.verificationUriComplete, apiBaseUrl) && openBrowser(start.verificationUriComplete);
975
+ console.log();
976
+ logger.info(
977
+ opened ? " Opened your browser to approve this connection." : " Open this link to approve this connection:"
978
+ );
979
+ console.log(` ${start.verificationUriComplete}`);
980
+ console.log(` The page should show the code ${start.userCode} \u2014 check that it matches.`);
981
+ console.log();
982
+ logger.info(" Waiting for approval. A store and a sales channel are created if you have none.");
983
+ const deadline = Date.now() + start.expiresIn * 1e3;
984
+ let interval = (start.interval || 5) * 1e3;
985
+ while (Date.now() < deadline) {
986
+ await new Promise((resolve) => setTimeout(resolve, interval));
987
+ let result;
988
+ try {
989
+ const res = await fetch(`${apiBaseUrl}/api/device-auth/poll`, {
990
+ method: "POST",
991
+ headers: { "Content-Type": "application/json" },
992
+ body: JSON.stringify({ deviceCode: start.deviceCode })
993
+ });
994
+ result = await res.json();
995
+ } catch {
996
+ continue;
997
+ }
998
+ if (result.status === "approved" && result.connectionId) {
999
+ logger.success(" Approved.");
1000
+ return { connectionId: result.connectionId, storeId: result.storeId ?? null };
1001
+ }
1002
+ if (result.status === "denied") {
1003
+ logger.error(" The connection request was declined. Nothing was created.");
1004
+ return null;
1005
+ }
1006
+ if (result.status === "expired") {
1007
+ logger.error(" The code expired before it was approved. Run the command again.");
1008
+ return null;
1009
+ }
1010
+ if (result.status === "slow_down") interval += 1e3;
1011
+ }
1012
+ logger.error(" Timed out waiting for approval. Run the command again for a fresh code.");
1013
+ return null;
1014
+ }
1015
+
902
1016
  // src/utils/spinner.ts
903
1017
  var import_ora = __toESM(require("ora"));
904
1018
  function createSpinner(text) {
@@ -944,6 +1058,7 @@ async function checkForUpdate(name, current) {
944
1058
  return null;
945
1059
  }
946
1060
  }
1061
+ var DEFERRED_CONNECTION_ID = "vc_REPLACE_ME_RUN_npm_run_connect";
947
1062
  async function resolveStoreInfoOrExit(connectionId, candidateApiUrls) {
948
1063
  const spinner = createSpinner("Fetching store info...");
949
1064
  spinner.start();
@@ -974,6 +1089,9 @@ async function resolveStoreInfoOrExit(connectionId, candidateApiUrls) {
974
1089
  }
975
1090
  var program = new import_commander.Command();
976
1091
  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(
1092
+ "--defer-connection",
1093
+ "Scaffold now and connect later. Skips the browser approval and writes a placeholder id; finish with `npm run connect` inside the project."
1094
+ ).option(
977
1095
  "--api-url <url>",
978
1096
  "Brainerce API base URL (overrides BRAINERCE_API_URL env, defaults to https://api.brainerce.com)"
979
1097
  ).option("--language <lang>", "Store language (en, he)").addOption(new import_commander.Option("--framework <framework>", "Framework to use").default("nextjs").hideHelp()).addOption(new import_commander.Option("--design <design>", "Storefront design pack").hideHelp()).option("--pkg-manager <manager>", "Package manager (npm, pnpm, yarn, bun)").option(
@@ -1043,11 +1161,28 @@ program.name("create-brainerce-store").description("Scaffold a production-ready
1043
1161
  let storeInfo = null;
1044
1162
  let resolvedApiUrl = candidateApiUrls[0];
1045
1163
  if (connectionId) {
1046
- const connError2 = validateConnectionId(connectionId);
1047
- if (connError2) {
1048
- logger.error(connError2);
1164
+ const connError = validateConnectionId(connectionId);
1165
+ if (connError) {
1166
+ logger.error(connError);
1167
+ process.exit(1);
1168
+ }
1169
+ const resolved = await resolveStoreInfoOrExit(connectionId, candidateApiUrls);
1170
+ storeInfo = resolved.info;
1171
+ resolvedApiUrl = resolved.apiBaseUrl;
1172
+ } else if (options.deferConnection === true) {
1173
+ connectionId = DEFERRED_CONNECTION_ID;
1174
+ storeInfo = {
1175
+ name: projectName || "My Store",
1176
+ storeName: projectName || "My Store",
1177
+ currency: "USD",
1178
+ language: language || "en"
1179
+ };
1180
+ } else {
1181
+ const approved = await runDeviceFlow(candidateApiUrls[0]);
1182
+ if (!approved) {
1049
1183
  process.exit(1);
1050
1184
  }
1185
+ connectionId = approved.connectionId;
1051
1186
  const resolved = await resolveStoreInfoOrExit(connectionId, candidateApiUrls);
1052
1187
  storeInfo = resolved.info;
1053
1188
  resolvedApiUrl = resolved.apiBaseUrl;
@@ -1057,6 +1192,16 @@ program.name("create-brainerce-store").description("Scaffold a production-ready
1057
1192
  const storeSlug = storeInfo ? slugify(storeInfo.storeName) : "";
1058
1193
  const projectNameSuggestion = channelSlug || storeSlug || void 0;
1059
1194
  language = language || storeInfo?.language;
1195
+ if (!projectName && !process.stdin.isTTY) {
1196
+ if (!projectNameSuggestion) {
1197
+ logger.error(
1198
+ "No project name given, and no terminal to ask on. Pass one: npm create brainerce-store@latest my-store"
1199
+ );
1200
+ process.exit(1);
1201
+ }
1202
+ projectName = projectNameSuggestion;
1203
+ logger.info(` No project name given. Using "${projectName}", from your store.`);
1204
+ }
1060
1205
  if (!projectName || !connectionId || !language) {
1061
1206
  const answers = await runInteractive({
1062
1207
  projectName,
@@ -1080,12 +1225,14 @@ program.name("create-brainerce-store").description("Scaffold a production-ready
1080
1225
  logger.error(nameError);
1081
1226
  process.exit(1);
1082
1227
  }
1083
- const connError = validateConnectionId(connectionId);
1084
- if (connError) {
1085
- logger.error(connError);
1086
- process.exit(1);
1228
+ if (connectionId !== DEFERRED_CONNECTION_ID) {
1229
+ const connError = validateConnectionId(connectionId);
1230
+ if (connError) {
1231
+ logger.error(connError);
1232
+ process.exit(1);
1233
+ }
1087
1234
  }
1088
- if (!storeInfo) {
1235
+ if (!storeInfo && connectionId !== DEFERRED_CONNECTION_ID) {
1089
1236
  const resolved = await resolveStoreInfoOrExit(connectionId, candidateApiUrls);
1090
1237
  storeInfo = resolved.info;
1091
1238
  resolvedApiUrl = resolved.apiBaseUrl;
@@ -1181,7 +1328,18 @@ Your store will be running at http://localhost:3000
1181
1328
  if (!scaffoldInPlace) {
1182
1329
  logger.step(`cd ${projectName}`);
1183
1330
  }
1331
+ if (connectionId === DEFERRED_CONNECTION_ID) {
1332
+ logger.step(`${pkgManager}${pkgManager === "npm" ? " run" : ""} connect`);
1333
+ }
1184
1334
  logger.step(`${pkgManager}${pkgManager === "npm" ? " run" : ""} dev`);
1335
+ if (connectionId === DEFERRED_CONNECTION_ID) {
1336
+ logger.info(
1337
+ `
1338
+ This store is not connected yet. \`connect\` opens one browser approval,
1339
+ writes the sales channel id into .env.local, and creates a store and a
1340
+ channel for you if you have neither. Until then the catalog is empty.`
1341
+ );
1342
+ }
1185
1343
  logger.info(`
1186
1344
  Your store will be running at http://localhost:3000`);
1187
1345
  logger.info(
package/messages/en.json CHANGED
@@ -42,7 +42,7 @@
42
42
  "birthday": "Birthday",
43
43
  "birthdayMonth": "Month",
44
44
  "birthdayDay": "Day",
45
- "birthdayGiftNote": "Add your birthday and a gift arrives by email on the day.",
45
+ "birthdayGiftNote": "Add your birthday and a gift arrives by email on the day, as long as you are signed up for our emails.",
46
46
  "birthdayIncomplete": "Choose both a month and a day, or leave both empty.",
47
47
  "birthdayRequired": "Choose your birthday month and day to continue.",
48
48
  "birthdayPlaceholder": "Choose your birthday",
@@ -104,6 +104,8 @@
104
104
  "addToCart": "Add to Cart",
105
105
  "addingToCart": "Adding...",
106
106
  "addedToCart": "Added to Cart!",
107
+ "cartFull": "Your cart is full. Remove an item before adding this one.",
108
+ "addToCartFailed": "We could not add this to your cart. Please try again.",
107
109
  "outOfStock": "Out of Stock",
108
110
  "inStock": "In Stock",
109
111
  "unavailable": "Unavailable",
@@ -122,7 +124,7 @@
122
124
  "addSelectedToCart": "Add Selected to Cart",
123
125
  "totalPrice": "Total: {price}",
124
126
  "addingAll": "Adding...",
125
- "whatsInTheBox": "What's in the box",
127
+ "whatsInTheBox": "What's in the box",
126
128
  "by": "By"
127
129
  },
128
130
  "reviews": {
@@ -191,12 +193,17 @@
191
193
  "upgrade": "Upgrade",
192
194
  "upgrading": "Upgrading...",
193
195
  "dismissUpgrade": "Dismiss",
196
+ "upgradeOriginalNotRemoved": "We added the upgrade to your cart, but we could not remove the original item. Press Upgrade again to remove it.",
194
197
  "bundleOffers": "Bundle & Save",
195
198
  "addBundleItem": "Add & Save",
196
199
  "addingBundle": "Adding...",
197
200
  "selectOptions": "Select options",
198
201
  "outOfStock": "Out of stock",
199
- "unavailableItemsHint": "Remove the items marked out of stock or unavailable to continue to checkout."
202
+ "unavailableItemsHint": "Remove the items marked out of stock or unavailable to continue to checkout.",
203
+ "notEnoughStock": "There is not enough stock left for that quantity.",
204
+ "itemUnavailable": "This item can no longer be bought. Remove it to continue.",
205
+ "quantityUpdateFailed": "We could not update the quantity. Please try again.",
206
+ "removeFailed": "We could not remove this item. Please try again."
200
207
  },
201
208
  "checkout": {
202
209
  "pageTitle": "Checkout",
@@ -569,7 +576,9 @@
569
576
  "submitting": "Sending...",
570
577
  "checkEmailTitle": "Almost there — check your email",
571
578
  "checkEmailBody": "We've sent you a link to confirm. Look in your spam folder too; you won't get another copy for 24 hours.",
572
- "discountCode": "Your code: {code}",
579
+ "offerPercent": "{value}% off your first order",
580
+ "offerAmount": "{value} off your first order",
581
+ "offerByEmail": "Your personal code is in that email too. It is one per address.",
573
582
  "genericError": "Something went wrong. Please try again."
574
583
  },
575
584
  "loyalty": {
package/messages/he.json CHANGED
@@ -42,7 +42,7 @@
42
42
  "birthday": "יום הולדת",
43
43
  "birthdayMonth": "חודש",
44
44
  "birthdayDay": "יום",
45
- "birthdayGiftNote": "הוסיפו את יום ההולדת ותקבלו מתנה במייל ביום עצמו.",
45
+ "birthdayGiftNote": "הוסיפו את יום ההולדת ותקבלו מתנה במייל ביום עצמו, בתנאי שאתם רשומים לדיוור שלנו.",
46
46
  "birthdayIncomplete": "בחרו גם חודש וגם יום, או השאירו את שניהם ריקים.",
47
47
  "birthdayRequired": "בחרו את חודש ויום ההולדת כדי להמשיך.",
48
48
  "birthdayPlaceholder": "בחרו את יום ההולדת",
@@ -104,6 +104,8 @@
104
104
  "addToCart": "הוסף לעגלה",
105
105
  "addingToCart": "מוסיף...",
106
106
  "addedToCart": "נוסף לעגלה!",
107
+ "cartFull": "העגלה מלאה. הסירו מוצר אחד לפני הוספת המוצר הזה.",
108
+ "addToCartFailed": "לא הצלחנו להוסיף את המוצר לעגלה. נסו שוב.",
107
109
  "outOfStock": "אזל מהמלאי",
108
110
  "inStock": "במלאי",
109
111
  "unavailable": "לא זמין",
@@ -122,7 +124,7 @@
122
124
  "addSelectedToCart": "הוסף הנבחרים לעגלה",
123
125
  "totalPrice": "סה\"כ: {price}",
124
126
  "addingAll": "מוסיף...",
125
- "whatsInTheBox": "מה יש בקופסה",
127
+ "whatsInTheBox": "מה יש בקופסה",
126
128
  "by": "מאת"
127
129
  },
128
130
  "reviews": {
@@ -191,12 +193,17 @@
191
193
  "upgrade": "שדרג",
192
194
  "upgrading": "משדרג...",
193
195
  "dismissUpgrade": "סגור",
196
+ "upgradeOriginalNotRemoved": "הוספנו את השדרוג לעגלה, אבל לא הצלחנו להסיר את המוצר המקורי. לחצו שוב על שדרוג כדי להסיר אותו.",
194
197
  "bundleOffers": "חבילה וחיסכון",
195
198
  "addBundleItem": "הוסף וחסוך",
196
199
  "addingBundle": "מוסיף...",
197
200
  "selectOptions": "בחר אפשרויות",
198
201
  "outOfStock": "אזל מהמלאי",
199
- "unavailableItemsHint": "הסירו את המוצרים שמסומנים כאזלו מהמלאי או כלא זמינים כדי להמשיך לתשלום."
202
+ "unavailableItemsHint": "הסירו את המוצרים שמסומנים כאזלו מהמלאי או כלא זמינים כדי להמשיך לתשלום.",
203
+ "notEnoughStock": "אין מספיק מלאי לכמות הזו.",
204
+ "itemUnavailable": "לא ניתן לרכוש את המוצר הזה יותר. הסירו אותו כדי להמשיך.",
205
+ "quantityUpdateFailed": "לא הצלחנו לעדכן את הכמות. נסו שוב.",
206
+ "removeFailed": "לא הצלחנו להסיר את המוצר. נסו שוב."
200
207
  },
201
208
  "checkout": {
202
209
  "pageTitle": "תשלום",
@@ -569,7 +576,9 @@
569
576
  "submitting": "שולח...",
570
577
  "checkEmailTitle": "כמעט סיימנו — בדקו את המייל",
571
578
  "checkEmailBody": "שלחנו לכם קישור לאישור. בדקו גם בתיקיית הספאם; עותק נוסף לא יישלח ב-24 השעות הקרובות.",
572
- "discountCode": "הקוד שלכם: {code}",
579
+ "offerPercent": "{value}% הנחה על ההזמנה הראשונה",
580
+ "offerAmount": "{value} הנחה על ההזמנה הראשונה",
581
+ "offerByEmail": "הקוד האישי שלכם נמצא באותו מייל. הטבה אחת לכל כתובת.",
573
582
  "genericError": "משהו השתבש. נסו שוב."
574
583
  },
575
584
  "loyalty": {
package/package.json CHANGED
@@ -1,7 +1,16 @@
1
1
  {
2
2
  "name": "create-brainerce-store",
3
- "version": "1.79.0",
3
+ "version": "1.81.0",
4
4
  "description": "Scaffold a production-ready e-commerce storefront connected to Brainerce",
5
+ "homepage": "https://brainerce.com",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/brainerce/brainerce.git",
9
+ "directory": "packages/create-brainerce-store"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/brainerce/brainerce/issues"
13
+ },
5
14
  "bin": {
6
15
  "create-brainerce-store": "dist/index.js"
7
16
  },
@@ -17,6 +17,8 @@
17
17
  "!@/core/lib/capabilities",
18
18
  "!@/core/lib/display-price",
19
19
  "!@/core/lib/image-hosts",
20
+ "!@/core/lib/kit",
21
+ "!@/core/lib/add-to-cart-error",
20
22
  "!@/core/lib/navigation",
21
23
  "!@/core/lib/product-options",
22
24
  "!@/core/lib/sanitize",
@@ -59,8 +59,38 @@ UI components get *everything* from `@/core/hooks/*` and
59
59
  - `useHomeData()` → `{ products, banners, loading }`
60
60
  - `useProductListing()` → products + filters/sort/facets/pagination + handlers
61
61
  - `useProductPage(product)` → variant/image selection, `priceInfo`, `inventory`,
62
- `quantity`, `handleAddToCart`, `addingToCart`, `addedMessage`, customization
63
- + modifier state and errors
62
+ `quantity`, `handleAddToCart`, `addingToCart`, `addedMessage`, `addToCartError`,
63
+ customization + modifier state and errors
64
+ - `addToCartError` is `'CART_FULL' | 'FAILED' | null`. **Render it, and render it
65
+ outside any `modifierGroups.length > 0` block** — a cart holds at most 50
66
+ distinct products and the 51st add is refused, so a design that drops this
67
+ leaves the shopper tapping a button that resets and adds nothing. Copy lives
68
+ in `productDetail.cartFull` / `productDetail.addToCartFailed`.
69
+ - Five more components hold the same message in their OWN local state, because
70
+ they add to the cart without this hook: `ui/product/product-card.tsx`,
71
+ `ui/product/frequently-bought-together.tsx`, `ui/cart/cart-bundle-offer.tsx`
72
+ (all three: `addError`), `app/checkout/page.tsx` (`bumpError`) and
73
+ `ui/cart/cart-upgrade-banner.tsx` (`upgradeError`). All five use
74
+ `toAddToCartError()` from `@/core/lib/add-to-cart-error`. **Nothing checks that
75
+ you kept them** — `check-template-parity.js` compares `@/ui/...` imports and SDK
76
+ calls only, so dropping one of these leaves a green build and a silent button.
77
+ - `ui/cart/cart-item.tsx` carries the same idea for the two writes it owns,
78
+ in its own `lineError` state: a failed quantity change or a failed Remove
79
+ used to log to the console and leave the row looking untouched. It uses
80
+ `getErrorCode()` from the same file (not `toAddToCartError()` — the 50-line
81
+ cap cannot refuse a quantity change, and "we could not add this" would be a
82
+ lie about a removal) and four `cart.*` strings, mapped in `LINE_ERROR_KEYS`:
83
+ `notEnoughStock`, `itemUnavailable`, `quantityUpdateFailed`, `removeFailed`.
84
+ The first two are for refusals a retry can never clear, so do not collapse
85
+ them into the generic one. Nothing checks that you kept any of this either.
86
+ - `ui/cart/cart-upgrade-banner.tsx` additionally swaps one cart line for
87
+ another, and the ORDER of its two SDK calls is a correctness property, not a
88
+ style choice: it **adds the upgrade first and removes the original second**,
89
+ so a failed add leaves the cart untouched instead of losing the shopper's
90
+ line. It also carries `upgradeAdded`, which stops a retry adding the upgrade
91
+ twice, and a third message, `cart.upgradeOriginalNotRemoved`, for the case
92
+ where the add landed and the removal did not. If you rebuild that component,
93
+ keep all three; the reasoning is in the JSDoc on `handleUpgrade`.
64
94
  - `useCartPage()` / `useCart()` → `{ cart, itemCount, totals, refreshCart }`
65
95
 
66
96
  Hooks return data and handlers, never JSX. All catalog content (names, prices,
@@ -1,52 +1,53 @@
1
- {
2
- "name": "<%= projectName %>",
3
- "version": "0.1.0",
4
- "private": true,
5
- "scripts": {
6
- "dev": "next dev",
7
- "build": "next build",
8
- "start": "next start",
9
- "lint": "next lint",
10
- "setup": "node scripts/fetch-store-info.mjs"
11
- },
12
- "dependencies": {
13
- "brainerce": "<%= brainerceVersion %>",
14
- "next": "^15.3.4",
15
- "react": "^19.0.0",
16
- "react-dom": "^19.0.0",
17
- "@radix-ui/react-accordion": "^1.2.0",
18
- "@radix-ui/react-checkbox": "^1.1.1",
19
- "@radix-ui/react-dialog": "^1.1.2",
20
- "@radix-ui/react-label": "^2.1.0",
21
- "@radix-ui/react-radio-group": "^1.2.0",
22
- "@radix-ui/react-select": "^2.1.1",
23
- "@radix-ui/react-separator": "^1.1.0",
24
- "@radix-ui/react-slot": "^1.1.0",
25
- "@radix-ui/react-tabs": "^1.1.0",
26
- "@radix-ui/react-tooltip": "^1.1.4",
27
- "class-variance-authority": "^0.7.1",
28
- "clsx": "^2.1.1",
29
- "lucide-react": "^0.462.0",
30
- "tailwind-merge": "^2.5.2",
31
- "tailwindcss-animate": "^1.0.7",
32
- "isomorphic-dompurify": "<%= isomorphicDompurifyVersion %>",
33
- "sharp": "^0.35.3"
34
- },
35
- "devDependencies": {
36
- "@types/node": "^20.0.0",
37
- "@types/react": "^19.0.0",
38
- "@types/react-dom": "^19.0.0",
39
- "autoprefixer": "^10.4.0",
40
- "eslint": "^9.0.0",
41
- "eslint-config-next": "^15.3.4",
42
- "postcss": "^8.4.49",
43
- "tailwindcss": "^3.4.0",
44
- "typescript": "^5.4.0"
45
- },
46
- "engines": {
47
- "node": "^20.19.0 || ^22.13.0 || >=24.0.0"
48
- },
49
- "pnpm": {
50
- "onlyBuiltDependencies": ["sharp"]
51
- }
52
- }
1
+ {
2
+ "name": "<%= projectName %>",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "next lint",
10
+ "setup": "node scripts/fetch-store-info.mjs",
11
+ "connect": "node scripts/connect.mjs"
12
+ },
13
+ "dependencies": {
14
+ "brainerce": "<%= brainerceVersion %>",
15
+ "next": "^15.3.4",
16
+ "react": "^19.0.0",
17
+ "react-dom": "^19.0.0",
18
+ "@radix-ui/react-accordion": "^1.2.0",
19
+ "@radix-ui/react-checkbox": "^1.1.1",
20
+ "@radix-ui/react-dialog": "^1.1.2",
21
+ "@radix-ui/react-label": "^2.1.0",
22
+ "@radix-ui/react-radio-group": "^1.2.0",
23
+ "@radix-ui/react-select": "^2.1.1",
24
+ "@radix-ui/react-separator": "^1.1.0",
25
+ "@radix-ui/react-slot": "^1.1.0",
26
+ "@radix-ui/react-tabs": "^1.1.0",
27
+ "@radix-ui/react-tooltip": "^1.1.4",
28
+ "class-variance-authority": "^0.7.1",
29
+ "clsx": "^2.1.1",
30
+ "lucide-react": "^0.462.0",
31
+ "tailwind-merge": "^2.5.2",
32
+ "tailwindcss-animate": "^1.0.7",
33
+ "isomorphic-dompurify": "<%= isomorphicDompurifyVersion %>",
34
+ "sharp": "^0.35.3"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^20.0.0",
38
+ "@types/react": "^19.0.0",
39
+ "@types/react-dom": "^19.0.0",
40
+ "autoprefixer": "^10.4.0",
41
+ "eslint": "^9.0.0",
42
+ "eslint-config-next": "^15.3.4",
43
+ "postcss": "^8.4.49",
44
+ "tailwindcss": "^3.4.0",
45
+ "typescript": "^5.4.0"
46
+ },
47
+ "engines": {
48
+ "node": "^20.19.0 || ^22.13.0 || >=24.0.0"
49
+ },
50
+ "pnpm": {
51
+ "onlyBuiltDependencies": ["sharp"]
52
+ }
53
+ }