create-brainerce-store 1.72.0 → 1.74.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 (88) hide show
  1. package/dist/index.js +37 -4
  2. package/messages/en.json +30 -1
  3. package/messages/he.json +30 -1
  4. package/package.json +1 -1
  5. package/templates/nextjs/base/AGENTS.md.ejs +63 -16
  6. package/templates/nextjs/base/AI-GUIDE.md +17 -4
  7. package/templates/nextjs/base/CLAUDE.md.ejs +67 -20
  8. package/templates/nextjs/base/TRANSLATIONS.md +207 -207
  9. package/templates/nextjs/base/src/app/account/page.tsx +168 -122
  10. package/templates/nextjs/base/src/app/category/[slug]/page.tsx +10 -1
  11. package/templates/nextjs/base/src/app/checkout/page.tsx +1179 -1074
  12. package/templates/nextjs/base/src/app/layout.tsx.ejs +48 -4
  13. package/templates/nextjs/base/src/app/order-confirmation/page.tsx +98 -3
  14. package/templates/nextjs/base/src/app/products/[slug]/page.tsx +14 -2
  15. package/templates/nextjs/base/src/components/account/loyalty-panel.tsx +226 -0
  16. package/templates/nextjs/base/src/components/account/order-history.tsx +422 -371
  17. package/templates/nextjs/base/src/components/account/order-status-timeline.tsx +85 -85
  18. package/templates/nextjs/base/src/components/account/saved-payment-methods.tsx +104 -0
  19. package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +618 -612
  20. package/templates/nextjs/base/src/components/checkout/custom-fields-step.tsx +354 -306
  21. package/templates/nextjs/base/src/components/checkout/tax-display.tsx +141 -74
  22. package/templates/nextjs/base/src/core/hooks/use-cart-page.ts +218 -127
  23. package/templates/nextjs/base/src/core/hooks/use-home-data.ts +63 -51
  24. package/templates/nextjs/base/src/core/hooks/use-product-listing.ts +379 -295
  25. package/templates/nextjs/base/src/core/hooks/use-product-page.ts +33 -1
  26. package/templates/nextjs/base/src/core/lib/capabilities.ts +65 -0
  27. package/templates/nextjs/base/src/core/lib/display-price.ts +139 -0
  28. package/templates/nextjs/base/src/core/lib/region.server.ts +112 -0
  29. package/templates/nextjs/base/src/core/lib/region.ts +112 -0
  30. package/templates/nextjs/base/src/core/lib/store-info.ts +19 -0
  31. package/templates/nextjs/base/src/core/providers/store-provider.tsx.ejs +423 -243
  32. package/templates/nextjs/base/src/ui/cart/cart-item.tsx +174 -164
  33. package/templates/nextjs/base/src/ui/cart/cart-summary.tsx +109 -110
  34. package/templates/nextjs/base/src/ui/cart/cart-view.tsx +219 -176
  35. package/templates/nextjs/base/src/ui/cart/gift-card-input.tsx +191 -0
  36. package/templates/nextjs/base/src/ui/cart/reservation-countdown.tsx +137 -137
  37. package/templates/nextjs/base/src/ui/cart/tax-estimate-line.tsx +108 -0
  38. package/templates/nextjs/base/src/ui/home/home-client.tsx +27 -29
  39. package/templates/nextjs/base/src/ui/layout/header-search.tsx +249 -0
  40. package/templates/nextjs/base/src/ui/layout/region-switcher.tsx +59 -0
  41. package/templates/nextjs/base/src/ui/layout/site-header.tsx.ejs +171 -152
  42. package/templates/nextjs/base/src/ui/product/frequently-bought-together.tsx +197 -191
  43. package/templates/nextjs/base/src/ui/product/product-card.tsx +25 -52
  44. package/templates/nextjs/base/src/ui/product/product-client-section.tsx +12 -3
  45. package/templates/nextjs/base/src/ui/product/product-listing.tsx +460 -446
  46. package/templates/nextjs/base/src/ui/product/recommendation-section.tsx +108 -102
  47. package/templates/nextjs/base/src/ui/product/review-form.tsx +33 -11
  48. package/templates/nextjs/base/src/ui/product/stock-badge.tsx +15 -3
  49. package/templates/nextjs/designs/atelier/app-overlay/layout.tsx.ejs +48 -4
  50. package/templates/nextjs/designs/atelier/messages-patch/en.json +21 -25
  51. package/templates/nextjs/designs/atelier/messages-patch/he.json +21 -25
  52. package/templates/nextjs/designs/atelier/ui/cart/cart-drawer.tsx +177 -181
  53. package/templates/nextjs/designs/atelier/ui/cart/cart-item.tsx +168 -158
  54. package/templates/nextjs/designs/atelier/ui/cart/cart-summary.tsx +106 -107
  55. package/templates/nextjs/designs/atelier/ui/cart/cart-view.tsx +229 -184
  56. package/templates/nextjs/designs/atelier/ui/cart/reservation-countdown.tsx +131 -131
  57. package/templates/nextjs/designs/atelier/ui/cart/tax-estimate-line.tsx +108 -0
  58. package/templates/nextjs/designs/atelier/ui/home/benefits-band.tsx +10 -1
  59. package/templates/nextjs/designs/atelier/ui/home/hero-section.tsx +178 -174
  60. package/templates/nextjs/designs/atelier/ui/home/home-client.tsx +4 -6
  61. package/templates/nextjs/designs/atelier/ui/home/newsletter-section.tsx +150 -78
  62. package/templates/nextjs/designs/atelier/ui/home/testimonials-band.tsx +79 -46
  63. package/templates/nextjs/designs/atelier/ui/layout/header-search.tsx +264 -0
  64. package/templates/nextjs/designs/atelier/ui/layout/region-switcher.tsx +44 -0
  65. package/templates/nextjs/designs/atelier/ui/layout/site-header.tsx.ejs +5 -0
  66. package/templates/nextjs/designs/atelier/ui/product/frequently-bought-together.tsx +202 -196
  67. package/templates/nextjs/designs/atelier/ui/product/product-card.tsx +27 -52
  68. package/templates/nextjs/designs/atelier/ui/product/product-client-section.tsx +12 -3
  69. package/templates/nextjs/designs/atelier/ui/product/product-listing.tsx +332 -260
  70. package/templates/nextjs/designs/atelier/ui/product/recommendation-section.tsx +101 -95
  71. package/templates/nextjs/designs/atelier/ui/product/review-form.tsx +30 -10
  72. package/templates/nextjs/designs/atelier/ui/product/stock-badge.tsx +17 -2
  73. package/templates/nextjs/designs/atelier/ui/shared/icons.tsx +9 -0
  74. package/templates/nextjs/ui-canvas/cart/cart-item.tsx +137 -137
  75. package/templates/nextjs/ui-canvas/cart/cart-summary.tsx +98 -99
  76. package/templates/nextjs/ui-canvas/cart/cart-view.tsx +140 -140
  77. package/templates/nextjs/ui-canvas/cart/gift-card-input.tsx +142 -0
  78. package/templates/nextjs/ui-canvas/cart/reservation-countdown.tsx +124 -124
  79. package/templates/nextjs/ui-canvas/cart/tax-estimate-line.tsx +104 -0
  80. package/templates/nextjs/ui-canvas/home/home-client.tsx +24 -26
  81. package/templates/nextjs/ui-canvas/layout/region-switcher.tsx +51 -0
  82. package/templates/nextjs/ui-canvas/layout/site-header.tsx.ejs +3 -0
  83. package/templates/nextjs/ui-canvas/product/frequently-bought-together.tsx +174 -168
  84. package/templates/nextjs/ui-canvas/product/product-card.tsx +9 -51
  85. package/templates/nextjs/ui-canvas/product/product-client-section.tsx +5 -3
  86. package/templates/nextjs/ui-canvas/product/recommendation-section.tsx +105 -99
  87. package/templates/nextjs/ui-canvas/product/review-form.tsx +9 -1
  88. package/templates/nextjs/ui-canvas/product/stock-badge.tsx +17 -2
