create-brainerce-store 1.43.0 → 1.43.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +11 -8
- package/messages/en.json +1 -0
- package/messages/he.json +1 -0
- package/package.json +1 -1
- package/templates/nextjs/base/next.config.ts +68 -69
- package/templates/nextjs/base/scripts/fetch-store-info.mjs +98 -93
- package/templates/nextjs/base/src/app/checkout/page.tsx +1004 -982
- package/templates/nextjs/base/src/app/products/[slug]/page.tsx +118 -118
- package/templates/nextjs/base/src/components/account/order-history.tsx +368 -368
- package/templates/nextjs/base/src/components/cart/cart-bundle-offer.tsx +111 -111
- package/templates/nextjs/base/src/components/cart/cart-item.tsx +152 -152
- package/templates/nextjs/base/src/components/cart/cart-summary.tsx +108 -108
- package/templates/nextjs/base/src/components/cart/cart-upgrade-banner.tsx +141 -141
- package/templates/nextjs/base/src/components/cart/free-shipping-bar.tsx +62 -62
- package/templates/nextjs/base/src/components/checkout/order-bump-card.tsx +242 -242
- package/templates/nextjs/base/src/components/checkout/pickup-step.tsx +198 -198
- package/templates/nextjs/base/src/components/checkout/shipping-step.tsx +109 -109
- package/templates/nextjs/base/src/components/checkout/tax-display.tsx +74 -64
- package/templates/nextjs/base/src/components/products/frequently-bought-together.tsx +203 -203
- package/templates/nextjs/base/src/components/products/product-card.tsx +46 -1
- package/templates/nextjs/base/src/components/products/variant-selector.tsx +291 -291
- package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +125 -129
- package/templates/nextjs/base/src/components/shared/price-display.tsx +61 -61
- package/templates/nextjs/base/src/lib/resolve-currency.ts +1 -6
- package/templates/nextjs/base/src/lib/use-currency.ts +1 -6
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.43.
|
|
34
|
+
version: "1.43.2",
|
|
35
35
|
description: "Scaffold a production-ready e-commerce storefront connected to Brainerce",
|
|
36
36
|
bin: {
|
|
37
37
|
"create-brainerce-store": "dist/index.js"
|
|
@@ -172,12 +172,12 @@ var ALLOWED_PACKAGE_MANAGERS = [
|
|
|
172
172
|
"bun"
|
|
173
173
|
];
|
|
174
174
|
var BRAINERCE_RUNTIME_DEPS = Object.freeze({
|
|
175
|
-
// 1.
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
brainerce: "^1.
|
|
175
|
+
// 1.28 = first cut after the PriceList API removal + the FX-driven display
|
|
176
|
+
// pricing rework (Product.displayPrice / displayCurrency / displaySalePrice
|
|
177
|
+
// attached additively when getProducts is called with a regionId). Older
|
|
178
|
+
// 1.27 had PriceList/Resolved* surface that no longer exists on the
|
|
179
|
+
// backend, so scaffolded stores must pin >=1.28 to compile.
|
|
180
|
+
brainerce: "^1.28.0",
|
|
181
181
|
"isomorphic-dompurify": "^3.8.0"
|
|
182
182
|
});
|
|
183
183
|
|
|
@@ -555,7 +555,10 @@ async function fetchStoreInfo(connectionId, baseUrl = KNOWN_API_URLS.production)
|
|
|
555
555
|
const timeout = setTimeout(() => controller.abort(), 1e4);
|
|
556
556
|
let res;
|
|
557
557
|
try {
|
|
558
|
-
res = await fetch(url, {
|
|
558
|
+
res = await fetch(url, {
|
|
559
|
+
signal: controller.signal,
|
|
560
|
+
headers: { Origin: "http://localhost" }
|
|
561
|
+
});
|
|
559
562
|
} catch (err) {
|
|
560
563
|
if (err.name === "AbortError") {
|
|
561
564
|
throw new Error(`Request to ${baseUrl} timed out`);
|
package/messages/en.json
CHANGED
package/messages/he.json
CHANGED
package/package.json
CHANGED
|
@@ -1,69 +1,68 @@
|
|
|
1
|
-
import type { NextConfig } from 'next';
|
|
2
|
-
|
|
3
|
-
// Build-time invariant — fail loud if NEXT_PUBLIC_STORE_CURRENCY is missing
|
|
4
|
-
// for a production build. Next.js inlines NEXT_PUBLIC_* into the client
|
|
5
|
-
// bundle at `next build` time; once inlined, the value cannot be patched at
|
|
6
|
-
// deploy time. A missing env here is the root cause of non-USD stores ending
|
|
7
|
-
// up with `$5,096` baked into their HTML (and indexed by Googlebot).
|
|
8
|
-
//
|
|
9
|
-
// In dev (`next dev`) we only warn, so local-only experiments don't break.
|
|
10
|
-
if (!process.env.NEXT_PUBLIC_STORE_CURRENCY) {
|
|
11
|
-
const msg =
|
|
12
|
-
'NEXT_PUBLIC_STORE_CURRENCY is not set.\n' +
|
|
13
|
-
'Next.js inlines NEXT_PUBLIC_* vars into the client bundle at build time;\n' +
|
|
14
|
-
'a missing value will silently fall back to USD in storefront price displays\n' +
|
|
15
|
-
'and ship that to search-engine crawlers. Set it in .env.local (written by\n' +
|
|
16
|
-
'create-brainerce-store) or in your CI / hosting build env (Coolify / Vercel).';
|
|
17
|
-
if (process.env.NODE_ENV === 'production') {
|
|
18
|
-
throw new Error(`[next.config] ${msg}`);
|
|
19
|
-
} else {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
},
|
|
52
|
-
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
{ key: '
|
|
58
|
-
{
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
export default nextConfig;
|
|
1
|
+
import type { NextConfig } from 'next';
|
|
2
|
+
|
|
3
|
+
// Build-time invariant — fail loud if NEXT_PUBLIC_STORE_CURRENCY is missing
|
|
4
|
+
// for a production build. Next.js inlines NEXT_PUBLIC_* into the client
|
|
5
|
+
// bundle at `next build` time; once inlined, the value cannot be patched at
|
|
6
|
+
// deploy time. A missing env here is the root cause of non-USD stores ending
|
|
7
|
+
// up with `$5,096` baked into their HTML (and indexed by Googlebot).
|
|
8
|
+
//
|
|
9
|
+
// In dev (`next dev`) we only warn, so local-only experiments don't break.
|
|
10
|
+
if (!process.env.NEXT_PUBLIC_STORE_CURRENCY) {
|
|
11
|
+
const msg =
|
|
12
|
+
'NEXT_PUBLIC_STORE_CURRENCY is not set.\n' +
|
|
13
|
+
'Next.js inlines NEXT_PUBLIC_* vars into the client bundle at build time;\n' +
|
|
14
|
+
'a missing value will silently fall back to USD in storefront price displays\n' +
|
|
15
|
+
'and ship that to search-engine crawlers. Set it in .env.local (written by\n' +
|
|
16
|
+
'create-brainerce-store) or in your CI / hosting build env (Coolify / Vercel).';
|
|
17
|
+
if (process.env.NODE_ENV === 'production') {
|
|
18
|
+
throw new Error(`[next.config] ${msg}`);
|
|
19
|
+
} else {
|
|
20
|
+
console.warn(`[next.config] warning: ${msg}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const nextConfig: NextConfig = {
|
|
25
|
+
// isomorphic-dompurify ships jsdom, which at runtime reads stylesheet files
|
|
26
|
+
// from its own package directory. Webpack bundling breaks those relative
|
|
27
|
+
// lookups — loading it externally from node_modules keeps the paths intact.
|
|
28
|
+
serverExternalPackages: ['isomorphic-dompurify'],
|
|
29
|
+
images: {
|
|
30
|
+
// The storefront is a consumer of the Brainerce API — it has to render
|
|
31
|
+
// whatever image URLs the API returns. In practice those URLs can be on
|
|
32
|
+
// cdn.brainerce.com OR on an upstream merchant host (WooCommerce, Shopify,
|
|
33
|
+
// self-hosted) depending on whether the product's image-import job has
|
|
34
|
+
// completed on the backend. Rather than hard-fail on unknown hosts, skip
|
|
35
|
+
// the server-side optimizer entirely and let the browser fetch each image
|
|
36
|
+
// directly from origin. No server-side fetching → no SSRF or DoS surface
|
|
37
|
+
// on this Next server. Trade-off: no webp/resize/lazy optimization, so
|
|
38
|
+
// LCP is marginally worse. Acceptable; the storefront is not the right
|
|
39
|
+
// layer to enforce a hostname policy.
|
|
40
|
+
unoptimized: true,
|
|
41
|
+
},
|
|
42
|
+
async headers() {
|
|
43
|
+
return [
|
|
44
|
+
{
|
|
45
|
+
source: '/(.*)',
|
|
46
|
+
headers: [
|
|
47
|
+
{
|
|
48
|
+
key: 'Strict-Transport-Security',
|
|
49
|
+
value: 'max-age=63072000; includeSubDomains; preload',
|
|
50
|
+
},
|
|
51
|
+
{ key: 'X-Content-Type-Options', value: 'nosniff' },
|
|
52
|
+
// SAMEORIGIN (not DENY) so iframe-based payment providers (e.g. Cardcom)
|
|
53
|
+
// can redirect the iframe back to /payment-complete on the storefront
|
|
54
|
+
// itself after a successful charge — the postMessage relay needs the
|
|
55
|
+
// parent frame to be able to render our own same-origin page.
|
|
56
|
+
{ key: 'X-Frame-Options', value: 'SAMEORIGIN' },
|
|
57
|
+
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
|
|
58
|
+
{
|
|
59
|
+
key: 'Permissions-Policy',
|
|
60
|
+
value: 'camera=(), microphone=(), geolocation=(), interest-cohort=()',
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export default nextConfig;
|
|
@@ -1,93 +1,98 @@
|
|
|
1
|
-
/* global process, console, fetch */
|
|
2
|
-
/* eslint-disable no-console */
|
|
3
|
-
/**
|
|
4
|
-
* Setup script: fetches store info from Brainerce using the connection ID
|
|
5
|
-
* and saves NEXT_PUBLIC_STORE_NAME (and other public fields) to .env.local.
|
|
6
|
-
*
|
|
7
|
-
* Run: node scripts/fetch-store-info.mjs
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
11
|
-
import { join } from 'path';
|
|
12
|
-
|
|
13
|
-
const envPath = join(process.cwd(), '.env.local');
|
|
14
|
-
|
|
15
|
-
if (!existsSync(envPath)) {
|
|
16
|
-
console.error(
|
|
17
|
-
'❌ .env.local not found. Create it first with NEXT_PUBLIC_BRAINERCE_SALES_CHANNEL_ID set.'
|
|
18
|
-
);
|
|
19
|
-
process.exit(1);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const envContent = readFileSync(envPath, 'utf-8');
|
|
23
|
-
|
|
24
|
-
function getVar(content, key) {
|
|
25
|
-
const match = content.match(new RegExp(`^${key}=(.*)$`, 'm'));
|
|
26
|
-
return match ? match[1].trim() : null;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function setVar(content, key, value) {
|
|
30
|
-
const regex = new RegExp(`^${key}=.*$`, 'm');
|
|
31
|
-
if (regex.test(content)) {
|
|
32
|
-
return content.replace(regex, `${key}=${value}`);
|
|
33
|
-
}
|
|
34
|
-
return content.trimEnd() + `\n${key}=${value}\n`;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Read either env var name. The new one is preferred; the old one is a soft
|
|
38
|
-
// alias kept for backwards compatibility — the SDK accepts both.
|
|
39
|
-
const connectionId =
|
|
40
|
-
getVar(envContent, 'NEXT_PUBLIC_BRAINERCE_SALES_CHANNEL_ID') ||
|
|
41
|
-
getVar(envContent, 'NEXT_PUBLIC_BRAINERCE_CONNECTION_ID');
|
|
42
|
-
const apiUrl = (getVar(envContent, 'BRAINERCE_API_URL') || 'https://api.brainerce.com').replace(
|
|
43
|
-
/\/$/,
|
|
44
|
-
''
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
if (!connectionId) {
|
|
48
|
-
console.error(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
1
|
+
/* global process, console, fetch */
|
|
2
|
+
/* eslint-disable no-console */
|
|
3
|
+
/**
|
|
4
|
+
* Setup script: fetches store info from Brainerce using the connection ID
|
|
5
|
+
* and saves NEXT_PUBLIC_STORE_NAME (and other public fields) to .env.local.
|
|
6
|
+
*
|
|
7
|
+
* Run: node scripts/fetch-store-info.mjs
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
11
|
+
import { join } from 'path';
|
|
12
|
+
|
|
13
|
+
const envPath = join(process.cwd(), '.env.local');
|
|
14
|
+
|
|
15
|
+
if (!existsSync(envPath)) {
|
|
16
|
+
console.error(
|
|
17
|
+
'❌ .env.local not found. Create it first with NEXT_PUBLIC_BRAINERCE_SALES_CHANNEL_ID set.'
|
|
18
|
+
);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const envContent = readFileSync(envPath, 'utf-8');
|
|
23
|
+
|
|
24
|
+
function getVar(content, key) {
|
|
25
|
+
const match = content.match(new RegExp(`^${key}=(.*)$`, 'm'));
|
|
26
|
+
return match ? match[1].trim() : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function setVar(content, key, value) {
|
|
30
|
+
const regex = new RegExp(`^${key}=.*$`, 'm');
|
|
31
|
+
if (regex.test(content)) {
|
|
32
|
+
return content.replace(regex, `${key}=${value}`);
|
|
33
|
+
}
|
|
34
|
+
return content.trimEnd() + `\n${key}=${value}\n`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Read either env var name. The new one is preferred; the old one is a soft
|
|
38
|
+
// alias kept for backwards compatibility — the SDK accepts both.
|
|
39
|
+
const connectionId =
|
|
40
|
+
getVar(envContent, 'NEXT_PUBLIC_BRAINERCE_SALES_CHANNEL_ID') ||
|
|
41
|
+
getVar(envContent, 'NEXT_PUBLIC_BRAINERCE_CONNECTION_ID');
|
|
42
|
+
const apiUrl = (getVar(envContent, 'BRAINERCE_API_URL') || 'https://api.brainerce.com').replace(
|
|
43
|
+
/\/$/,
|
|
44
|
+
''
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
if (!connectionId) {
|
|
48
|
+
console.error('❌ NEXT_PUBLIC_BRAINERCE_SALES_CHANNEL_ID is not set in .env.local');
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
console.log(`Fetching store info for sales channel: ${connectionId} ...`);
|
|
53
|
+
|
|
54
|
+
let storeInfo;
|
|
55
|
+
try {
|
|
56
|
+
// /api/vc/* enforces an Origin check: TEST channels accept local/tunnel
|
|
57
|
+
// hosts, LIVE channels require the configured domain. Prefer the project's
|
|
58
|
+
// own NEXT_PUBLIC_SITE_URL (the real domain for a LIVE store), falling back
|
|
59
|
+
// to localhost for local dev against a TEST channel.
|
|
60
|
+
const siteUrl = getVar(envContent, 'NEXT_PUBLIC_SITE_URL') || 'http://localhost:3000';
|
|
61
|
+
const res = await fetch(`${apiUrl}/api/vc/${connectionId}/info`, {
|
|
62
|
+
headers: { Origin: siteUrl },
|
|
63
|
+
});
|
|
64
|
+
if (!res.ok) {
|
|
65
|
+
console.error(`❌ API returned ${res.status}: ${await res.text()}`);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
storeInfo = await res.json();
|
|
69
|
+
} catch (err) {
|
|
70
|
+
console.error(`❌ Failed to reach ${apiUrl}: ${err.message}`);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const name = storeInfo.name;
|
|
75
|
+
const currency = storeInfo.currency;
|
|
76
|
+
|
|
77
|
+
if (!name) {
|
|
78
|
+
console.error('❌ Store info response has no `name` field:', storeInfo);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
// Currency is NOT NULL in the backend schema — a response without it
|
|
82
|
+
// is a real backend bug, not a "use the default" signal. Fail loud rather
|
|
83
|
+
// than silently leaving the env stale (which would bake the previous
|
|
84
|
+
// value into the next client bundle build).
|
|
85
|
+
if (!currency) {
|
|
86
|
+
console.error('❌ Store info response has no `currency` field:', storeInfo);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let updated = envContent;
|
|
91
|
+
updated = setVar(updated, 'NEXT_PUBLIC_STORE_NAME', name);
|
|
92
|
+
updated = setVar(updated, 'NEXT_PUBLIC_STORE_CURRENCY', currency);
|
|
93
|
+
|
|
94
|
+
writeFileSync(envPath, updated, 'utf-8');
|
|
95
|
+
|
|
96
|
+
console.log(`✓ NEXT_PUBLIC_STORE_NAME=${name}`);
|
|
97
|
+
console.log(`✓ NEXT_PUBLIC_STORE_CURRENCY=${currency}`);
|
|
98
|
+
console.log('Done. Restart the dev server for changes to take effect.');
|