create-brainerce-store 1.43.3 → 1.44.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/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.3",
34
+ version: "1.44.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"
@@ -172,12 +172,13 @@ var ALLOWED_PACKAGE_MANAGERS = [
172
172
  "bun"
173
173
  ];
174
174
  var BRAINERCE_RUNTIME_DEPS = Object.freeze({
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",
175
+ // 1.30 adds the storefront seam for geo-IP region resolution
176
+ // (getAutoRegion) + non-binding tax preview (estimateTax). 1.28 cut the
177
+ // PriceList API + introduced the FX display overlay
178
+ // (Product.displayPrice / displayCurrency / displaySalePrice attached
179
+ // additively when getProducts is called with a regionId). Scaffolded stores
180
+ // must pin >=1.30 to get the auto-region + tax-estimate methods.
181
+ brainerce: "^1.30.0",
181
182
  "isomorphic-dompurify": "^3.8.0"
182
183
  });
183
184
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-brainerce-store",
3
- "version": "1.43.3",
3
+ "version": "1.44.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"
@@ -1,15 +1,19 @@
1
- # Brainerce Sales Channel — preferred env var name.
2
- NEXT_PUBLIC_BRAINERCE_SALES_CHANNEL_ID=<%= connectionId %>
3
- # Legacy alias kept for backwards compatibility — both are accepted by the SDK.
4
- # @deprecated will be removed when SDK 2.0 ships.
5
- NEXT_PUBLIC_BRAINERCE_CONNECTION_ID=<%= connectionId %>
6
-
7
- # Store info (pre-fetched during setup to avoid flash on first load)
8
- NEXT_PUBLIC_STORE_NAME=<%- storeNameEnv %>
9
- NEXT_PUBLIC_STORE_CURRENCY=<%- currencyEnv %>
10
-
11
- # Backend API URL (server-side only — used by BFF proxy and SSR, never exposed to browser)
12
- BRAINERCE_API_URL=<%- apiBaseUrlEnv %>
13
-
14
- # Public site URL used for sitemap, robots.txt, and SEO metadata
15
- NEXT_PUBLIC_SITE_URL=http://localhost:3000
1
+ # Brainerce Sales Channel — preferred env var name.
2
+ NEXT_PUBLIC_BRAINERCE_SALES_CHANNEL_ID=<%= connectionId %>
3
+ # Legacy alias kept for backwards compatibility — both are accepted by the SDK.
4
+ # @deprecated will be removed when SDK 2.0 ships.
5
+ NEXT_PUBLIC_BRAINERCE_CONNECTION_ID=<%= connectionId %>
6
+
7
+ # Store info (pre-fetched during setup to avoid flash on first load)
8
+ NEXT_PUBLIC_STORE_NAME=<%- storeNameEnv %>
9
+ NEXT_PUBLIC_STORE_CURRENCY=<%- currencyEnv %>
10
+
11
+ # Backend API URL (server-side only — used by BFF proxy and SSR, never exposed to browser)
12
+ BRAINERCE_API_URL=<%- apiBaseUrlEnv %>
13
+
14
+ # Public API origin for the AI chat widget (public, unauthenticated endpoints only).
15
+ # The widget streams chat directly from the browser, so this one IS public.
16
+ NEXT_PUBLIC_BRAINERCE_API_URL=<%- apiBaseUrlEnv %>
17
+
18
+ # Public site URL — used for sitemap, robots.txt, and SEO metadata
19
+ NEXT_PUBLIC_SITE_URL=http://localhost:3000
@@ -1,195 +1,199 @@
1
- <% if (i18nEnabled) { %>
2
- import type { Metadata } from 'next';
3
- <%- fontImport %>
4
- import { StoreProvider } from '@/providers/store-provider';
5
- import { AnnouncementBar } from '@/components/content/announcement-bar';
6
- import { SiteHeader } from '@/components/content/site-header';
7
- import { SiteFooter } from '@/components/content/site-footer';
8
- import { getServerClient, fetchStoreInfo } from '@/lib/brainerce';
9
- import { getDirection, supportedLocales } from '@/i18n';
10
- import { getNonce } from '@/lib/nonce';
11
- import '../globals.css';
12
-
13
- <%- fontVariable %>
14
-
15
- const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com';
16
-
17
- export const metadata: Metadata = {
18
- metadataBase: new URL(baseUrl),
19
- title: {
20
- default: <%- storeNameJs %>,
21
- template: <%- titleTemplateJs %>,
22
- },
23
- description: <%- storeNameJs %>,
24
- alternates: {
25
- canonical: '/',
26
- },
27
- openGraph: {
28
- siteName: <%- storeNameJs %>,
29
- type: 'website',
30
- },
31
- robots: {
32
- index: true,
33
- follow: true,
34
- },
35
- };
36
-
37
- const organizationJsonLd = {
38
- '@context': 'https://schema.org',
39
- '@type': 'Organization',
40
- name: <%- storeNameJs %>,
41
- url: baseUrl,
42
- };
43
-
44
- export function generateStaticParams() {
45
- return supportedLocales.map((locale) => ({ locale }));
46
- }
47
-
48
- export default async function RootLayout({
49
- children,
50
- params,
51
- }: {
52
- children: React.ReactNode;
53
- params: Promise<{ locale: string }>;
54
- }) {
55
- const { locale } = await params;
56
- const dir = getDirection(locale);
57
- const nonce = await getNonce();
58
-
59
- // Merchant-driven layout chrome — fetched server-side. Each call falls back
60
- // to null/[] on 404 so the layout never crashes when the merchant hasn't
61
- // seeded a particular content type yet. New stores ship with default rows
62
- // seeded by the backend (StoresService.seedDefaultContent).
63
- const client = getServerClient(locale);
64
- const [announcements, siteHeader, siteFooter, storeInfo] = await Promise.all([
65
- client.content.announcement.list(locale).catch(() => []),
66
- client.content.header.get('main', locale).catch(() => null),
67
- client.content.footer.get('main', locale).catch(() => null),
68
- // SSR-fetch store config so PriceDisplay / FreeShippingBar / upsell UI
69
- // render with the real currency, feature flags, and i18n config at frame 0
70
- // without this, Googlebot sees USD/defaults baked into the HTML.
71
- fetchStoreInfo(locale),
72
- ]);
73
-
74
- return (
75
- <html lang={locale} dir={dir}>
76
- <head>
77
- <script
78
- type="application/ld+json"
79
- nonce={nonce}
80
- suppressHydrationWarning
81
- dangerouslySetInnerHTML={{
82
- __html: JSON.stringify(organizationJsonLd)
83
- .replace(/</g, '\\u003c')
84
- .replace(/>/g, '\\u003e')
85
- .replace(/&/g, '\\u0026'),
86
- }}
87
- />
88
- </head>
89
- <body className={font.className}>
90
- <StoreProvider locale={locale} initialStoreInfo={storeInfo}>
91
- <div className="min-h-screen flex flex-col">
92
- <AnnouncementBar announcements={announcements} />
93
- <SiteHeader header={siteHeader} storeName={<%- storeNameJs %>} />
94
- <main className="flex-1">{children}</main>
95
- <SiteFooter footer={siteFooter} storeName={<%- storeNameJs %>} />
96
- </div>
97
- </StoreProvider>
98
- </body>
99
- </html>
100
- );
101
- }
102
- <% } else { %>
103
- import type { Metadata } from 'next';
104
- <%- fontImport %>
105
- import { StoreProvider } from '@/providers/store-provider';
106
- import { AnnouncementBar } from '@/components/content/announcement-bar';
107
- import { SiteHeader } from '@/components/content/site-header';
108
- import { SiteFooter } from '@/components/content/site-footer';
109
- import { getServerClient, fetchStoreInfo } from '@/lib/brainerce';
110
- import { getNonce } from '@/lib/nonce';
111
- import './globals.css';
112
-
113
- <%- fontVariable %>
114
-
115
- const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com';
116
-
117
- export const metadata: Metadata = {
118
- metadataBase: new URL(baseUrl),
119
- title: {
120
- default: <%- storeNameJs %>,
121
- template: <%- titleTemplateJs %>,
122
- },
123
- description: <%- storeNameJs %>,
124
- alternates: {
125
- canonical: '/',
126
- },
127
- openGraph: {
128
- siteName: <%- storeNameJs %>,
129
- locale: '<%= ogLocale %>',
130
- type: 'website',
131
- },
132
- robots: {
133
- index: true,
134
- follow: true,
135
- },
136
- };
137
-
138
- const organizationJsonLd = {
139
- '@context': 'https://schema.org',
140
- '@type': 'Organization',
141
- name: <%- storeNameJs %>,
142
- url: baseUrl,
143
- };
144
-
145
- export default async function RootLayout({
146
- children,
147
- }: {
148
- children: React.ReactNode;
149
- }) {
150
- const nonce = await getNonce();
151
-
152
- // Merchant-driven layout chrome — fetched server-side. Each call falls back
153
- // to null/[] on 404 so the layout never crashes when the merchant hasn't
154
- // seeded a particular content type yet. New stores ship with default rows
155
- // seeded by the backend (StoresService.seedDefaultContent).
156
- const client = getServerClient();
157
- const [announcements, siteHeader, siteFooter, storeInfo] = await Promise.all([
158
- client.content.announcement.list().catch(() => []),
159
- client.content.header.get('main').catch(() => null),
160
- client.content.footer.get('main').catch(() => null),
161
- // SSR-fetch store config so PriceDisplay / FreeShippingBar / upsell UI
162
- // render with the real currency, feature flags, and i18n config at frame 0
163
- // without this, Googlebot sees USD/defaults baked into the HTML.
164
- fetchStoreInfo(),
165
- ]);
166
-
167
- return (
168
- <html lang="<%= language %>" dir="<%= direction %>">
169
- <head>
170
- <script
171
- type="application/ld+json"
172
- nonce={nonce}
173
- suppressHydrationWarning
174
- dangerouslySetInnerHTML={{
175
- __html: JSON.stringify(organizationJsonLd)
176
- .replace(/</g, '\\u003c')
177
- .replace(/>/g, '\\u003e')
178
- .replace(/&/g, '\\u0026'),
179
- }}
180
- />
181
- </head>
182
- <body className={font.className}>
183
- <StoreProvider initialStoreInfo={storeInfo}>
184
- <div className="min-h-screen flex flex-col">
185
- <AnnouncementBar announcements={announcements} />
186
- <SiteHeader header={siteHeader} storeName={<%- storeNameJs %>} />
187
- <main className="flex-1">{children}</main>
188
- <SiteFooter footer={siteFooter} storeName={<%- storeNameJs %>} />
189
- </div>
190
- </StoreProvider>
191
- </body>
192
- </html>
193
- );
194
- }
195
- <% } %>
1
+ <% if (i18nEnabled) { %>
2
+ import type { Metadata } from 'next';
3
+ <%- fontImport %>
4
+ import { StoreProvider } from '@/providers/store-provider';
5
+ import { AnnouncementBar } from '@/components/content/announcement-bar';
6
+ import { SiteHeader } from '@/components/content/site-header';
7
+ import { SiteFooter } from '@/components/content/site-footer';
8
+ import { BrainerceBotWidget } from '@/components/brainerce-bot';
9
+ import { getServerClient, fetchStoreInfo } from '@/lib/brainerce';
10
+ import { getDirection, supportedLocales } from '@/i18n';
11
+ import { getNonce } from '@/lib/nonce';
12
+ import '../globals.css';
13
+
14
+ <%- fontVariable %>
15
+
16
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com';
17
+
18
+ export const metadata: Metadata = {
19
+ metadataBase: new URL(baseUrl),
20
+ title: {
21
+ default: <%- storeNameJs %>,
22
+ template: <%- titleTemplateJs %>,
23
+ },
24
+ description: <%- storeNameJs %>,
25
+ alternates: {
26
+ canonical: '/',
27
+ },
28
+ openGraph: {
29
+ siteName: <%- storeNameJs %>,
30
+ type: 'website',
31
+ },
32
+ robots: {
33
+ index: true,
34
+ follow: true,
35
+ },
36
+ };
37
+
38
+ const organizationJsonLd = {
39
+ '@context': 'https://schema.org',
40
+ '@type': 'Organization',
41
+ name: <%- storeNameJs %>,
42
+ url: baseUrl,
43
+ };
44
+
45
+ export function generateStaticParams() {
46
+ return supportedLocales.map((locale) => ({ locale }));
47
+ }
48
+
49
+ export default async function RootLayout({
50
+ children,
51
+ params,
52
+ }: {
53
+ children: React.ReactNode;
54
+ params: Promise<{ locale: string }>;
55
+ }) {
56
+ const { locale } = await params;
57
+ const dir = getDirection(locale);
58
+ const nonce = await getNonce();
59
+
60
+ // Merchant-driven layout chrome fetched server-side. Each call falls back
61
+ // to null/[] on 404 so the layout never crashes when the merchant hasn't
62
+ // seeded a particular content type yet. New stores ship with default rows
63
+ // seeded by the backend (StoresService.seedDefaultContent).
64
+ const client = getServerClient(locale);
65
+ const [announcements, siteHeader, siteFooter, storeInfo] = await Promise.all([
66
+ client.content.announcement.list(locale).catch(() => []),
67
+ client.content.header.get('main', locale).catch(() => null),
68
+ client.content.footer.get('main', locale).catch(() => null),
69
+ // SSR-fetch store config so PriceDisplay / FreeShippingBar / upsell UI
70
+ // render with the real currency, feature flags, and i18n config at frame 0
71
+ // — without this, Googlebot sees USD/defaults baked into the HTML.
72
+ fetchStoreInfo(locale),
73
+ ]);
74
+
75
+ return (
76
+ <html lang={locale} dir={dir}>
77
+ <head>
78
+ <script
79
+ type="application/ld+json"
80
+ nonce={nonce}
81
+ suppressHydrationWarning
82
+ dangerouslySetInnerHTML={{
83
+ __html: JSON.stringify(organizationJsonLd)
84
+ .replace(/</g, '\\u003c')
85
+ .replace(/>/g, '\\u003e')
86
+ .replace(/&/g, '\\u0026'),
87
+ }}
88
+ />
89
+ </head>
90
+ <body className={font.className}>
91
+ <StoreProvider locale={locale} initialStoreInfo={storeInfo}>
92
+ <div className="min-h-screen flex flex-col">
93
+ <AnnouncementBar announcements={announcements} />
94
+ <SiteHeader header={siteHeader} storeName={<%- storeNameJs %>} />
95
+ <main className="flex-1">{children}</main>
96
+ <SiteFooter footer={siteFooter} storeName={<%- storeNameJs %>} />
97
+ </div>
98
+ <BrainerceBotWidget />
99
+ </StoreProvider>
100
+ </body>
101
+ </html>
102
+ );
103
+ }
104
+ <% } else { %>
105
+ import type { Metadata } from 'next';
106
+ <%- fontImport %>
107
+ import { StoreProvider } from '@/providers/store-provider';
108
+ import { AnnouncementBar } from '@/components/content/announcement-bar';
109
+ import { SiteHeader } from '@/components/content/site-header';
110
+ import { SiteFooter } from '@/components/content/site-footer';
111
+ import { BrainerceBotWidget } from '@/components/brainerce-bot';
112
+ import { getServerClient, fetchStoreInfo } from '@/lib/brainerce';
113
+ import { getNonce } from '@/lib/nonce';
114
+ import './globals.css';
115
+
116
+ <%- fontVariable %>
117
+
118
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com';
119
+
120
+ export const metadata: Metadata = {
121
+ metadataBase: new URL(baseUrl),
122
+ title: {
123
+ default: <%- storeNameJs %>,
124
+ template: <%- titleTemplateJs %>,
125
+ },
126
+ description: <%- storeNameJs %>,
127
+ alternates: {
128
+ canonical: '/',
129
+ },
130
+ openGraph: {
131
+ siteName: <%- storeNameJs %>,
132
+ locale: '<%= ogLocale %>',
133
+ type: 'website',
134
+ },
135
+ robots: {
136
+ index: true,
137
+ follow: true,
138
+ },
139
+ };
140
+
141
+ const organizationJsonLd = {
142
+ '@context': 'https://schema.org',
143
+ '@type': 'Organization',
144
+ name: <%- storeNameJs %>,
145
+ url: baseUrl,
146
+ };
147
+
148
+ export default async function RootLayout({
149
+ children,
150
+ }: {
151
+ children: React.ReactNode;
152
+ }) {
153
+ const nonce = await getNonce();
154
+
155
+ // Merchant-driven layout chrome fetched server-side. Each call falls back
156
+ // to null/[] on 404 so the layout never crashes when the merchant hasn't
157
+ // seeded a particular content type yet. New stores ship with default rows
158
+ // seeded by the backend (StoresService.seedDefaultContent).
159
+ const client = getServerClient();
160
+ const [announcements, siteHeader, siteFooter, storeInfo] = await Promise.all([
161
+ client.content.announcement.list().catch(() => []),
162
+ client.content.header.get('main').catch(() => null),
163
+ client.content.footer.get('main').catch(() => null),
164
+ // SSR-fetch store config so PriceDisplay / FreeShippingBar / upsell UI
165
+ // render with the real currency, feature flags, and i18n config at frame 0
166
+ // — without this, Googlebot sees USD/defaults baked into the HTML.
167
+ fetchStoreInfo(),
168
+ ]);
169
+
170
+ return (
171
+ <html lang="<%= language %>" dir="<%= direction %>">
172
+ <head>
173
+ <script
174
+ type="application/ld+json"
175
+ nonce={nonce}
176
+ suppressHydrationWarning
177
+ dangerouslySetInnerHTML={{
178
+ __html: JSON.stringify(organizationJsonLd)
179
+ .replace(/</g, '\\u003c')
180
+ .replace(/>/g, '\\u003e')
181
+ .replace(/&/g, '\\u0026'),
182
+ }}
183
+ />
184
+ </head>
185
+ <body className={font.className}>
186
+ <StoreProvider initialStoreInfo={storeInfo}>
187
+ <div className="min-h-screen flex flex-col">
188
+ <AnnouncementBar announcements={announcements} />
189
+ <SiteHeader header={siteHeader} storeName={<%- storeNameJs %>} />
190
+ <main className="flex-1">{children}</main>
191
+ <SiteFooter footer={siteFooter} storeName={<%- storeNameJs %>} />
192
+ </div>
193
+ <BrainerceBotWidget />
194
+ </StoreProvider>
195
+ </body>
196
+ </html>
197
+ );
198
+ }
199
+ <% } %>
@@ -0,0 +1,39 @@
1
+ 'use client';
2
+
3
+ import { useEffect } from 'react';
4
+
5
+ /**
6
+ * Mounts the Brainerce AI chat widget (the store's shopping assistant).
7
+ *
8
+ * Everything about the bot — name, avatar, colors, greeting, starter
9
+ * questions, capabilities, guardrails — is configured by the merchant in the
10
+ * Brainerce dashboard (Customers → Storefront Bot). This component renders
11
+ * nothing until the bot is switched Live there, so it is always safe to keep
12
+ * mounted.
13
+ */
14
+ export function BrainerceBotWidget() {
15
+ useEffect(() => {
16
+ const connectionId =
17
+ process.env.NEXT_PUBLIC_BRAINERCE_SALES_CHANNEL_ID ||
18
+ process.env.NEXT_PUBLIC_BRAINERCE_CONNECTION_ID;
19
+ if (!connectionId) return;
20
+
21
+ let bot: { destroy(): void } | null = null;
22
+ let cancelled = false;
23
+ void import('brainerce/bot').then(({ BrainerceBot }) =>
24
+ BrainerceBot.mount({
25
+ connectionId,
26
+ baseUrl: process.env.NEXT_PUBLIC_BRAINERCE_API_URL || undefined,
27
+ }).then((mounted) => {
28
+ if (cancelled) mounted?.destroy();
29
+ else bot = mounted;
30
+ }),
31
+ );
32
+ return () => {
33
+ cancelled = true;
34
+ bot?.destroy();
35
+ };
36
+ }, []);
37
+
38
+ return null;
39
+ }