@@ -1,243 +1,423 @@
1
- 'use client';
2
-
3
- import React, { createContext, useContext, useEffect, useState, useCallback } from 'react';
4
- import type { Cart, CustomerProfile } from 'brainerce';
5
- import { getCartTotals } from 'brainerce';
6
- import { getClient, initClient } from '@/core/lib/brainerce';
7
- import { pickPublicStoreInfo, type PublicStoreInfo } from '@/core/lib/store-info';
8
- import { checkAuthStatus, proxyLogout } from '@/core/lib/auth';
9
- <% if (i18nEnabled) { %>
10
- import { MessagesContext } from '@/core/lib/translations';
11
- import { getMessages, defaultLocale } from '@/i18n';
12
- <% } %>
13
-
14
- // ---- Store Info Context ----
15
- interface StoreInfoContextValue {
16
- storeInfo: PublicStoreInfo | null;
17
- loading: boolean;
18
- }
19
-
20
- const StoreInfoContext = createContext<StoreInfoContextValue>({
21
- storeInfo: null,
22
- loading: true,
23
- });
24
-
25
- export function useStoreInfo() {
26
- return useContext(StoreInfoContext);
27
- }
28
-
29
- // ---- Auth Context ----
30
- interface AuthContextValue {
31
- isLoggedIn: boolean;
32
- authLoading: boolean;
33
- customer: CustomerProfile | null;
34
- login: () => Promise<void>;
35
- logout: () => Promise<void>;
36
- }
37
-
38
- const AuthContext = createContext<AuthContextValue>({
39
- isLoggedIn: false,
40
- authLoading: true,
41
- customer: null,
42
- login: async () => {},
43
- logout: async () => {},
44
- });
45
-
46
- export function useAuth() {
47
- return useContext(AuthContext);
48
- }
49
-
50
- // ---- Cart Context ----
51
- interface CartContextValue {
52
- cart: Cart | null;
53
- cartLoading: boolean;
54
- refreshCart: () => Promise<void>;
55
- itemCount: number;
56
- totals: { subtotal: number; discount: number; shipping: number; total: number };
57
- }
58
-
59
- const CartContext = createContext<CartContextValue>({
60
- cart: null,
61
- cartLoading: true,
62
- refreshCart: async () => {},
63
- itemCount: 0,
64
- totals: { subtotal: 0, discount: 0, shipping: 0, total: 0 },
65
- });
66
-
67
- export function useCart() {
68
- return useContext(CartContext);
69
- }
70
-
71
- // ---- Provider Component ----
72
- <% if (i18nEnabled) { %>
73
- export function StoreProvider({
74
- children,
75
- locale,
76
- initialStoreInfo = null,
77
- initialMessages,
78
- }: {
79
- children: React.ReactNode;
80
- locale?: string;
81
- initialStoreInfo?: PublicStoreInfo | null;
82
- initialMessages?: Record<string, Record<string, string>>;
83
- }) {
84
- <% } else { %>
85
- export function StoreProvider({
86
- children,
87
- initialStoreInfo = null,
88
- }: {
89
- children: React.ReactNode;
90
- initialStoreInfo?: PublicStoreInfo | null;
91
- }) {
92
- <% } %>
93
- // Seed from server-rendered data when available so SSR consumers (PriceDisplay,
94
- // FreeShippingBar, locale-aware UI, etc.) render with real values from frame 0
95
- // instead of falling back to USD/defaults until hydration completes.
96
- const [storeInfo, setStoreInfo] = useState<PublicStoreInfo | null>(initialStoreInfo);
97
- const [storeLoading, setStoreLoading] = useState(!initialStoreInfo);
98
- const [isLoggedIn, setIsLoggedIn] = useState(false);
99
- const [customer, setCustomer] = useState<CustomerProfile | null>(null);
100
- const [authLoading, setAuthLoading] = useState(true);
101
- const [cart, setCart] = useState<Cart | null>(null);
102
- const [cartLoading, setCartLoading] = useState(true);
103
- <% if (i18nEnabled) { %>
104
- // Seed from server-rendered messages (see RootLayout) so translated text
105
- // renders at frame 0 instead of a window of raw "namespace.key" strings
106
- // before the client-only fetch in the effect below resolves — this also
107
- // closes the same gap on every locale switch, since RootLayout re-runs
108
- // server-side per locale and StoreProvider remounts with fresh initial data.
109
- const [messages, setMessages] = useState<Record<string, Record<string, string>>>(
110
- initialMessages ?? {}
111
- );
112
- <% } %>
113
-
114
- // Check auth status via httpOnly cookie (server-side validation)
115
- const refreshAuth = useCallback(async () => {
116
- try {
117
- const status = await checkAuthStatus();
118
- setIsLoggedIn(status.isLoggedIn);
119
- setCustomer(status.isLoggedIn ? (status.customer as CustomerProfile) : null);
120
- } catch {
121
- setIsLoggedIn(false);
122
- setCustomer(null);
123
- } finally {
124
- setAuthLoading(false);
125
- }
126
- }, []);
127
-
128
- // Initialize client, check auth, and fetch store info
129
- useEffect(() => {
130
- const client = initClient();
131
- <% if (i18nEnabled) { %>
132
-
133
- // Set locale on SDK client for translated product content
134
- if (locale) {
135
- client.setLocale(locale);
136
- }
137
-
138
- // Load UI message strings for current locale
139
- getMessages(locale || defaultLocale).then(setMessages);
140
- <% } %>
141
-
142
- // Optimistic check: if brainerce_logged_in cookie exists, assume logged in
143
- // while we validate the actual token server-side
144
- if (typeof document !== 'undefined' && document.cookie.includes('brainerce_logged_in=1')) {
145
- setIsLoggedIn(true);
146
- }
147
-
148
- // Validate auth token server-side
149
- refreshAuth();
150
-
151
- // Fetch store info only when the server didn't already provide it. The
152
- // happy path is that <RootLayout> SSR'd fetchStoreInfo() and seeded us via
153
- // initialStoreInfo; this branch is the failure fallback (server fetch
154
- // returned null) so user-facing components don't render forever as 'USD'.
155
- if (!initialStoreInfo) {
156
- client
157
- .getStoreInfo()
158
- .then((raw) => setStoreInfo(pickPublicStoreInfo(raw)))
159
- .catch(console.error)
160
- .finally(() => setStoreLoading(false));
161
- }
162
- <% if (i18nEnabled) { %>
163
- }, [refreshAuth, locale, initialStoreInfo]);
164
- <% } else { %>
165
- }, [refreshAuth, initialStoreInfo]);
166
- <% } %>
167
-
168
- // Cart management
169
- const refreshCart = useCallback(async () => {
170
- try {
171
- setCartLoading(true);
172
- const client = getClient();
173
- // The SDK owns cart identity: smartGetCart() picks the guest, session or
174
- // customer cart on its own. Never mirror the id into localStorage.
175
- const c = await client.smartGetCart();
176
- setCart(c);
177
- } catch (err) {
178
- console.error('Failed to load cart:', err);
179
- } finally {
180
- setCartLoading(false);
181
- }
182
- }, []);
183
-
184
- useEffect(() => {
185
- refreshCart();
186
- }, [refreshCart, isLoggedIn]);
187
-
188
- // Called after successful login (cookie already set by proxy)
189
- const login = useCallback(async () => {
190
- // Refresh auth state from server (reads httpOnly cookie)
191
- await refreshAuth();
192
-
193
- // Merge guest session cart into customer cart
194
- const client = getClient();
195
- client.syncCartOnLogin().catch(console.error);
196
- }, [refreshAuth]);
197
-
198
- const logout = useCallback(async () => {
199
- // Clear httpOnly cookie server-side
200
- await proxyLogout();
201
-
202
- const client = getClient();
203
- client.onLogout();
204
- setIsLoggedIn(false);
205
- setCustomer(null);
206
- setCart(null);
207
- refreshCart();
208
- }, [refreshCart]);
209
-
210
- const itemCount = cart
211
- ? cart.items.reduce((sum, item) => sum + item.quantity, 0)
212
- : 0;
213
-
214
- const totals = cart ? getCartTotals(cart) : { subtotal: 0, discount: 0, shipping: 0, total: 0 };
215
-
216
- <% if (i18nEnabled) { %>
217
- return (
218
- <MessagesContext.Provider value={messages}>
219
- <StoreInfoContext.Provider value={{ storeInfo, loading: storeLoading }}>
220
- <AuthContext.Provider value={{ isLoggedIn, authLoading, customer, login, logout }}>
221
- <CartContext.Provider
222
- value={{ cart, cartLoading, refreshCart, itemCount, totals }}
223
- >
224
- {children}
225
- </CartContext.Provider>
226
- </AuthContext.Provider>
227
- </StoreInfoContext.Provider>
228
- </MessagesContext.Provider>
229
- );
230
- <% } else { %>
231
- return (
232
- <StoreInfoContext.Provider value={{ storeInfo, loading: storeLoading }}>
233
- <AuthContext.Provider value={{ isLoggedIn, authLoading, customer, login, logout }}>
234
- <CartContext.Provider
235
- value={{ cart, cartLoading, refreshCart, itemCount, totals }}
236
- >
237
- {children}
238
- </CartContext.Provider>
239
- </AuthContext.Provider>
240
- </StoreInfoContext.Provider>
241
- );
242
- <% } %>
243
- }
1
+ 'use client';
2
+
3
+ import React, { createContext, useContext, useEffect, useState, useCallback } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+ import type { Cart, CustomerProfile, PublicRegion } from 'brainerce';
6
+ import { getCartTotals } from 'brainerce';
7
+ import { getClient, initClient } from '@/core/lib/brainerce';
8
+ import { pickRegion, readRegionCookie, writeRegionCookie } from '@/core/lib/region';
9
+ import { pickPublicStoreInfo, type PublicStoreInfo } from '@/core/lib/store-info';
10
+ import {
11
+ pickPublicCapabilities,
12
+ type PublicStoreCapabilities,
13
+ } from '@/core/lib/capabilities';
14
+ import { checkAuthStatus, proxyLogout } from '@/core/lib/auth';
15
+ <% if (i18nEnabled) { %>
16
+ import { MessagesContext } from '@/core/lib/translations';
17
+ import { getMessages, defaultLocale } from '@/i18n';
18
+ <% } %>
19
+
20
+ // ---- Store Info Context ----
21
+ interface StoreInfoContextValue {
22
+ storeInfo: PublicStoreInfo | null;
23
+ loading: boolean;
24
+ }
25
+
26
+ const StoreInfoContext = createContext<StoreInfoContextValue>({
27
+ storeInfo: null,
28
+ loading: true,
29
+ });
30
+
31
+ export function useStoreInfo() {
32
+ return useContext(StoreInfoContext);
33
+ }
34
+
35
+ // ---- Store Capabilities Context ----
36
+ // What the merchant configured on this sales channel: low-stock warning and
37
+ // threshold, back-in-stock alerts, reservation timeout, and which optional
38
+ // features are switched on. Fetched ONCE for the whole app — it is per sales
39
+ // channel, not per product, so a page-level fetch would be repeated work.
40
+ interface StoreCapabilitiesContextValue {
41
+ capabilities: PublicStoreCapabilities | null;
42
+ capabilitiesLoading: boolean;
43
+ }
44
+
45
+ // The default is a real value, not a throw: a component rendered during SSR or
46
+ // outside <StoreProvider> reads `null` and falls back to its own default rather
47
+ // than crashing the tree.
48
+ const StoreCapabilitiesContext = createContext<StoreCapabilitiesContextValue>({
49
+ capabilities: null,
50
+ capabilitiesLoading: true,
51
+ });
52
+
53
+ export function useStoreCapabilities() {
54
+ return useContext(StoreCapabilitiesContext);
55
+ }
56
+
57
+ // ---- Region Context ----
58
+ // Which region prices this session. Seeded from the server (see RootLayout,
59
+ // which calls resolveRegion()) so SSR and the client agree from frame 0
60
+ // instead of the client flipping the currency after hydration.
61
+ //
62
+ // A single-region store is a NO-OP by design. `regions.length <= 1` means
63
+ // the switcher renders nothing and `regionId` is the default the backend
64
+ // would have picked anyway. Nothing about this is conditional on a capability
65
+ // flag: there ISN'T one for regions, and a storefront that skips `regionId`
66
+ // does not hide a feature, it shows every shopper the default region's prices.
67
+ interface RegionContextValue {
68
+ /** Active regions, default first. Empty when the store has none. */
69
+ regions: PublicRegion[];
70
+ /** The region in force, or null when the store has no regions. */
71
+ region: PublicRegion | null;
72
+ /** Spread into SDK reads: `...(regionId ? { regionId } : {})`. */
73
+ regionId: string | undefined;
74
+ /** Buyer country from the edge, or null. Reused by the tax estimate. */
75
+ buyerCountry: string | null;
76
+ /** Switch region: persists the choice and re-fetches server + client data. */
77
+ setRegion: (regionId: string) => void;
78
+ }
79
+
80
+ const RegionContext = createContext<RegionContextValue>({
81
+ regions: [],
82
+ region: null,
83
+ regionId: undefined,
84
+ buyerCountry: null,
85
+ setRegion: () => {},
86
+ });
87
+
88
+ export function useRegion() {
89
+ return useContext(RegionContext);
90
+ }
91
+
92
+ // ---- Auth Context ----
93
+ interface AuthContextValue {
94
+ isLoggedIn: boolean;
95
+ authLoading: boolean;
96
+ customer: CustomerProfile | null;
97
+ login: () => Promise<void>;
98
+ logout: () => Promise<void>;
99
+ }
100
+
101
+ const AuthContext = createContext<AuthContextValue>({
102
+ isLoggedIn: false,
103
+ authLoading: true,
104
+ customer: null,
105
+ login: async () => {},
106
+ logout: async () => {},
107
+ });
108
+
109
+ export function useAuth() {
110
+ return useContext(AuthContext);
111
+ }
112
+
113
+ // ---- Cart Context ----
114
+ interface CartContextValue {
115
+ cart: Cart | null;
116
+ cartLoading: boolean;
117
+ refreshCart: () => Promise<void>;
118
+ itemCount: number;
119
+ totals: { subtotal: number; discount: number; shipping: number; total: number };
120
+ }
121
+
122
+ const CartContext = createContext<CartContextValue>({
123
+ cart: null,
124
+ cartLoading: true,
125
+ refreshCart: async () => {},
126
+ itemCount: 0,
127
+ totals: { subtotal: 0, discount: 0, shipping: 0, total: 0 },
128
+ });
129
+
130
+ export function useCart() {
131
+ return useContext(CartContext);
132
+ }
133
+
134
+ // ---- Provider Component ----
135
+ <% if (i18nEnabled) { %>
136
+ export function StoreProvider({
137
+ children,
138
+ locale,
139
+ initialStoreInfo = null,
140
+ initialMessages,
141
+ initialRegions = [],
142
+ initialRegion = null,
143
+ initialCountry = null,
144
+ }: {
145
+ children: React.ReactNode;
146
+ locale?: string;
147
+ initialStoreInfo?: PublicStoreInfo | null;
148
+ initialMessages?: Record<string, Record<string, string>>;
149
+ initialRegions?: PublicRegion[];
150
+ initialRegion?: PublicRegion | null;
151
+ initialCountry?: string | null;
152
+ }) {
153
+ <% } else { %>
154
+ export function StoreProvider({
155
+ children,
156
+ initialStoreInfo = null,
157
+ initialRegions = [],
158
+ initialRegion = null,
159
+ initialCountry = null,
160
+ }: {
161
+ children: React.ReactNode;
162
+ initialStoreInfo?: PublicStoreInfo | null;
163
+ initialRegions?: PublicRegion[];
164
+ initialRegion?: PublicRegion | null;
165
+ initialCountry?: string | null;
166
+ }) {
167
+ <% } %>
168
+ // Seed from server-rendered data when available so SSR consumers (PriceDisplay,
169
+ // FreeShippingBar, locale-aware UI, etc.) render with real values from frame 0
170
+ // instead of falling back to USD/defaults until hydration completes.
171
+ const [storeInfo, setStoreInfo] = useState<PublicStoreInfo | null>(initialStoreInfo);
172
+ const [storeLoading, setStoreLoading] = useState(!initialStoreInfo);
173
+ const [capabilities, setCapabilities] = useState<PublicStoreCapabilities | null>(null);
174
+ const [capabilitiesLoading, setCapabilitiesLoading] = useState(true);
175
+ const [isLoggedIn, setIsLoggedIn] = useState(false);
176
+ const [customer, setCustomer] = useState<CustomerProfile | null>(null);
177
+ const [authLoading, setAuthLoading] = useState(true);
178
+ const [cart, setCart] = useState<Cart | null>(null);
179
+ const [cartLoading, setCartLoading] = useState(true);
180
+ const [regions, setRegions] = useState<PublicRegion[]>(initialRegions);
181
+ const [region, setRegionState] = useState<PublicRegion | null>(initialRegion);
182
+ const router = useRouter();
183
+ <% if (i18nEnabled) { %>
184
+ // Seed from server-rendered messages (see RootLayout) so translated text
185
+ // renders at frame 0 instead of a window of raw "namespace.key" strings
186
+ // before the client-only fetch in the effect below resolves — this also
187
+ // closes the same gap on every locale switch, since RootLayout re-runs
188
+ // server-side per locale and StoreProvider remounts with fresh initial data.
189
+ const [messages, setMessages] = useState<Record<string, Record<string, string>>>(
190
+ initialMessages ?? {}
191
+ );
192
+ <% } %>
193
+
194
+ // Check auth status via httpOnly cookie (server-side validation)
195
+ const refreshAuth = useCallback(async () => {
196
+ try {
197
+ const status = await checkAuthStatus();
198
+ setIsLoggedIn(status.isLoggedIn);
199
+ setCustomer(status.isLoggedIn ? (status.customer as CustomerProfile) : null);
200
+ } catch {
201
+ setIsLoggedIn(false);
202
+ setCustomer(null);
203
+ } finally {
204
+ setAuthLoading(false);
205
+ }
206
+ }, []);
207
+
208
+ // Initialize client, check auth, and fetch store info
209
+ useEffect(() => {
210
+ const client = initClient();
211
+ <% if (i18nEnabled) { %>
212
+
213
+ // Set locale on SDK client for translated product content
214
+ if (locale) {
215
+ client.setLocale(locale);
216
+ }
217
+
218
+ // Load UI message strings for current locale
219
+ getMessages(locale || defaultLocale).then(setMessages);
220
+ <% } %>
221
+
222
+ // Optimistic check: if brainerce_logged_in cookie exists, assume logged in
223
+ // while we validate the actual token server-side
224
+ if (typeof document !== 'undefined' && document.cookie.includes('brainerce_logged_in=1')) {
225
+ setIsLoggedIn(true);
226
+ }
227
+
228
+ // Validate auth token server-side
229
+ refreshAuth();
230
+
231
+ // Fetch store info only when the server didn't already provide it. The
232
+ // happy path is that <RootLayout> SSR'd fetchStoreInfo() and seeded us via
233
+ // initialStoreInfo; this branch is the failure fallback (server fetch
234
+ // returned null) so user-facing components don't render forever as 'USD'.
235
+ if (!initialStoreInfo) {
236
+ client
237
+ .getStoreInfo()
238
+ .then((raw) => setStoreInfo(pickPublicStoreInfo(raw)))
239
+ .catch(console.error)
240
+ .finally(() => setStoreLoading(false));
241
+ }
242
+ <% if (i18nEnabled) { %>
243
+ }, [refreshAuth, locale, initialStoreInfo]);
244
+ <% } else { %>
245
+ }, [refreshAuth, initialStoreInfo]);
246
+ <% } %>
247
+
248
+ // Fetch the channel's capabilities once, on mount. Deliberately its own
249
+ // effect with no dependencies: the payload carries no translated content, so
250
+ // folding it into the effect above would refetch it on every locale switch
251
+ // for nothing.
252
+ //
253
+ // ⛔ This must never break the store. A brand-new store is empty and this
254
+ // call can fail (offline, a channel not yet live, a backend hiccup). On
255
+ // failure we keep `capabilities` at null and every consumer falls back to its
256
+ // own default, which is the behaviour the storefront had before capabilities
257
+ // existed. Nothing here throws and nothing renders an empty box.
258
+ useEffect(() => {
259
+ let cancelled = false;
260
+ getClient()
261
+ .getStoreCapabilities()
262
+ .then((raw) => {
263
+ if (!cancelled) setCapabilities(pickPublicCapabilities(raw));
264
+ })
265
+ .catch(() => {
266
+ // Swallowed on purpose. See above. Consumers use their defaults.
267
+ })
268
+ .finally(() => {
269
+ if (!cancelled) setCapabilitiesLoading(false);
270
+ });
271
+ return () => {
272
+ cancelled = true;
273
+ };
274
+ }, []);
275
+
276
+ // Regions. The happy path is that RootLayout resolved them server-side and
277
+ // seeded us, so this effect does nothing at all. It is the fallback for a
278
+ // failed SSR resolve, and it deliberately mirrors the capabilities effect:
279
+ //
280
+ // ⛔ It must never break the store. An empty store, a channel that is not
281
+ // live yet and a backend hiccup all land in the catch, leave `regions` at
282
+ // `[]` and `region` at null, and every consumer then sends no `regionId`,
283
+ // exactly what this storefront did before regions existed. Nothing throws,
284
+ // and the switcher renders nothing rather than an empty dropdown.
285
+ useEffect(() => {
286
+ if (initialRegions.length > 0) return;
287
+ let cancelled = false;
288
+ getClient()
289
+ .getStoreRegions()
290
+ .then((result) => {
291
+ if (cancelled) return;
292
+ const list = Array.isArray(result?.data) ? result.data : [];
293
+ if (list.length === 0) return;
294
+ setRegions(list);
295
+ // The cookie is the shopper's own choice and outranks the country
296
+ // guess. A stale id (region deleted or deactivated) is ignored.
297
+ const chosenId = readRegionCookie();
298
+ const chosen = chosenId ? list.find((r) => r.id === chosenId) : undefined;
299
+ setRegionState(chosen ?? pickRegion(list, initialCountry));
300
+ })
301
+ .catch(() => {
302
+ // Swallowed on purpose. See above.
303
+ });
304
+ return () => {
305
+ cancelled = true;
306
+ };
307
+ }, [initialRegions, initialCountry]);
308
+
309
+ // Switching region has to move BOTH halves of the app: the cookie so the
310
+ // next server render prices the same way, `router.refresh()` so the server
311
+ // components already on screen re-fetch, and local state so client hooks
312
+ // (product listing, home grid) refetch immediately instead of waiting.
313
+ const setRegion = useCallback(
314
+ (regionId: string) => {
315
+ const next = regions.find((r) => r.id === regionId);
316
+ if (!next) return;
317
+ writeRegionCookie(regionId);
318
+ setRegionState(next);
319
+ router.refresh();
320
+ },
321
+ [regions, router]
322
+ );
323
+
324
+ // Cart management
325
+ const refreshCart = useCallback(async () => {
326
+ try {
327
+ setCartLoading(true);
328
+ const client = getClient();
329
+ // The SDK owns cart identity: smartGetCart() picks the guest, session or
330
+ // customer cart on its own. Never mirror the id into localStorage.
331
+ const c = await client.smartGetCart();
332
+ setCart(c);
333
+ } catch (err) {
334
+ console.error('Failed to load cart:', err);
335
+ } finally {
336
+ setCartLoading(false);
337
+ }
338
+ }, []);
339
+
340
+ useEffect(() => {
341
+ refreshCart();
342
+ }, [refreshCart, isLoggedIn]);
343
+
344
+ // Called after successful login (cookie already set by proxy)
345
+ const login = useCallback(async () => {
346
+ // Refresh auth state from server (reads httpOnly cookie)
347
+ await refreshAuth();
348
+
349
+ // Merge guest session cart into customer cart
350
+ const client = getClient();
351
+ client.syncCartOnLogin().catch(console.error);
352
+ }, [refreshAuth]);
353
+
354
+ const logout = useCallback(async () => {
355
+ // Clear httpOnly cookie server-side
356
+ await proxyLogout();
357
+
358
+ const client = getClient();
359
+ client.onLogout();
360
+ setIsLoggedIn(false);
361
+ setCustomer(null);
362
+ setCart(null);
363
+ refreshCart();
364
+ }, [refreshCart]);
365
+
366
+ const itemCount = cart
367
+ ? cart.items.reduce((sum, item) => sum + item.quantity, 0)
368
+ : 0;
369
+
370
+ const totals = cart ? getCartTotals(cart) : { subtotal: 0, discount: 0, shipping: 0, total: 0 };
371
+
372
+ <% if (i18nEnabled) { %>
373
+ return (
374
+ <MessagesContext.Provider value={messages}>
375
+ <StoreInfoContext.Provider value={{ storeInfo, loading: storeLoading }}>
376
+ <StoreCapabilitiesContext.Provider value={{ capabilities, capabilitiesLoading }}>
377
+ <RegionContext.Provider
378
+ value={{
379
+ regions,
380
+ region,
381
+ regionId: region?.id,
382
+ buyerCountry: initialCountry,
383
+ setRegion,
384
+ }}
385
+ >
386
+ <AuthContext.Provider value={{ isLoggedIn, authLoading, customer, login, logout }}>
387
+ <CartContext.Provider
388
+ value={{ cart, cartLoading, refreshCart, itemCount, totals }}
389
+ >
390
+ {children}
391
+ </CartContext.Provider>
392
+ </AuthContext.Provider>
393
+ </RegionContext.Provider>
394
+ </StoreCapabilitiesContext.Provider>
395
+ </StoreInfoContext.Provider>
396
+ </MessagesContext.Provider>
397
+ );
398
+ <% } else { %>
399
+ return (
400
+ <StoreInfoContext.Provider value={{ storeInfo, loading: storeLoading }}>
401
+ <StoreCapabilitiesContext.Provider value={{ capabilities, capabilitiesLoading }}>
402
+ <RegionContext.Provider
403
+ value={{
404
+ regions,
405
+ region,
406
+ regionId: region?.id,
407
+ buyerCountry: initialCountry,
408
+ setRegion,
409
+ }}
410
+ >
411
+ <AuthContext.Provider value={{ isLoggedIn, authLoading, customer, login, logout }}>
412
+ <CartContext.Provider
413
+ value={{ cart, cartLoading, refreshCart, itemCount, totals }}
414
+ >
415
+ {children}
416
+ </CartContext.Provider>
417
+ </AuthContext.Provider>
418
+ </RegionContext.Provider>
419
+ </StoreCapabilitiesContext.Provider>
420
+ </StoreInfoContext.Provider>
421
+ );
422
+ <% } %>
423
+ }