create-brainerce-store 1.52.4 → 1.53.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.
@@ -1,223 +1,227 @@
1
- <% if (i18nEnabled) { %>
2
- import type { Metadata } from 'next';
3
- <%- fontImport %>
4
- import { StoreProvider } from '@/core/providers/store-provider';
5
- import { AnnouncementBar } from '@/ui/layout/announcement-bar';
6
- import { SiteHeader } from '@/ui/layout/site-header';
7
- import { SiteFooter } from '@/ui/layout/site-footer';
8
- import { BrainerceBotWidget } from '@/components/brainerce-bot';
9
- import { getServerClient, fetchStoreInfo } from '@/core/lib/brainerce';
10
- import { getDirection, supportedLocales } from '@/i18n';
11
- import { getNonce } from '@/core/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
- {/* Brainerce cookieless traffic analytics — auto-tracks pageviews +
90
- SPA route changes. Cookieless (no consent banner needed); the
91
- merchant can toggle it per sales channel in the dashboard. */}
92
- <script
93
- defer
94
- src={`${process.env.NEXT_PUBLIC_BRAINERCE_PIXEL_URL || 'https://api.brainerce.com'}/t.js`}
95
- data-channel={
96
- process.env.NEXT_PUBLIC_BRAINERCE_SALES_CHANNEL_ID || '<%= connectionId %>'
97
- }
98
- nonce={nonce}
99
- suppressHydrationWarning
100
- />
101
- </head>
102
- <body className={font.className}>
103
- <StoreProvider locale={locale} initialStoreInfo={storeInfo}>
104
- <div className="min-h-screen flex flex-col">
105
- <AnnouncementBar announcements={announcements} />
106
- <SiteHeader header={siteHeader} storeName={<%- storeNameJs %>} />
107
- <main className="flex-1">{children}</main>
108
- <SiteFooter footer={siteFooter} storeName={<%- storeNameJs %>} />
109
- </div>
110
- <BrainerceBotWidget />
111
- </StoreProvider>
112
- </body>
113
- </html>
114
- );
115
- }
116
- <% } else { %>
117
- import type { Metadata } from 'next';
118
- <%- fontImport %>
119
- import { StoreProvider } from '@/core/providers/store-provider';
120
- import { AnnouncementBar } from '@/ui/layout/announcement-bar';
121
- import { SiteHeader } from '@/ui/layout/site-header';
122
- import { SiteFooter } from '@/ui/layout/site-footer';
123
- import { BrainerceBotWidget } from '@/components/brainerce-bot';
124
- import { getServerClient, fetchStoreInfo } from '@/core/lib/brainerce';
125
- import { getNonce } from '@/core/lib/nonce';
126
- import './globals.css';
127
-
128
- <%- fontVariable %>
129
-
130
- const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com';
131
-
132
- export const metadata: Metadata = {
133
- metadataBase: new URL(baseUrl),
134
- title: {
135
- default: <%- storeNameJs %>,
136
- template: <%- titleTemplateJs %>,
137
- },
138
- description: <%- storeNameJs %>,
139
- alternates: {
140
- canonical: '/',
141
- },
142
- openGraph: {
143
- siteName: <%- storeNameJs %>,
144
- locale: '<%= ogLocale %>',
145
- type: 'website',
146
- },
147
- robots: {
148
- index: true,
149
- follow: true,
150
- },
151
- };
152
-
153
- const organizationJsonLd = {
154
- '@context': 'https://schema.org',
155
- '@type': 'Organization',
156
- name: <%- storeNameJs %>,
157
- url: baseUrl,
158
- };
159
-
160
- export default async function RootLayout({
161
- children,
162
- }: {
163
- children: React.ReactNode;
164
- }) {
165
- const nonce = await getNonce();
166
-
167
- // Merchant-driven layout chrome — fetched server-side. Each call falls back
168
- // to null/[] on 404 so the layout never crashes when the merchant hasn't
169
- // seeded a particular content type yet. New stores ship with default rows
170
- // seeded by the backend (StoresService.seedDefaultContent).
171
- const client = getServerClient();
172
- const [announcements, siteHeader, siteFooter, storeInfo] = await Promise.all([
173
- client.content.announcement.list().catch(() => []),
174
- client.content.header.get('main').catch(() => null),
175
- client.content.footer.get('main').catch(() => null),
176
- // SSR-fetch store config so PriceDisplay / FreeShippingBar / upsell UI
177
- // render with the real currency, feature flags, and i18n config at frame 0
178
- // without this, Googlebot sees USD/defaults baked into the HTML.
179
- fetchStoreInfo(),
180
- ]);
181
-
182
- return (
183
- <html lang="<%= language %>" dir="<%= direction %>">
184
- <head>
185
- <script
186
- type="application/ld+json"
187
- nonce={nonce}
188
- suppressHydrationWarning
189
- dangerouslySetInnerHTML={{
190
- __html: JSON.stringify(organizationJsonLd)
191
- .replace(/</g, '\\u003c')
192
- .replace(/>/g, '\\u003e')
193
- .replace(/&/g, '\\u0026'),
194
- }}
195
- />
196
- {/* Brainerce cookieless traffic analytics — auto-tracks pageviews +
197
- SPA route changes. Cookieless (no consent banner needed); the
198
- merchant can toggle it per sales channel in the dashboard. */}
199
- <script
200
- defer
201
- src={`${process.env.NEXT_PUBLIC_BRAINERCE_PIXEL_URL || 'https://api.brainerce.com'}/t.js`}
202
- data-channel={
203
- process.env.NEXT_PUBLIC_BRAINERCE_SALES_CHANNEL_ID || '<%= connectionId %>'
204
- }
205
- nonce={nonce}
206
- suppressHydrationWarning
207
- />
208
- </head>
209
- <body className={font.className}>
210
- <StoreProvider initialStoreInfo={storeInfo}>
211
- <div className="min-h-screen flex flex-col">
212
- <AnnouncementBar announcements={announcements} />
213
- <SiteHeader header={siteHeader} storeName={<%- storeNameJs %>} />
214
- <main className="flex-1">{children}</main>
215
- <SiteFooter footer={siteFooter} storeName={<%- storeNameJs %>} />
216
- </div>
217
- <BrainerceBotWidget />
218
- </StoreProvider>
219
- </body>
220
- </html>
221
- );
222
- }
223
- <% } %>
1
+ <% if (i18nEnabled) { %>
2
+ import type { Metadata } from 'next';
3
+ <%- fontImport %>
4
+ import { StoreProvider } from '@/core/providers/store-provider';
5
+ import { AnnouncementBar } from '@/ui/layout/announcement-bar';
6
+ import { SiteHeader } from '@/ui/layout/site-header';
7
+ import { SiteFooter } from '@/ui/layout/site-footer';
8
+ import { BrainerceBotWidget } from '@/components/brainerce-bot';
9
+ import { getServerClient, fetchStoreInfo } from '@/core/lib/brainerce';
10
+ import { getDirection, getMessages, supportedLocales } from '@/i18n';
11
+ import { getNonce } from '@/core/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, messages] = 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
+ // SSR-fetch UI message strings so translated text renders at frame 0 —
74
+ // without this, every page (and every locale switch) shows raw
75
+ // "namespace.key" strings until the client-only fetch resolves.
76
+ getMessages(locale),
77
+ ]);
78
+
79
+ return (
80
+ <html lang={locale} dir={dir}>
81
+ <head>
82
+ <script
83
+ type="application/ld+json"
84
+ nonce={nonce}
85
+ suppressHydrationWarning
86
+ dangerouslySetInnerHTML={{
87
+ __html: JSON.stringify(organizationJsonLd)
88
+ .replace(/</g, '\\u003c')
89
+ .replace(/>/g, '\\u003e')
90
+ .replace(/&/g, '\\u0026'),
91
+ }}
92
+ />
93
+ {/* Brainerce cookieless traffic analytics — auto-tracks pageviews +
94
+ SPA route changes. Cookieless (no consent banner needed); the
95
+ merchant can toggle it per sales channel in the dashboard. */}
96
+ <script
97
+ defer
98
+ src={`${process.env.NEXT_PUBLIC_BRAINERCE_PIXEL_URL || 'https://api.brainerce.com'}/t.js`}
99
+ data-channel={
100
+ process.env.NEXT_PUBLIC_BRAINERCE_SALES_CHANNEL_ID || '<%= connectionId %>'
101
+ }
102
+ nonce={nonce}
103
+ suppressHydrationWarning
104
+ />
105
+ </head>
106
+ <body className={font.className}>
107
+ <StoreProvider locale={locale} initialStoreInfo={storeInfo} initialMessages={messages}>
108
+ <div className="min-h-screen flex flex-col">
109
+ <AnnouncementBar announcements={announcements} />
110
+ <SiteHeader header={siteHeader} storeName={<%- storeNameJs %>} />
111
+ <main className="flex-1">{children}</main>
112
+ <SiteFooter footer={siteFooter} storeName={<%- storeNameJs %>} />
113
+ </div>
114
+ <BrainerceBotWidget />
115
+ </StoreProvider>
116
+ </body>
117
+ </html>
118
+ );
119
+ }
120
+ <% } else { %>
121
+ import type { Metadata } from 'next';
122
+ <%- fontImport %>
123
+ import { StoreProvider } from '@/core/providers/store-provider';
124
+ import { AnnouncementBar } from '@/ui/layout/announcement-bar';
125
+ import { SiteHeader } from '@/ui/layout/site-header';
126
+ import { SiteFooter } from '@/ui/layout/site-footer';
127
+ import { BrainerceBotWidget } from '@/components/brainerce-bot';
128
+ import { getServerClient, fetchStoreInfo } from '@/core/lib/brainerce';
129
+ import { getNonce } from '@/core/lib/nonce';
130
+ import './globals.css';
131
+
132
+ <%- fontVariable %>
133
+
134
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com';
135
+
136
+ export const metadata: Metadata = {
137
+ metadataBase: new URL(baseUrl),
138
+ title: {
139
+ default: <%- storeNameJs %>,
140
+ template: <%- titleTemplateJs %>,
141
+ },
142
+ description: <%- storeNameJs %>,
143
+ alternates: {
144
+ canonical: '/',
145
+ },
146
+ openGraph: {
147
+ siteName: <%- storeNameJs %>,
148
+ locale: '<%= ogLocale %>',
149
+ type: 'website',
150
+ },
151
+ robots: {
152
+ index: true,
153
+ follow: true,
154
+ },
155
+ };
156
+
157
+ const organizationJsonLd = {
158
+ '@context': 'https://schema.org',
159
+ '@type': 'Organization',
160
+ name: <%- storeNameJs %>,
161
+ url: baseUrl,
162
+ };
163
+
164
+ export default async function RootLayout({
165
+ children,
166
+ }: {
167
+ children: React.ReactNode;
168
+ }) {
169
+ const nonce = await getNonce();
170
+
171
+ // Merchant-driven layout chrome — fetched server-side. Each call falls back
172
+ // to null/[] on 404 so the layout never crashes when the merchant hasn't
173
+ // seeded a particular content type yet. New stores ship with default rows
174
+ // seeded by the backend (StoresService.seedDefaultContent).
175
+ const client = getServerClient();
176
+ const [announcements, siteHeader, siteFooter, storeInfo] = await Promise.all([
177
+ client.content.announcement.list().catch(() => []),
178
+ client.content.header.get('main').catch(() => null),
179
+ client.content.footer.get('main').catch(() => null),
180
+ // SSR-fetch store config so PriceDisplay / FreeShippingBar / upsell UI
181
+ // render with the real currency, feature flags, and i18n config at frame 0
182
+ // — without this, Googlebot sees USD/defaults baked into the HTML.
183
+ fetchStoreInfo(),
184
+ ]);
185
+
186
+ return (
187
+ <html lang="<%= language %>" dir="<%= direction %>">
188
+ <head>
189
+ <script
190
+ type="application/ld+json"
191
+ nonce={nonce}
192
+ suppressHydrationWarning
193
+ dangerouslySetInnerHTML={{
194
+ __html: JSON.stringify(organizationJsonLd)
195
+ .replace(/</g, '\\u003c')
196
+ .replace(/>/g, '\\u003e')
197
+ .replace(/&/g, '\\u0026'),
198
+ }}
199
+ />
200
+ {/* Brainerce cookieless traffic analytics — auto-tracks pageviews +
201
+ SPA route changes. Cookieless (no consent banner needed); the
202
+ merchant can toggle it per sales channel in the dashboard. */}
203
+ <script
204
+ defer
205
+ src={`${process.env.NEXT_PUBLIC_BRAINERCE_PIXEL_URL || 'https://api.brainerce.com'}/t.js`}
206
+ data-channel={
207
+ process.env.NEXT_PUBLIC_BRAINERCE_SALES_CHANNEL_ID || '<%= connectionId %>'
208
+ }
209
+ nonce={nonce}
210
+ suppressHydrationWarning
211
+ />
212
+ </head>
213
+ <body className={font.className}>
214
+ <StoreProvider initialStoreInfo={storeInfo}>
215
+ <div className="min-h-screen flex flex-col">
216
+ <AnnouncementBar announcements={announcements} />
217
+ <SiteHeader header={siteHeader} storeName={<%- storeNameJs %>} />
218
+ <main className="flex-1">{children}</main>
219
+ <SiteFooter footer={siteFooter} storeName={<%- storeNameJs %>} />
220
+ </div>
221
+ <BrainerceBotWidget />
222
+ </StoreProvider>
223
+ </body>
224
+ </html>
225
+ );
226
+ }
227
+ <% } %>
@@ -1,10 +1,17 @@
1
1
  'use client';
