create-brainerce-store 1.18.0 → 1.19.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 +31 -9
- package/messages/en.json +366 -362
- package/messages/he.json +366 -362
- package/package.json +45 -45
- package/templates/nextjs/base/next.config.ts +31 -31
- package/templates/nextjs/base/scripts/fetch-store-info.mjs +81 -81
- package/templates/nextjs/base/src/app/.well-known/apple-developer-merchantid-domain-association/route.ts +26 -26
- package/templates/nextjs/base/src/app/account/layout.tsx +9 -9
- package/templates/nextjs/base/src/app/account/page.tsx +122 -122
- package/templates/nextjs/base/src/app/api/auth/logout/route.ts +14 -14
- package/templates/nextjs/base/src/app/api/auth/me/route.ts +56 -56
- package/templates/nextjs/base/src/app/api/auth/oauth-callback/route.ts +59 -59
- package/templates/nextjs/base/src/app/api/auth/reset-callback/route.ts +41 -41
- package/templates/nextjs/base/src/app/api/auth/reset-password/route.ts +77 -77
- package/templates/nextjs/base/src/app/api/store/[...path]/route.ts +198 -198
- package/templates/nextjs/base/src/app/auth/callback/page.tsx +92 -92
- package/templates/nextjs/base/src/app/cart/layout.tsx +9 -9
- package/templates/nextjs/base/src/app/cart/page.tsx +204 -204
- package/templates/nextjs/base/src/app/checkout/layout.tsx +9 -9
- package/templates/nextjs/base/src/app/checkout/page.tsx +860 -860
- package/templates/nextjs/base/src/app/forgot-password/page.tsx +112 -112
- package/templates/nextjs/base/src/app/layout.tsx.ejs +75 -0
- package/templates/nextjs/base/src/app/login/layout.tsx +9 -9
- package/templates/nextjs/base/src/app/login/page.tsx +59 -59
- package/templates/nextjs/base/src/app/order-confirmation/layout.tsx +9 -9
- package/templates/nextjs/base/src/app/order-confirmation/page.tsx +254 -254
- package/templates/nextjs/base/src/app/products/[slug]/page.tsx +67 -67
- package/templates/nextjs/base/src/app/products/[slug]/product-client-section.tsx +486 -486
- package/templates/nextjs/base/src/app/products/layout.tsx +18 -18
- package/templates/nextjs/base/src/app/products/page.tsx +431 -431
- package/templates/nextjs/base/src/app/register/layout.tsx +9 -9
- package/templates/nextjs/base/src/app/register/page.tsx +65 -65
- package/templates/nextjs/base/src/app/reset-password/page.tsx +132 -132
- package/templates/nextjs/base/src/app/robots.ts +14 -14
- package/templates/nextjs/base/src/app/sitemap.ts +25 -25
- package/templates/nextjs/base/src/app/verify-email/page.tsx +258 -258
- package/templates/nextjs/base/src/components/account/address-book.tsx +432 -432
- package/templates/nextjs/base/src/components/account/order-history.tsx +350 -350
- package/templates/nextjs/base/src/components/auth/oauth-buttons.tsx +137 -137
- package/templates/nextjs/base/src/components/auth/register-form.tsx +232 -232
- package/templates/nextjs/base/src/components/cart/cart-bundle-offer.tsx +247 -111
- package/templates/nextjs/base/src/components/cart/cart-item.tsx +153 -153
- package/templates/nextjs/base/src/components/cart/cart-upgrade-banner.tsx +142 -142
- package/templates/nextjs/base/src/components/cart/free-shipping-bar.tsx +59 -59
- package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +415 -415
- package/templates/nextjs/base/src/components/checkout/order-bump-card.tsx +243 -83
- package/templates/nextjs/base/src/components/checkout/payment-step.tsx +519 -519
- package/templates/nextjs/base/src/components/layout/footer.tsx +41 -41
- package/templates/nextjs/base/src/components/layout/header.tsx +336 -336
- package/templates/nextjs/base/src/components/layout/language-switcher.tsx.ejs +63 -0
- package/templates/nextjs/base/src/components/products/discount-badge.tsx +22 -22
- package/templates/nextjs/base/src/components/products/frequently-bought-together.tsx +202 -202
- package/templates/nextjs/base/src/components/products/product-card.tsx +218 -218
- package/templates/nextjs/base/src/components/products/recommendation-section.tsx +107 -107
- package/templates/nextjs/base/src/components/products/stock-badge.tsx +63 -63
- package/templates/nextjs/base/src/components/products/variant-selector.tsx +292 -292
- package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +72 -72
- package/templates/nextjs/base/src/i18n.ts.ejs +21 -0
- package/templates/nextjs/base/src/lib/auth.ts +149 -149
- package/templates/nextjs/base/src/lib/brainerce.ts.ejs +9 -0
- package/templates/nextjs/base/src/lib/translations.ts.ejs +31 -0
- package/templates/nextjs/base/src/middleware.ts.ejs +81 -0
- package/templates/nextjs/base/src/providers/store-provider.tsx.ejs +41 -0
- package/templates/nextjs/base/src/lib/translations.ts +0 -11
- package/templates/nextjs/base/src/middleware.ts +0 -25
|
@@ -1,72 +1,72 @@
|
|
|
1
|
-
import type { Product } from 'brainerce';
|
|
2
|
-
import { getProductPriceInfo } from 'brainerce';
|
|
3
|
-
|
|
4
|
-
interface ProductJsonLdProps {
|
|
5
|
-
product: Product;
|
|
6
|
-
url: string;
|
|
7
|
-
currency?: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function ProductJsonLd({ product, url, currency = 'USD' }: ProductJsonLdProps) {
|
|
11
|
-
const priceInfo = getProductPriceInfo(product);
|
|
12
|
-
const imageUrl = product.images?.[0]?.url;
|
|
13
|
-
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || '';
|
|
14
|
-
|
|
15
|
-
const productJsonLd = {
|
|
16
|
-
'@context': 'https://schema.org',
|
|
17
|
-
'@type': 'Product',
|
|
18
|
-
name: product.name,
|
|
19
|
-
description: product.description || product.name,
|
|
20
|
-
image: imageUrl,
|
|
21
|
-
url,
|
|
22
|
-
sku: product.sku || product.id,
|
|
23
|
-
offers: {
|
|
24
|
-
'@type': 'Offer',
|
|
25
|
-
price: priceInfo.price,
|
|
26
|
-
priceCurrency: currency,
|
|
27
|
-
availability:
|
|
28
|
-
product.inventory?.canPurchase !== false
|
|
29
|
-
? 'https://schema.org/InStock'
|
|
30
|
-
: 'https://schema.org/OutOfStock',
|
|
31
|
-
url,
|
|
32
|
-
},
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
const breadcrumbJsonLd = {
|
|
36
|
-
'@context': 'https://schema.org',
|
|
37
|
-
'@type': 'BreadcrumbList',
|
|
38
|
-
itemListElement: [
|
|
39
|
-
{
|
|
40
|
-
'@type': 'ListItem',
|
|
41
|
-
position: 1,
|
|
42
|
-
name: 'Home',
|
|
43
|
-
item: baseUrl || '/',
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
'@type': 'ListItem',
|
|
47
|
-
position: 2,
|
|
48
|
-
name: 'Products',
|
|
49
|
-
item: `${baseUrl}/products`,
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
'@type': 'ListItem',
|
|
53
|
-
position: 3,
|
|
54
|
-
name: product.name,
|
|
55
|
-
item: url,
|
|
56
|
-
},
|
|
57
|
-
],
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
return (
|
|
61
|
-
<>
|
|
62
|
-
<script
|
|
63
|
-
type="application/ld+json"
|
|
64
|
-
dangerouslySetInnerHTML={{ __html: JSON.stringify(productJsonLd) }}
|
|
65
|
-
/>
|
|
66
|
-
<script
|
|
67
|
-
type="application/ld+json"
|
|
68
|
-
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbJsonLd) }}
|
|
69
|
-
/>
|
|
70
|
-
</>
|
|
71
|
-
);
|
|
72
|
-
}
|
|
1
|
+
import type { Product } from 'brainerce';
|
|
2
|
+
import { getProductPriceInfo } from 'brainerce';
|
|
3
|
+
|
|
4
|
+
interface ProductJsonLdProps {
|
|
5
|
+
product: Product;
|
|
6
|
+
url: string;
|
|
7
|
+
currency?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function ProductJsonLd({ product, url, currency = 'USD' }: ProductJsonLdProps) {
|
|
11
|
+
const priceInfo = getProductPriceInfo(product);
|
|
12
|
+
const imageUrl = product.images?.[0]?.url;
|
|
13
|
+
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || '';
|
|
14
|
+
|
|
15
|
+
const productJsonLd = {
|
|
16
|
+
'@context': 'https://schema.org',
|
|
17
|
+
'@type': 'Product',
|
|
18
|
+
name: product.name,
|
|
19
|
+
description: product.description || product.name,
|
|
20
|
+
image: imageUrl,
|
|
21
|
+
url,
|
|
22
|
+
sku: product.sku || product.id,
|
|
23
|
+
offers: {
|
|
24
|
+
'@type': 'Offer',
|
|
25
|
+
price: priceInfo.price,
|
|
26
|
+
priceCurrency: currency,
|
|
27
|
+
availability:
|
|
28
|
+
product.inventory?.canPurchase !== false
|
|
29
|
+
? 'https://schema.org/InStock'
|
|
30
|
+
: 'https://schema.org/OutOfStock',
|
|
31
|
+
url,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const breadcrumbJsonLd = {
|
|
36
|
+
'@context': 'https://schema.org',
|
|
37
|
+
'@type': 'BreadcrumbList',
|
|
38
|
+
itemListElement: [
|
|
39
|
+
{
|
|
40
|
+
'@type': 'ListItem',
|
|
41
|
+
position: 1,
|
|
42
|
+
name: 'Home',
|
|
43
|
+
item: baseUrl || '/',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
'@type': 'ListItem',
|
|
47
|
+
position: 2,
|
|
48
|
+
name: 'Products',
|
|
49
|
+
item: `${baseUrl}/products`,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
'@type': 'ListItem',
|
|
53
|
+
position: 3,
|
|
54
|
+
name: product.name,
|
|
55
|
+
item: url,
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<>
|
|
62
|
+
<script
|
|
63
|
+
type="application/ld+json"
|
|
64
|
+
dangerouslySetInnerHTML={{ __html: JSON.stringify(productJsonLd) }}
|
|
65
|
+
/>
|
|
66
|
+
<script
|
|
67
|
+
type="application/ld+json"
|
|
68
|
+
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbJsonLd) }}
|
|
69
|
+
/>
|
|
70
|
+
</>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
@@ -1,5 +1,26 @@
|
|
|
1
|
+
<% if (i18nEnabled) { %>
|
|
2
|
+
// Multi-language store — locales loaded dynamically
|
|
3
|
+
export const defaultLocale = '<%= defaultLocale %>';
|
|
4
|
+
export const supportedLocales = <%- supportedLocales %> as const;
|
|
5
|
+
export type Locale = (typeof supportedLocales)[number];
|
|
6
|
+
|
|
7
|
+
const RTL_LOCALES = new Set(['he', 'ar']);
|
|
8
|
+
|
|
9
|
+
export function getDirection(locale: string): 'ltr' | 'rtl' {
|
|
10
|
+
return RTL_LOCALES.has(locale) ? 'rtl' : 'ltr';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function getMessages(locale: string): Promise<Record<string, Record<string, string>>> {
|
|
14
|
+
try {
|
|
15
|
+
return (await import(`../messages/${locale}.json`)).default;
|
|
16
|
+
} catch {
|
|
17
|
+
return (await import(`../messages/<%= defaultLocale %>.json`)).default;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
<% } else { %>
|
|
1
21
|
import messages from '../messages/<%= language %>.json';
|
|
2
22
|
|
|
3
23
|
export const defaultLocale = '<%= language %>';
|
|
4
24
|
export const direction = '<%= direction %>' as 'ltr' | 'rtl';
|
|
5
25
|
export { messages };
|
|
26
|
+
<% } %>
|
|
@@ -1,149 +1,149 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Client-side auth helpers that call the BFF proxy API routes.
|
|
3
|
-
* All mutating requests include the CSRF header.
|
|
4
|
-
* The token is managed server-side via httpOnly cookies — never exposed to JS.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
const CONNECTION_ID = process.env.NEXT_PUBLIC_BRAINERCE_CONNECTION_ID || '';
|
|
8
|
-
|
|
9
|
-
const CSRF_HEADERS: Record<string, string> = {
|
|
10
|
-
'Content-Type': 'application/json',
|
|
11
|
-
'X-Requested-With': 'brainerce',
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
interface LoginResult {
|
|
15
|
-
customer: {
|
|
16
|
-
id: string;
|
|
17
|
-
email: string;
|
|
18
|
-
firstName?: string;
|
|
19
|
-
lastName?: string;
|
|
20
|
-
emailVerified: boolean;
|
|
21
|
-
};
|
|
22
|
-
expiresAt: string;
|
|
23
|
-
requiresVerification?: boolean;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
interface RegisterResult {
|
|
27
|
-
customer: {
|
|
28
|
-
id: string;
|
|
29
|
-
email: string;
|
|
30
|
-
firstName?: string;
|
|
31
|
-
lastName?: string;
|
|
32
|
-
emailVerified: boolean;
|
|
33
|
-
};
|
|
34
|
-
expiresAt: string;
|
|
35
|
-
requiresVerification?: boolean;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
interface AuthStatus {
|
|
39
|
-
isLoggedIn: boolean;
|
|
40
|
-
customer?: {
|
|
41
|
-
id: string;
|
|
42
|
-
email: string;
|
|
43
|
-
firstName?: string;
|
|
44
|
-
lastName?: string;
|
|
45
|
-
phone?: string;
|
|
46
|
-
emailVerified: boolean;
|
|
47
|
-
};
|
|
48
|
-
error?: string;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
interface VerifyEmailResult {
|
|
52
|
-
verified: boolean;
|
|
53
|
-
message?: string;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async function handleResponse<T>(response: Response): Promise<T> {
|
|
57
|
-
const data = await response.json();
|
|
58
|
-
if (!response.ok) {
|
|
59
|
-
throw new Error(data.message || data.error || `Request failed (${response.status})`);
|
|
60
|
-
}
|
|
61
|
-
return data as T;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Login via BFF proxy. The proxy sets the httpOnly cookie on success.
|
|
66
|
-
*/
|
|
67
|
-
export async function proxyLogin(email: string, password: string): Promise<LoginResult> {
|
|
68
|
-
const response = await fetch(`/api/store/api/vc/${CONNECTION_ID}/customers/login`, {
|
|
69
|
-
method: 'POST',
|
|
70
|
-
headers: CSRF_HEADERS,
|
|
71
|
-
body: JSON.stringify({ email, password }),
|
|
72
|
-
});
|
|
73
|
-
return handleResponse<LoginResult>(response);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Register via BFF proxy. The proxy sets the httpOnly cookie on success.
|
|
78
|
-
*/
|
|
79
|
-
export async function proxyRegister(data: {
|
|
80
|
-
firstName: string;
|
|
81
|
-
lastName: string;
|
|
82
|
-
email: string;
|
|
83
|
-
password: string;
|
|
84
|
-
acceptsMarketing?: boolean;
|
|
85
|
-
}): Promise<RegisterResult> {
|
|
86
|
-
const response = await fetch(`/api/store/api/vc/${CONNECTION_ID}/customers/register`, {
|
|
87
|
-
method: 'POST',
|
|
88
|
-
headers: CSRF_HEADERS,
|
|
89
|
-
body: JSON.stringify(data),
|
|
90
|
-
});
|
|
91
|
-
return handleResponse<RegisterResult>(response);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Check auth status. Reads httpOnly cookie server-side and validates with backend.
|
|
96
|
-
*/
|
|
97
|
-
export async function checkAuthStatus(): Promise<AuthStatus> {
|
|
98
|
-
const response = await fetch('/api/auth/me');
|
|
99
|
-
return response.json();
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Logout. Clears httpOnly auth cookies server-side.
|
|
104
|
-
*/
|
|
105
|
-
export async function proxyLogout(): Promise<void> {
|
|
106
|
-
await fetch('/api/auth/logout', {
|
|
107
|
-
method: 'POST',
|
|
108
|
-
headers: { 'X-Requested-With': 'brainerce' },
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Verify email via BFF proxy. The auth token is in the httpOnly cookie (set during login/register).
|
|
114
|
-
* The proxy adds the Authorization header automatically.
|
|
115
|
-
*/
|
|
116
|
-
export async function proxyVerifyEmail(code: string): Promise<VerifyEmailResult> {
|
|
117
|
-
const response = await fetch(`/api/store/api/vc/${CONNECTION_ID}/customers/verify-email`, {
|
|
118
|
-
method: 'POST',
|
|
119
|
-
headers: CSRF_HEADERS,
|
|
120
|
-
body: JSON.stringify({ code }),
|
|
121
|
-
});
|
|
122
|
-
return handleResponse<VerifyEmailResult>(response);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Resend verification email via BFF proxy.
|
|
127
|
-
* Uses the auth token from the httpOnly cookie.
|
|
128
|
-
*/
|
|
129
|
-
export async function proxyResendVerification(): Promise<{ message: string }> {
|
|
130
|
-
const response = await fetch(`/api/store/api/vc/${CONNECTION_ID}/customers/resend-verification`, {
|
|
131
|
-
method: 'POST',
|
|
132
|
-
headers: CSRF_HEADERS,
|
|
133
|
-
});
|
|
134
|
-
return handleResponse<{ message: string }>(response);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Reset password via BFF proxy.
|
|
139
|
-
* The reset token is in an httpOnly cookie (set by /api/auth/reset-callback when the user
|
|
140
|
-
* clicked the email link). The proxy reads it server-side — the token never reaches client JS.
|
|
141
|
-
*/
|
|
142
|
-
export async function proxyResetPassword(newPassword: string): Promise<{ message: string }> {
|
|
143
|
-
const response = await fetch('/api/auth/reset-password', {
|
|
144
|
-
method: 'POST',
|
|
145
|
-
headers: CSRF_HEADERS,
|
|
146
|
-
body: JSON.stringify({ newPassword }),
|
|
147
|
-
});
|
|
148
|
-
return handleResponse<{ message: string }>(response);
|
|
149
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Client-side auth helpers that call the BFF proxy API routes.
|
|
3
|
+
* All mutating requests include the CSRF header.
|
|
4
|
+
* The token is managed server-side via httpOnly cookies — never exposed to JS.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const CONNECTION_ID = process.env.NEXT_PUBLIC_BRAINERCE_CONNECTION_ID || '';
|
|
8
|
+
|
|
9
|
+
const CSRF_HEADERS: Record<string, string> = {
|
|
10
|
+
'Content-Type': 'application/json',
|
|
11
|
+
'X-Requested-With': 'brainerce',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
interface LoginResult {
|
|
15
|
+
customer: {
|
|
16
|
+
id: string;
|
|
17
|
+
email: string;
|
|
18
|
+
firstName?: string;
|
|
19
|
+
lastName?: string;
|
|
20
|
+
emailVerified: boolean;
|
|
21
|
+
};
|
|
22
|
+
expiresAt: string;
|
|
23
|
+
requiresVerification?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface RegisterResult {
|
|
27
|
+
customer: {
|
|
28
|
+
id: string;
|
|
29
|
+
email: string;
|
|
30
|
+
firstName?: string;
|
|
31
|
+
lastName?: string;
|
|
32
|
+
emailVerified: boolean;
|
|
33
|
+
};
|
|
34
|
+
expiresAt: string;
|
|
35
|
+
requiresVerification?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface AuthStatus {
|
|
39
|
+
isLoggedIn: boolean;
|
|
40
|
+
customer?: {
|
|
41
|
+
id: string;
|
|
42
|
+
email: string;
|
|
43
|
+
firstName?: string;
|
|
44
|
+
lastName?: string;
|
|
45
|
+
phone?: string;
|
|
46
|
+
emailVerified: boolean;
|
|
47
|
+
};
|
|
48
|
+
error?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface VerifyEmailResult {
|
|
52
|
+
verified: boolean;
|
|
53
|
+
message?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function handleResponse<T>(response: Response): Promise<T> {
|
|
57
|
+
const data = await response.json();
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
throw new Error(data.message || data.error || `Request failed (${response.status})`);
|
|
60
|
+
}
|
|
61
|
+
return data as T;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Login via BFF proxy. The proxy sets the httpOnly cookie on success.
|
|
66
|
+
*/
|
|
67
|
+
export async function proxyLogin(email: string, password: string): Promise<LoginResult> {
|
|
68
|
+
const response = await fetch(`/api/store/api/vc/${CONNECTION_ID}/customers/login`, {
|
|
69
|
+
method: 'POST',
|
|
70
|
+
headers: CSRF_HEADERS,
|
|
71
|
+
body: JSON.stringify({ email, password }),
|
|
72
|
+
});
|
|
73
|
+
return handleResponse<LoginResult>(response);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Register via BFF proxy. The proxy sets the httpOnly cookie on success.
|
|
78
|
+
*/
|
|
79
|
+
export async function proxyRegister(data: {
|
|
80
|
+
firstName: string;
|
|
81
|
+
lastName: string;
|
|
82
|
+
email: string;
|
|
83
|
+
password: string;
|
|
84
|
+
acceptsMarketing?: boolean;
|
|
85
|
+
}): Promise<RegisterResult> {
|
|
86
|
+
const response = await fetch(`/api/store/api/vc/${CONNECTION_ID}/customers/register`, {
|
|
87
|
+
method: 'POST',
|
|
88
|
+
headers: CSRF_HEADERS,
|
|
89
|
+
body: JSON.stringify(data),
|
|
90
|
+
});
|
|
91
|
+
return handleResponse<RegisterResult>(response);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Check auth status. Reads httpOnly cookie server-side and validates with backend.
|
|
96
|
+
*/
|
|
97
|
+
export async function checkAuthStatus(): Promise<AuthStatus> {
|
|
98
|
+
const response = await fetch('/api/auth/me');
|
|
99
|
+
return response.json();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Logout. Clears httpOnly auth cookies server-side.
|
|
104
|
+
*/
|
|
105
|
+
export async function proxyLogout(): Promise<void> {
|
|
106
|
+
await fetch('/api/auth/logout', {
|
|
107
|
+
method: 'POST',
|
|
108
|
+
headers: { 'X-Requested-With': 'brainerce' },
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Verify email via BFF proxy. The auth token is in the httpOnly cookie (set during login/register).
|
|
114
|
+
* The proxy adds the Authorization header automatically.
|
|
115
|
+
*/
|
|
116
|
+
export async function proxyVerifyEmail(code: string): Promise<VerifyEmailResult> {
|
|
117
|
+
const response = await fetch(`/api/store/api/vc/${CONNECTION_ID}/customers/verify-email`, {
|
|
118
|
+
method: 'POST',
|
|
119
|
+
headers: CSRF_HEADERS,
|
|
120
|
+
body: JSON.stringify({ code }),
|
|
121
|
+
});
|
|
122
|
+
return handleResponse<VerifyEmailResult>(response);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Resend verification email via BFF proxy.
|
|
127
|
+
* Uses the auth token from the httpOnly cookie.
|
|
128
|
+
*/
|
|
129
|
+
export async function proxyResendVerification(): Promise<{ message: string }> {
|
|
130
|
+
const response = await fetch(`/api/store/api/vc/${CONNECTION_ID}/customers/resend-verification`, {
|
|
131
|
+
method: 'POST',
|
|
132
|
+
headers: CSRF_HEADERS,
|
|
133
|
+
});
|
|
134
|
+
return handleResponse<{ message: string }>(response);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Reset password via BFF proxy.
|
|
139
|
+
* The reset token is in an httpOnly cookie (set by /api/auth/reset-callback when the user
|
|
140
|
+
* clicked the email link). The proxy reads it server-side — the token never reaches client JS.
|
|
141
|
+
*/
|
|
142
|
+
export async function proxyResetPassword(newPassword: string): Promise<{ message: string }> {
|
|
143
|
+
const response = await fetch('/api/auth/reset-password', {
|
|
144
|
+
method: 'POST',
|
|
145
|
+
headers: CSRF_HEADERS,
|
|
146
|
+
body: JSON.stringify({ newPassword }),
|
|
147
|
+
});
|
|
148
|
+
return handleResponse<{ message: string }>(response);
|
|
149
|
+
}
|
|
@@ -15,6 +15,15 @@ export function getClient(): BrainerceClient {
|
|
|
15
15
|
}
|
|
16
16
|
return clientInstance;
|
|
17
17
|
}
|
|
18
|
+
<% if (i18nEnabled) { %>
|
|
19
|
+
|
|
20
|
+
/** Initialize client with a specific locale for translated content */
|
|
21
|
+
export function initClientWithLocale(locale: string): BrainerceClient {
|
|
22
|
+
const client = getClient();
|
|
23
|
+
client.setLocale(locale);
|
|
24
|
+
return client;
|
|
25
|
+
}
|
|
26
|
+
<% } %>
|
|
18
27
|
|
|
19
28
|
// Cart ID helpers (not a security token — safe in localStorage)
|
|
20
29
|
const CART_ID_KEY = 'brainerce_cart_id';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<% if (i18nEnabled) { %>
|
|
2
|
+
'use client';
|
|
3
|
+
|
|
4
|
+
import { createContext, useContext } from 'react';
|
|
5
|
+
|
|
6
|
+
type Messages = Record<string, Record<string, string>>;
|
|
7
|
+
|
|
8
|
+
const MessagesContext = createContext<Messages>({});
|
|
9
|
+
|
|
10
|
+
export { MessagesContext };
|
|
11
|
+
|
|
12
|
+
export function useTranslations(namespace: string) {
|
|
13
|
+
const messages = useContext(MessagesContext);
|
|
14
|
+
const ns = (messages[namespace] || {}) as Record<string, string>;
|
|
15
|
+
return function t(key: string): string {
|
|
16
|
+
return ns[key] || `${namespace}.${key}`;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
<% } else { %>
|
|
20
|
+
import { messages } from '@/i18n';
|
|
21
|
+
|
|
22
|
+
type Messages = typeof messages;
|
|
23
|
+
type Namespace = keyof Messages;
|
|
24
|
+
|
|
25
|
+
export function useTranslations<N extends Namespace>(namespace: N) {
|
|
26
|
+
const ns = messages[namespace] as Record<string, string>;
|
|
27
|
+
return function t(key: keyof Messages[N]): string {
|
|
28
|
+
return ns[key as string] || `${String(namespace)}.${key as string}`;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
<% } %>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<% if (i18nEnabled) { %>
|
|
2
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
3
|
+
|
|
4
|
+
const TOKEN_COOKIE = 'brainerce_customer_token';
|
|
5
|
+
const PROTECTED_PATHS = ['/account'];
|
|
6
|
+
const supportedLocales = <%- supportedLocales %>;
|
|
7
|
+
const defaultLocale = '<%= defaultLocale %>';
|
|
8
|
+
|
|
9
|
+
function getLocaleFromPath(pathname: string): string | null {
|
|
10
|
+
const segment = pathname.split('/')[1];
|
|
11
|
+
return supportedLocales.includes(segment) ? segment : null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function middleware(request: NextRequest) {
|
|
15
|
+
const { pathname } = request.nextUrl;
|
|
16
|
+
|
|
17
|
+
// Skip static files and API routes
|
|
18
|
+
if (
|
|
19
|
+
pathname.startsWith('/api/') ||
|
|
20
|
+
pathname.startsWith('/_next/') ||
|
|
21
|
+
pathname.includes('.')
|
|
22
|
+
) {
|
|
23
|
+
return NextResponse.next();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const pathnameLocale = getLocaleFromPath(pathname);
|
|
27
|
+
|
|
28
|
+
// Redirect to default locale if no locale prefix
|
|
29
|
+
if (!pathnameLocale) {
|
|
30
|
+
const url = request.nextUrl.clone();
|
|
31
|
+
url.pathname = `/${defaultLocale}${pathname}`;
|
|
32
|
+
return NextResponse.redirect(url);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Auth protection (with locale prefix)
|
|
36
|
+
const pathWithoutLocale = pathname.replace(`/${pathnameLocale}`, '') || '/';
|
|
37
|
+
const isProtected = PROTECTED_PATHS.some((p) => pathWithoutLocale.startsWith(p));
|
|
38
|
+
if (isProtected) {
|
|
39
|
+
const token = request.cookies.get(TOKEN_COOKIE);
|
|
40
|
+
if (!token?.value) {
|
|
41
|
+
const loginUrl = new URL(`/${pathnameLocale}/login`, request.url);
|
|
42
|
+
return NextResponse.redirect(loginUrl);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Set locale header for server components
|
|
47
|
+
const response = NextResponse.next();
|
|
48
|
+
response.headers.set('x-locale', pathnameLocale);
|
|
49
|
+
return response;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const config = {
|
|
53
|
+
matcher: ['/((?!_next|api|.*\\..*).*)'],
|
|
54
|
+
};
|
|
55
|
+
<% } else { %>
|
|
56
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
57
|
+
|
|
58
|
+
const TOKEN_COOKIE = 'brainerce_customer_token';
|
|
59
|
+
|
|
60
|
+
/** Routes that require customer authentication */
|
|
61
|
+
const PROTECTED_PATHS = ['/account'];
|
|
62
|
+
|
|
63
|
+
export function middleware(request: NextRequest) {
|
|
64
|
+
const { pathname } = request.nextUrl;
|
|
65
|
+
const isProtected = PROTECTED_PATHS.some((p) => pathname.startsWith(p));
|
|
66
|
+
|
|
67
|
+
if (isProtected) {
|
|
68
|
+
const token = request.cookies.get(TOKEN_COOKIE);
|
|
69
|
+
if (!token?.value) {
|
|
70
|
+
const loginUrl = new URL('/login', request.url);
|
|
71
|
+
return NextResponse.redirect(loginUrl);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return NextResponse.next();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const config = {
|
|
79
|
+
matcher: ['/account/:path*'],
|
|
80
|
+
};
|
|
81
|
+
<% } %>
|