2
2
 
3
3
  import { useState, useEffect, useRef } from 'react';
4
- import type { SetShippingAddressDto, ShippingDestinations } from 'brainerce';
4
+ import type { SetShippingAddressDto, ShippingDestinations, AddressSuggestion } from 'brainerce';
5
5
  import { useTranslations } from '@/core/lib/translations';
6
6
  import { cn } from '@/core/lib/utils';
7
7
  import { isValidEmail } from '@/core/lib/validation';
8
+ import { getClient } from '@/core/lib/brainerce';
9
+
10
+ // Debounce address-suggestion calls rather than firing on every keystroke —
11
+ // both to avoid a jumpy dropdown and to keep autocomplete-session call volume
12
+ // reasonable (see `getAddressSuggestions` doc in the SDK).
13
+ const ADDRESS_SEARCH_DEBOUNCE_MS = 300;
14
+ const ADDRESS_SEARCH_MIN_CHARS = 3;
8
15
 
9
16
  interface CheckoutConsent {
10
17
  acceptsMarketing: boolean;
@@ -51,6 +58,84 @@ export function CheckoutForm({
51
58
  const tc = useTranslations('common');
52
59
  const hasAppliedPrefill = useRef(!!initialValues);
53
60
 
61
+ // Address-autocomplete state — groups one address-entry attempt for
62
+ // session-token billing (see `getAddressSuggestions` in the SDK). A fresh
63
+ // token is minted whenever a suggestion is picked, ending that session.
64
+ const [addressSuggestions, setAddressSuggestions] = useState<AddressSuggestion[]>([]);
65
+ const [showAddressSuggestions, setShowAddressSuggestions] = useState(false);
66
+ const [isSearchingAddress, setIsSearchingAddress] = useState(false);
67
+ const [outsideDeliveryZone, setOutsideDeliveryZone] = useState(false);
68
+ const addressSessionToken = useRef(
69
+ typeof crypto !== 'undefined' ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`
70
+ );
71
+ const addressSearchTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
72
+
73
+ useEffect(() => {
74
+ return () => {
75
+ if (addressSearchTimer.current) clearTimeout(addressSearchTimer.current);
76
+ };
77
+ }, []);
78
+
79
+ function handleLine1Change(value: string) {
80
+ updateField('line1', value);
81
+ setOutsideDeliveryZone(false);
82
+
83
+ if (addressSearchTimer.current) clearTimeout(addressSearchTimer.current);
84
+
85
+ const query = value.trim();
86
+ if (query.length < ADDRESS_SEARCH_MIN_CHARS) {
87
+ setAddressSuggestions([]);
88
+ setShowAddressSuggestions(false);
89
+ return;
90
+ }
91
+
92
+ addressSearchTimer.current = setTimeout(async () => {
93
+ setIsSearchingAddress(true);
94
+ try {
95
+ const results = await getClient().getAddressSuggestions(
96
+ query,
97
+ addressSessionToken.current
98
+ );
99
+ setAddressSuggestions(results);
100
+ setShowAddressSuggestions(results.length > 0);
101
+ } catch {
102
+ // Autocomplete is a soft enhancement — a failed lookup just means no
103
+ // suggestions this keystroke; the shopper can keep typing manually.
104
+ setAddressSuggestions([]);
105
+ setShowAddressSuggestions(false);
106
+ } finally {
107
+ setIsSearchingAddress(false);
108
+ }
109
+ }, ADDRESS_SEARCH_DEBOUNCE_MS);
110
+ }
111
+
112
+ async function handleSelectAddressSuggestion(suggestion: AddressSuggestion) {
113
+ setShowAddressSuggestions(false);
114
+ try {
115
+ const { address, inZone } = await getClient().getAddressDetails(
116
+ suggestion.placeId,
117
+ addressSessionToken.current
118
+ );
119
+ setFormData((prev) => ({
120
+ ...prev,
121
+ line1: address.line1 || prev.line1,
122
+ city: address.city || prev.city,
123
+ region: address.region || prev.region,
124
+ postalCode: address.postalCode || prev.postalCode,
125
+ country: address.country || prev.country,
126
+ }));
127
+ setOutsideDeliveryZone(!inZone);
128
+ } catch {
129
+ // Resolution failed — keep whatever the shopper had typed/selected as
130
+ // the visible text; they can still fill in the rest manually.
131
+ } finally {
132
+ // New session for the next address-entry attempt.
133
+ addressSessionToken.current =
134
+ typeof crypto !== 'undefined' ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`;
135
+ setAddressSuggestions([]);
136
+ }
137
+ }
138
+
54
139
  // Sync prefill data when it arrives async (e.g. from getCheckoutPrefillData)
55
140
  useEffect(() => {
56
141
  if (!initialValues || hasAppliedPrefill.current) return;
@@ -261,20 +346,49 @@ export function CheckoutForm({
261
346
  </div>
262
347
  </div>
263
348
 
264
- {/* Address line 1 */}
265
- <div>
349
+ {/* Address line 1 — autocomplete typeahead */}
350
+ <div className="relative">
266
351
  <label htmlFor="line1" className="text-foreground mb-1 block text-sm font-medium">
267
352
  {t('address')} <span className="text-destructive">*</span>
268
353
  </label>
269
354
  <input
270
355
  id="line1"
271
356
  type="text"
357
+ autoComplete="off"
272
358
  value={formData.line1}
273
- onChange={(e) => updateField('line1', e.target.value)}
359
+ onChange={(e) => handleLine1Change(e.target.value)}
360
+ onFocus={() => setShowAddressSuggestions(addressSuggestions.length > 0)}
361
+ onBlur={() => setTimeout(() => setShowAddressSuggestions(false), 150)}
274
362
  className={cn(inputClass, errors.line1 ? 'border-destructive' : 'border-border')}
275
363
  placeholder={t('streetAddress')}
276
364
  />
277
365
  {errors.line1 && <p className="text-destructive mt-1 text-xs">{errors.line1}</p>}
366
+
367
+ {showAddressSuggestions && addressSuggestions.length > 0 && (
368
+ <ul className="bg-background border-border absolute z-10 mt-1 max-h-60 w-full overflow-y-auto rounded border shadow-lg">
369
+ {addressSuggestions.map((suggestion) => (
370
+ <li key={suggestion.placeId}>
371
+ <button
372
+ type="button"
373
+ // onMouseDown fires before the input's onBlur, so the
374
+ // click registers before the dropdown closes.
375
+ onMouseDown={() => handleSelectAddressSuggestion(suggestion)}
376
+ className="hover:bg-muted w-full px-3 py-2 text-start text-sm"
377
+ >
378
+ {suggestion.description}
379
+ </button>
380
+ </li>
381
+ ))}
382
+ </ul>
383
+ )}
384
+ {isSearchingAddress && (
385
+ <p className="text-muted-foreground mt-1 text-xs">{t('searchingAddress')}</p>
386
+ )}
387
+ {outsideDeliveryZone && (
388
+ <p className="mt-2 rounded border border-amber-300 bg-amber-50 px-3 py-2 text-xs text-amber-800 dark:border-amber-800 dark:bg-amber-950 dark:text-amber-200">
389
+ {t('outsideDeliveryZone')}
390
+ </p>
391
+ )}
278
392
  </div>
279
393
 
280
394
  {/* Address line 2 */}
@@ -74,10 +74,12 @@ export function StoreProvider({
74
74
  children,
75
75
  locale,
76
76
  initialStoreInfo = null,
77
+ initialMessages,
77
78
  }: {
78
79
  children: React.ReactNode;
79
80
  locale?: string;
80
81
  initialStoreInfo?: PublicStoreInfo | null;
82
+ initialMessages?: Record<string, Record<string, string>>;
81
83
  }) {
82
84
  <% } else { %>
83
85
  export function StoreProvider({
@@ -99,7 +101,14 @@ export function StoreProvider({
99
101
  const [cart, setCart] = useState<Cart | null>(null);
100
102
  const [cartLoading, setCartLoading] = useState(true);
101
103
  <% if (i18nEnabled) { %>
102
- const [messages, setMessages] = useState<Record<string, Record<string, string>>>({});
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
+ );
103
112
  <% } %>
104
113
 
105
114
  // Check auth status via httpOnly cookie (server-side validation)