@brainerce/mcp-server 2.4.0 → 2.7.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/bin/http.js +970 -236
- package/dist/bin/stdio.js +970 -236
- package/dist/index.d.mts +10 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +970 -224
- package/dist/index.mjs +970 -214
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __create = Object.create;
|
|
3
2
|
var __defProp = Object.defineProperty;
|
|
4
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
7
5
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
6
|
var __export = (target, all) => {
|
|
9
7
|
for (var name in all)
|
|
@@ -17,14 +15,6 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
17
15
|
}
|
|
18
16
|
return to;
|
|
19
17
|
};
|
|
20
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
-
mod
|
|
27
|
-
));
|
|
28
18
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
19
|
|
|
30
20
|
// src/index.ts
|
|
@@ -197,7 +187,7 @@ import {
|
|
|
197
187
|
getVariantPrice, getVariantOptions, getStockStatus,
|
|
198
188
|
getCartItemName, getCartItemImage,
|
|
199
189
|
getDescriptionContent, isHtmlDescription,
|
|
200
|
-
getProductMetafieldValue,
|
|
190
|
+
getProductMetafieldValue, getProductCustomizationFields,
|
|
201
191
|
} from 'brainerce';
|
|
202
192
|
\`\`\``;
|
|
203
193
|
}
|
|
@@ -239,6 +229,7 @@ async function startCheckout() {
|
|
|
239
229
|
4. Start checkout session (\`startGuestCheckout()\` or \`createCheckout()\`)
|
|
240
230
|
5. Set shipping address (includes required email) \u2192 \`setShippingAddress()\`
|
|
241
231
|
6. Select shipping method \u2192 \`selectShippingMethod()\`
|
|
232
|
+
6b. (Optional) Set checkout custom fields \u2192 \`setCheckoutCustomFields()\` \u2014 surcharges auto-calculated
|
|
242
233
|
7. Create payment intent \u2192 \`createPaymentIntent()\` \u2192 returns \`{ clientSecret, provider }\`
|
|
243
234
|
8. Branch on \`provider\`: \`'stripe'\` \u2192 Stripe Elements, \`'grow'\` \u2192 iframe, \`'paypal'\` \u2192 PayPal Buttons
|
|
244
235
|
9. **Order is created AUTOMATICALLY after payment succeeds (via webhook) \u2014 for ALL providers!**
|
|
@@ -621,6 +612,35 @@ const material = getProductMetafieldValue(product, 'material');
|
|
|
621
612
|
|
|
622
613
|
**Metafield fields:** \`definitionName\` (display label), \`definitionKey\` (lookup key), \`value\`, \`type\` (IMAGE, GALLERY, URL, COLOR, BOOLEAN, DATE, DATETIME, TEXT, TEXTAREA, NUMBER, DIMENSION, WEIGHT, JSON)
|
|
623
614
|
|
|
615
|
+
### Product Customization Fields (Customer Input)
|
|
616
|
+
|
|
617
|
+
Some products allow customers to provide input at purchase time (e.g., text on a cake, upload a logo). Check \`product.customizationFields\` and render input fields accordingly.
|
|
618
|
+
|
|
619
|
+
\`\`\`typescript
|
|
620
|
+
import { getProductCustomizationFields } from 'brainerce';
|
|
621
|
+
import type { ProductCustomizationField } from 'brainerce';
|
|
622
|
+
|
|
623
|
+
const fields = getProductCustomizationFields(product);
|
|
624
|
+
|
|
625
|
+
// Render input for each field based on field.type:
|
|
626
|
+
// TEXT/TEXTAREA \u2192 text input, NUMBER \u2192 number input, BOOLEAN \u2192 checkbox,
|
|
627
|
+
// COLOR \u2192 color picker, DATE \u2192 date picker, IMAGE \u2192 file upload, etc.
|
|
628
|
+
// Check field.required, field.minLength, field.maxLength, field.enumValues for validation.
|
|
629
|
+
|
|
630
|
+
// Pass customer values in metadata when adding to cart:
|
|
631
|
+
await client.addToCart(cartId, {
|
|
632
|
+
productId: product.id,
|
|
633
|
+
quantity: 1,
|
|
634
|
+
metadata: { cake_text: 'Happy Birthday!' },
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
// For IMAGE fields, upload first:
|
|
638
|
+
const { url } = await client.uploadCustomizationFile(file);
|
|
639
|
+
// Then use the URL in metadata: metadata: { logo: url }
|
|
640
|
+
\`\`\`
|
|
641
|
+
|
|
642
|
+
**Customization field properties:** \`key\`, \`name\` (display label), \`type\`, \`required\`, \`minLength\`, \`maxLength\`, \`minValue\`, \`maxValue\`, \`enumValues\`, \`defaultValue\`, \`position\`
|
|
643
|
+
|
|
624
644
|
### Downloadable / Digital Products
|
|
625
645
|
|
|
626
646
|
Products with \`isDownloadable: true\` are digital products. Show a digital badge instead of stock badge, list included files, and note that download links appear after purchase.
|
|
@@ -807,9 +827,14 @@ const { bundles } = await client.getCartBundles(cartId);
|
|
|
807
827
|
// bundles[].bundleProduct \u2014 the product to offer
|
|
808
828
|
// bundles[].originalPrice / bundles[].discountedPrice \u2014 prices
|
|
809
829
|
// bundles[].discountType ('PERCENTAGE' | 'FIXED_AMOUNT') + discountValue
|
|
830
|
+
// bundles[].requiresVariantSelection \u2014 true if customer must pick a variant
|
|
831
|
+
// bundles[].lockedVariant \u2014 set if admin pre-selected a specific variant
|
|
832
|
+
// bundles[].bundleProduct.variants \u2014 available variants (only when requiresVariantSelection)
|
|
810
833
|
|
|
811
834
|
// Add a bundle to cart (applies the discount automatically):
|
|
812
835
|
await client.addBundleToCart(cartId, bundleOfferId);
|
|
836
|
+
// For products with variants \u2014 pass the selected variantId:
|
|
837
|
+
await client.addBundleToCart(cartId, bundleOfferId, selectedVariantId);
|
|
813
838
|
// Remove a bundle from cart:
|
|
814
839
|
await client.removeBundleFromCart(cartId, bundleOfferId);
|
|
815
840
|
// Detect already-added bundles: check cart.items for metadata?.isBundleItem === true
|
|
@@ -834,9 +859,14 @@ const { bumps } = await client.getCheckoutBumps(checkoutId);
|
|
|
834
859
|
// bumps[].bumpProduct \u2014 product to offer
|
|
835
860
|
// bumps[].originalPrice / discountedPrice \u2014 with optional discount
|
|
836
861
|
// bumps[].title / description \u2014 display text
|
|
862
|
+
// bumps[].requiresVariantSelection \u2014 true if customer must pick a variant
|
|
863
|
+
// bumps[].lockedVariant \u2014 set if admin pre-selected a specific variant
|
|
864
|
+
// bumps[].bumpProduct.variants \u2014 available variants (only when requiresVariantSelection)
|
|
837
865
|
|
|
838
866
|
// Add a bump to cart:
|
|
839
867
|
await client.addOrderBump(cartId, bumpId);
|
|
868
|
+
// For products with variants \u2014 pass the selected variantId:
|
|
869
|
+
await client.addOrderBump(cartId, bumpId, selectedVariantId);
|
|
840
870
|
// Remove a bump:
|
|
841
871
|
await client.removeOrderBump(cartId, bumpId);
|
|
842
872
|
// Detect already-added bumps: check cart.items for metadata?.isOrderBump === true
|
|
@@ -861,10 +891,11 @@ Always check the flag before rendering: \`if (storeInfo?.upsell?.featureName !==
|
|
|
861
891
|
| CartRecommendationSection | products/ | Cross-sell grid at bottom of cart page |
|
|
862
892
|
| FreeShippingBar | cart/ | Progress bar toward free shipping threshold |
|
|
863
893
|
| CartUpgradeBanner | cart/ | Inline "Upgrade to X for +$Y" per cart item |
|
|
864
|
-
| CartBundleOfferCard | cart/ | Discounted bundle offer card in cart |
|
|
865
|
-
| OrderBumpCard | checkout/ | Checkbox add-on card in checkout sidebar |
|
|
894
|
+
| CartBundleOfferCard | cart/ | Discounted bundle offer card in cart (with inline variant selector for variable products) |
|
|
895
|
+
| OrderBumpCard | checkout/ | Checkbox add-on card in checkout sidebar (with inline variant selector for variable products) |
|
|
866
896
|
|
|
867
|
-
ProductRecommendation: \`id\`, \`name\`, \`slug\`, \`basePrice\`, \`salePrice\`, \`images\`, \`type\`, \`inventory\`, \`relationType
|
|
897
|
+
ProductRecommendation: \`id\`, \`name\`, \`slug\`, \`basePrice\`, \`salePrice\`, \`images\`, \`type\`, \`inventory\`, \`relationType\`, \`variants?\` (when variant selection needed).
|
|
898
|
+
OrderBump/CartBundleOffer: includes \`requiresVariantSelection\` (boolean) and \`lockedVariant?\` (\`{ id, name, attributes }\`).`;
|
|
868
899
|
}
|
|
869
900
|
function getInventorySection() {
|
|
870
901
|
return `## Inventory, Stock Display & Reservation Countdown
|
|
@@ -2004,6 +2035,15 @@ export function getClient(): BrainerceClient {
|
|
|
2004
2035
|
}
|
|
2005
2036
|
return clientInstance;
|
|
2006
2037
|
}
|
|
2038
|
+
<% if (i18nEnabled) { %>
|
|
2039
|
+
|
|
2040
|
+
/** Initialize client with a specific locale for translated content */
|
|
2041
|
+
export function initClientWithLocale(locale: string): BrainerceClient {
|
|
2042
|
+
const client = getClient();
|
|
2043
|
+
client.setLocale(locale);
|
|
2044
|
+
return client;
|
|
2045
|
+
}
|
|
2046
|
+
<% } %>
|
|
2007
2047
|
|
|
2008
2048
|
// Cart ID helpers (not a security token \u2014 safe in localStorage)
|
|
2009
2049
|
const CART_ID_KEY = 'brainerce_cart_id';
|
|
@@ -2202,6 +2242,10 @@ import type { StoreInfo, Cart, CustomerProfile } from 'brainerce';
|
|
|
2202
2242
|
import { getCartTotals } from 'brainerce';
|
|
2203
2243
|
import { getClient, initClient, setStoredCartId } from '@/lib/brainerce';
|
|
2204
2244
|
import { checkAuthStatus, proxyLogout } from '@/lib/auth';
|
|
2245
|
+
<% if (i18nEnabled) { %>
|
|
2246
|
+
import { MessagesContext } from '@/lib/translations';
|
|
2247
|
+
import { getMessages, defaultLocale } from '@/i18n';
|
|
2248
|
+
<% } %>
|
|
2205
2249
|
|
|
2206
2250
|
// ---- Store Info Context ----
|
|
2207
2251
|
interface StoreInfoContextValue {
|
|
@@ -2261,7 +2305,11 @@ export function useCart() {
|
|
|
2261
2305
|
}
|
|
2262
2306
|
|
|
2263
2307
|
// ---- Provider Component ----
|
|
2308
|
+
<% if (i18nEnabled) { %>
|
|
2309
|
+
export function StoreProvider({ children, locale }: { children: React.ReactNode; locale?: string }) {
|
|
2310
|
+
<% } else { %>
|
|
2264
2311
|
export function StoreProvider({ children }: { children: React.ReactNode }) {
|
|
2312
|
+
<% } %>
|
|
2265
2313
|
const [storeInfo, setStoreInfo] = useState<StoreInfo | null>(null);
|
|
2266
2314
|
const [storeLoading, setStoreLoading] = useState(true);
|
|
2267
2315
|
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
|
@@ -2269,6 +2317,9 @@ export function StoreProvider({ children }: { children: React.ReactNode }) {
|
|
|
2269
2317
|
const [authLoading, setAuthLoading] = useState(true);
|
|
2270
2318
|
const [cart, setCart] = useState<Cart | null>(null);
|
|
2271
2319
|
const [cartLoading, setCartLoading] = useState(true);
|
|
2320
|
+
<% if (i18nEnabled) { %>
|
|
2321
|
+
const [messages, setMessages] = useState<Record<string, Record<string, string>>>({});
|
|
2322
|
+
<% } %>
|
|
2272
2323
|
|
|
2273
2324
|
// Check auth status via httpOnly cookie (server-side validation)
|
|
2274
2325
|
const refreshAuth = useCallback(async () => {
|
|
@@ -2287,6 +2338,16 @@ export function StoreProvider({ children }: { children: React.ReactNode }) {
|
|
|
2287
2338
|
// Initialize client, check auth, and fetch store info
|
|
2288
2339
|
useEffect(() => {
|
|
2289
2340
|
const client = initClient();
|
|
2341
|
+
<% if (i18nEnabled) { %>
|
|
2342
|
+
|
|
2343
|
+
// Set locale on SDK client for translated product content
|
|
2344
|
+
if (locale) {
|
|
2345
|
+
client.setLocale(locale);
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
// Load UI message strings for current locale
|
|
2349
|
+
getMessages(locale || defaultLocale).then(setMessages);
|
|
2350
|
+
<% } %>
|
|
2290
2351
|
|
|
2291
2352
|
// Optimistic check: if brainerce_logged_in cookie exists, assume logged in
|
|
2292
2353
|
// while we validate the actual token server-side
|
|
@@ -2303,7 +2364,11 @@ export function StoreProvider({ children }: { children: React.ReactNode }) {
|
|
|
2303
2364
|
.then(setStoreInfo)
|
|
2304
2365
|
.catch(console.error)
|
|
2305
2366
|
.finally(() => setStoreLoading(false));
|
|
2367
|
+
<% if (i18nEnabled) { %>
|
|
2368
|
+
}, [refreshAuth, locale]);
|
|
2369
|
+
<% } else { %>
|
|
2306
2370
|
}, [refreshAuth]);
|
|
2371
|
+
<% } %>
|
|
2307
2372
|
|
|
2308
2373
|
// Cart management
|
|
2309
2374
|
const refreshCart = useCallback(async () => {
|
|
@@ -2356,6 +2421,21 @@ export function StoreProvider({ children }: { children: React.ReactNode }) {
|
|
|
2356
2421
|
|
|
2357
2422
|
const totals = cart ? getCartTotals(cart) : { subtotal: 0, discount: 0, shipping: 0, total: 0 };
|
|
2358
2423
|
|
|
2424
|
+
<% if (i18nEnabled) { %>
|
|
2425
|
+
return (
|
|
2426
|
+
<MessagesContext.Provider value={messages}>
|
|
2427
|
+
<StoreInfoContext.Provider value={{ storeInfo, loading: storeLoading }}>
|
|
2428
|
+
<AuthContext.Provider value={{ isLoggedIn, authLoading, customer, login, logout }}>
|
|
2429
|
+
<CartContext.Provider
|
|
2430
|
+
value={{ cart, cartLoading, refreshCart, itemCount, totals }}
|
|
2431
|
+
>
|
|
2432
|
+
{children}
|
|
2433
|
+
</CartContext.Provider>
|
|
2434
|
+
</AuthContext.Provider>
|
|
2435
|
+
</StoreInfoContext.Provider>
|
|
2436
|
+
</MessagesContext.Provider>
|
|
2437
|
+
);
|
|
2438
|
+
<% } else { %>
|
|
2359
2439
|
return (
|
|
2360
2440
|
<StoreInfoContext.Provider value={{ storeInfo, loading: storeLoading }}>
|
|
2361
2441
|
<AuthContext.Provider value={{ isLoggedIn, authLoading, customer, login, logout }}>
|
|
@@ -2367,6 +2447,7 @@ export function StoreProvider({ children }: { children: React.ReactNode }) {
|
|
|
2367
2447
|
</AuthContext.Provider>
|
|
2368
2448
|
</StoreInfoContext.Provider>
|
|
2369
2449
|
);
|
|
2450
|
+
<% } %>
|
|
2370
2451
|
}
|
|
2371
2452
|
`,
|
|
2372
2453
|
"src/app/page.tsx": `'use client';
|
|
@@ -2468,7 +2549,81 @@ export default function HomePage() {
|
|
|
2468
2549
|
);
|
|
2469
2550
|
}
|
|
2470
2551
|
`,
|
|
2471
|
-
"src/app/layout.tsx.ejs":
|
|
2552
|
+
"src/app/layout.tsx.ejs": `<% if (i18nEnabled) { %>
|
|
2553
|
+
import type { Metadata } from 'next';
|
|
2554
|
+
<%- fontImport %>
|
|
2555
|
+
import { StoreProvider } from '@/providers/store-provider';
|
|
2556
|
+
import { Header } from '@/components/layout/header';
|
|
2557
|
+
import { Footer } from '@/components/layout/footer';
|
|
2558
|
+
import { getDirection, supportedLocales } from '@/i18n';
|
|
2559
|
+
import '../globals.css';
|
|
2560
|
+
|
|
2561
|
+
<%- fontVariable %>
|
|
2562
|
+
|
|
2563
|
+
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com';
|
|
2564
|
+
|
|
2565
|
+
export const metadata: Metadata = {
|
|
2566
|
+
metadataBase: new URL(baseUrl),
|
|
2567
|
+
title: {
|
|
2568
|
+
default: 'My Store',
|
|
2569
|
+
template: \`%s | My Store\`,
|
|
2570
|
+
},
|
|
2571
|
+
description: 'My Store',
|
|
2572
|
+
alternates: {
|
|
2573
|
+
canonical: '/',
|
|
2574
|
+
},
|
|
2575
|
+
openGraph: {
|
|
2576
|
+
siteName: 'My Store',
|
|
2577
|
+
type: 'website',
|
|
2578
|
+
},
|
|
2579
|
+
robots: {
|
|
2580
|
+
index: true,
|
|
2581
|
+
follow: true,
|
|
2582
|
+
},
|
|
2583
|
+
};
|
|
2584
|
+
|
|
2585
|
+
const organizationJsonLd = {
|
|
2586
|
+
'@context': 'https://schema.org',
|
|
2587
|
+
'@type': 'Organization',
|
|
2588
|
+
name: 'My Store',
|
|
2589
|
+
url: baseUrl,
|
|
2590
|
+
};
|
|
2591
|
+
|
|
2592
|
+
export function generateStaticParams() {
|
|
2593
|
+
return supportedLocales.map((locale) => ({ locale }));
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
export default function RootLayout({
|
|
2597
|
+
children,
|
|
2598
|
+
params,
|
|
2599
|
+
}: {
|
|
2600
|
+
children: React.ReactNode;
|
|
2601
|
+
params: { locale: string };
|
|
2602
|
+
}) {
|
|
2603
|
+
const dir = getDirection(params.locale);
|
|
2604
|
+
|
|
2605
|
+
return (
|
|
2606
|
+
<html lang={params.locale} dir={dir}>
|
|
2607
|
+
<head>
|
|
2608
|
+
<script
|
|
2609
|
+
type="application/ld+json"
|
|
2610
|
+
dangerouslySetInnerHTML={{ __html: JSON.stringify(organizationJsonLd) }}
|
|
2611
|
+
/>
|
|
2612
|
+
</head>
|
|
2613
|
+
<body className={font.className}>
|
|
2614
|
+
<StoreProvider locale={params.locale}>
|
|
2615
|
+
<div className="min-h-screen flex flex-col">
|
|
2616
|
+
<Header />
|
|
2617
|
+
<main className="flex-1">{children}</main>
|
|
2618
|
+
<Footer />
|
|
2619
|
+
</div>
|
|
2620
|
+
</StoreProvider>
|
|
2621
|
+
</body>
|
|
2622
|
+
</html>
|
|
2623
|
+
);
|
|
2624
|
+
}
|
|
2625
|
+
<% } else { %>
|
|
2626
|
+
import type { Metadata } from 'next';
|
|
2472
2627
|
<%- fontImport %>
|
|
2473
2628
|
import { StoreProvider } from '@/providers/store-provider';
|
|
2474
2629
|
import { Header } from '@/components/layout/header';
|
|
@@ -2532,6 +2687,7 @@ export default function RootLayout({
|
|
|
2532
2687
|
</html>
|
|
2533
2688
|
);
|
|
2534
2689
|
}
|
|
2690
|
+
<% } %>
|
|
2535
2691
|
`,
|
|
2536
2692
|
"src/app/products/page.tsx": `'use client';
|
|
2537
2693
|
|
|
@@ -3432,13 +3588,13 @@ function CheckoutContent() {
|
|
|
3432
3588
|
}, [checkout?.id, storeInfo?.upsell?.checkoutOrderBumpEnabled]);
|
|
3433
3589
|
|
|
3434
3590
|
// Handle bump toggle
|
|
3435
|
-
async function handleBumpToggle(bumpId: string, add: boolean) {
|
|
3591
|
+
async function handleBumpToggle(bumpId: string, add: boolean, variantId?: string) {
|
|
3436
3592
|
if (!cart?.id || bumpLoading) return;
|
|
3437
3593
|
try {
|
|
3438
3594
|
setBumpLoading(bumpId);
|
|
3439
3595
|
const client = getClient();
|
|
3440
3596
|
if (add) {
|
|
3441
|
-
await client.addOrderBump(cart.id, bumpId);
|
|
3597
|
+
await client.addOrderBump(cart.id, bumpId, variantId);
|
|
3442
3598
|
setAddedBumpIds((prev) => new Set([...prev, bumpId]));
|
|
3443
3599
|
} else {
|
|
3444
3600
|
await client.removeOrderBump(cart.id, bumpId);
|
|
@@ -4136,6 +4292,23 @@ function OrderConfirmationContent() {
|
|
|
4136
4292
|
client.handlePaymentSuccess(checkoutId!);
|
|
4137
4293
|
await refreshCart();
|
|
4138
4294
|
|
|
4295
|
+
// For redirect-based payment providers (e.g. CardCom), the customer
|
|
4296
|
+
// returns with provider params in the URL (lowprofilecode, etc.).
|
|
4297
|
+
// Send these to the backend for server-side verification via the
|
|
4298
|
+
// provider's API (e.g. GetLpResult) \u2014 never trust URL params alone.
|
|
4299
|
+
const lowProfileCode =
|
|
4300
|
+
searchParams.get('lowprofilecode') || searchParams.get('LowProfileCode');
|
|
4301
|
+
if (lowProfileCode) {
|
|
4302
|
+
try {
|
|
4303
|
+
await client.confirmSdkPayment(checkoutId!, {
|
|
4304
|
+
paymentIntentId: lowProfileCode,
|
|
4305
|
+
});
|
|
4306
|
+
} catch (err) {
|
|
4307
|
+
console.warn('Redirect payment confirmation failed:', err);
|
|
4308
|
+
// Don't block \u2014 webhook may still process the payment
|
|
4309
|
+
}
|
|
4310
|
+
}
|
|
4311
|
+
|
|
4139
4312
|
const orderResult = await client.waitForOrder(checkoutId!, {
|
|
4140
4313
|
maxWaitMs: 30000,
|
|
4141
4314
|
});
|
|
@@ -5652,7 +5825,62 @@ export async function POST(request: NextRequest) {
|
|
|
5652
5825
|
return response;
|
|
5653
5826
|
}
|
|
5654
5827
|
`,
|
|
5655
|
-
"src/middleware.ts":
|
|
5828
|
+
"src/middleware.ts.ejs": `<% if (i18nEnabled) { %>
|
|
5829
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
5830
|
+
|
|
5831
|
+
const TOKEN_COOKIE = 'brainerce_customer_token';
|
|
5832
|
+
const PROTECTED_PATHS = ['/account'];
|
|
5833
|
+
const supportedLocales = <%- supportedLocales %>;
|
|
5834
|
+
const defaultLocale = '<%= defaultLocale %>';
|
|
5835
|
+
|
|
5836
|
+
function getLocaleFromPath(pathname: string): string | null {
|
|
5837
|
+
const segment = pathname.split('/')[1];
|
|
5838
|
+
return supportedLocales.includes(segment) ? segment : null;
|
|
5839
|
+
}
|
|
5840
|
+
|
|
5841
|
+
export function middleware(request: NextRequest) {
|
|
5842
|
+
const { pathname } = request.nextUrl;
|
|
5843
|
+
|
|
5844
|
+
// Skip static files and API routes
|
|
5845
|
+
if (
|
|
5846
|
+
pathname.startsWith('/api/') ||
|
|
5847
|
+
pathname.startsWith('/_next/') ||
|
|
5848
|
+
pathname.includes('.')
|
|
5849
|
+
) {
|
|
5850
|
+
return NextResponse.next();
|
|
5851
|
+
}
|
|
5852
|
+
|
|
5853
|
+
const pathnameLocale = getLocaleFromPath(pathname);
|
|
5854
|
+
|
|
5855
|
+
// Redirect to default locale if no locale prefix
|
|
5856
|
+
if (!pathnameLocale) {
|
|
5857
|
+
const url = request.nextUrl.clone();
|
|
5858
|
+
url.pathname = \`/\${defaultLocale}\${pathname}\`;
|
|
5859
|
+
return NextResponse.redirect(url);
|
|
5860
|
+
}
|
|
5861
|
+
|
|
5862
|
+
// Auth protection (with locale prefix)
|
|
5863
|
+
const pathWithoutLocale = pathname.replace(\`/\${pathnameLocale}\`, '') || '/';
|
|
5864
|
+
const isProtected = PROTECTED_PATHS.some((p) => pathWithoutLocale.startsWith(p));
|
|
5865
|
+
if (isProtected) {
|
|
5866
|
+
const token = request.cookies.get(TOKEN_COOKIE);
|
|
5867
|
+
if (!token?.value) {
|
|
5868
|
+
const loginUrl = new URL(\`/\${pathnameLocale}/login\`, request.url);
|
|
5869
|
+
return NextResponse.redirect(loginUrl);
|
|
5870
|
+
}
|
|
5871
|
+
}
|
|
5872
|
+
|
|
5873
|
+
// Set locale header for server components
|
|
5874
|
+
const response = NextResponse.next();
|
|
5875
|
+
response.headers.set('x-locale', pathnameLocale);
|
|
5876
|
+
return response;
|
|
5877
|
+
}
|
|
5878
|
+
|
|
5879
|
+
export const config = {
|
|
5880
|
+
matcher: ['/((?!_next|api|.*\\\\..*).*)'],
|
|
5881
|
+
};
|
|
5882
|
+
<% } else { %>
|
|
5883
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
5656
5884
|
|
|
5657
5885
|
const TOKEN_COOKIE = 'brainerce_customer_token';
|
|
5658
5886
|
|
|
@@ -5677,6 +5905,7 @@ export function middleware(request: NextRequest) {
|
|
|
5677
5905
|
export const config = {
|
|
5678
5906
|
matcher: ['/account/:path*'],
|
|
5679
5907
|
};
|
|
5908
|
+
<% } %>
|
|
5680
5909
|
`,
|
|
5681
5910
|
"src/components/products/product-card.tsx": `'use client';
|
|
5682
5911
|
|
|
@@ -7538,7 +7767,9 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
|
|
|
7538
7767
|
initialized.current = true;
|
|
7539
7768
|
|
|
7540
7769
|
const client = getClient();
|
|
7541
|
-
const
|
|
7770
|
+
const iframeSuccessUrl = \`\${window.location.origin}/payment-complete?checkout_id=\${checkoutId}\`;
|
|
7771
|
+
const iframeFailedUrl = \`\${window.location.origin}/payment-complete?checkout_id=\${checkoutId}&failed=true\`;
|
|
7772
|
+
const redirectSuccessUrl = \`\${window.location.origin}/order-confirmation?checkout_id=\${checkoutId}\`;
|
|
7542
7773
|
const cancelUrl = \`\${window.location.origin}/checkout?checkout_id=\${checkoutId}&canceled=true\`;
|
|
7543
7774
|
|
|
7544
7775
|
let sdkInitDone = false;
|
|
@@ -7699,8 +7930,19 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
|
|
|
7699
7930
|
});
|
|
7700
7931
|
|
|
7701
7932
|
// C) Create payment intent (starts wallet timer)
|
|
7702
|
-
|
|
7703
|
-
|
|
7933
|
+
// Wait for provider info so we can choose the right success URL:
|
|
7934
|
+
// iframe providers redirect inside the iframe to /payment-complete (postMessage),
|
|
7935
|
+
// redirect providers go straight to /order-confirmation.
|
|
7936
|
+
const intentPromise = providerPromise
|
|
7937
|
+
.then((providerSdk) => {
|
|
7938
|
+
const isIframe = providerSdk?.renderType === 'iframe';
|
|
7939
|
+
const successUrl = isIframe ? iframeSuccessUrl : redirectSuccessUrl;
|
|
7940
|
+
const failedUrl = isIframe ? iframeFailedUrl : cancelUrl;
|
|
7941
|
+
return client.createPaymentIntent(checkoutId, {
|
|
7942
|
+
successUrl,
|
|
7943
|
+
cancelUrl: failedUrl,
|
|
7944
|
+
});
|
|
7945
|
+
})
|
|
7704
7946
|
.then((intent) => {
|
|
7705
7947
|
setPaymentIntent(intent);
|
|
7706
7948
|
return intent;
|
|
@@ -7725,6 +7967,36 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
|
|
|
7725
7967
|
window.location.href = intent.clientSecret;
|
|
7726
7968
|
return;
|
|
7727
7969
|
}
|
|
7970
|
+
|
|
7971
|
+
// Iframe mode: listen for postMessage from the /payment-complete callback
|
|
7972
|
+
// page that loads inside the iframe after the provider redirects on completion.
|
|
7973
|
+
if (sdk.renderType === 'iframe') {
|
|
7974
|
+
const handleMessage = (event: MessageEvent) => {
|
|
7975
|
+
if (event.origin !== window.location.origin) return;
|
|
7976
|
+
if (event.data?.type !== 'brainerce:payment-complete') return;
|
|
7977
|
+
|
|
7978
|
+
const params = event.data.data as Record<string, string> | undefined;
|
|
7979
|
+
if (params?.failed === 'true') {
|
|
7980
|
+
setError(t('paymentError'));
|
|
7981
|
+
return;
|
|
7982
|
+
}
|
|
7983
|
+
|
|
7984
|
+
// Map provider-specific params to normalized format for
|
|
7985
|
+
// server-side verification (e.g. CardCom lowprofilecode \u2192 paymentIntentId)
|
|
7986
|
+
const lowProfileCode = params?.lowprofilecode || params?.LowProfileCode;
|
|
7987
|
+
const normalized: Record<string, unknown> = { ...params };
|
|
7988
|
+
if (lowProfileCode) {
|
|
7989
|
+
normalized.paymentIntentId = lowProfileCode;
|
|
7990
|
+
}
|
|
7991
|
+
|
|
7992
|
+
// Trigger server-side verification + order creation
|
|
7993
|
+
handleSuccess(normalized);
|
|
7994
|
+
};
|
|
7995
|
+
window.addEventListener('message', handleMessage);
|
|
7996
|
+
cleanups.push(() => window.removeEventListener('message', handleMessage));
|
|
7997
|
+
return;
|
|
7998
|
+
}
|
|
7999
|
+
|
|
7728
8000
|
if (sdk.renderType !== 'sdk-widget' || !sdk.globalName) return;
|
|
7729
8001
|
|
|
7730
8002
|
// Store for retryRender from onError callback
|
|
@@ -7875,15 +8147,45 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
|
|
|
7875
8147
|
|
|
7876
8148
|
if (sdk.renderType === 'iframe') {
|
|
7877
8149
|
return (
|
|
7878
|
-
|
|
7879
|
-
|
|
7880
|
-
|
|
7881
|
-
className="w-full
|
|
7882
|
-
|
|
7883
|
-
|
|
7884
|
-
|
|
7885
|
-
|
|
7886
|
-
|
|
8150
|
+
<>
|
|
8151
|
+
{/* Modal overlay */}
|
|
8152
|
+
<div className="fixed inset-0 z-50 flex items-start justify-center overflow-y-auto bg-black/50 py-6 backdrop-blur-sm">
|
|
8153
|
+
<div className="relative mx-4 w-full max-w-md rounded-2xl bg-white shadow-2xl">
|
|
8154
|
+
{/* Close button */}
|
|
8155
|
+
<button
|
|
8156
|
+
onClick={() => {
|
|
8157
|
+
window.location.href = \`/checkout?checkout_id=\${checkoutId}&canceled=true\`;
|
|
8158
|
+
}}
|
|
8159
|
+
className="absolute end-3 top-3 z-10 flex h-8 w-8 items-center justify-center rounded-full bg-white/80 text-gray-500 shadow-sm transition-colors hover:bg-gray-100 hover:text-gray-700"
|
|
8160
|
+
aria-label="Close"
|
|
8161
|
+
>
|
|
8162
|
+
<svg
|
|
8163
|
+
width="14"
|
|
8164
|
+
height="14"
|
|
8165
|
+
viewBox="0 0 14 14"
|
|
8166
|
+
fill="none"
|
|
8167
|
+
stroke="currentColor"
|
|
8168
|
+
strokeWidth="2"
|
|
8169
|
+
strokeLinecap="round"
|
|
8170
|
+
>
|
|
8171
|
+
<path d="M1 1l12 12M13 1L1 13" />
|
|
8172
|
+
</svg>
|
|
8173
|
+
</button>
|
|
8174
|
+
<iframe
|
|
8175
|
+
src={paymentIntent.clientSecret}
|
|
8176
|
+
className="w-full rounded-2xl border-0"
|
|
8177
|
+
style={{ height: '80vh' }}
|
|
8178
|
+
title={t('payment')}
|
|
8179
|
+
allow="payment"
|
|
8180
|
+
/>
|
|
8181
|
+
</div>
|
|
8182
|
+
</div>
|
|
8183
|
+
{/* Placeholder so the checkout layout doesn't collapse */}
|
|
8184
|
+
<div className={cn('flex flex-col items-center justify-center py-12', className)}>
|
|
8185
|
+
<LoadingSpinner size="lg" />
|
|
8186
|
+
<p className="text-muted-foreground mt-4 text-sm">{t('preparingPayment')}</p>
|
|
8187
|
+
</div>
|
|
8188
|
+
</>
|
|
7887
8189
|
);
|
|
7888
8190
|
}
|
|
7889
8191
|
|
|
@@ -10010,10 +10312,10 @@ export function CartUpgradeBanner({
|
|
|
10010
10312
|
`,
|
|
10011
10313
|
"src/components/cart/cart-bundle-offer.tsx": `'use client';
|
|
10012
10314
|
|
|
10013
|
-
import { useState } from 'react';
|
|
10315
|
+
import { useState, useMemo } from 'react';
|
|
10014
10316
|
import Image from 'next/image';
|
|
10015
10317
|
import type { CartBundleOffer as CartBundleOfferType } from 'brainerce';
|
|
10016
|
-
import { formatPrice } from 'brainerce';
|
|
10318
|
+
import { formatPrice, getVariantOptions } from 'brainerce';
|
|
10017
10319
|
import { useStoreInfo } from '@/providers/store-provider';
|
|
10018
10320
|
import { useTranslations } from '@/lib/translations';
|
|
10019
10321
|
import { cn } from '@/lib/utils';
|
|
@@ -10030,28 +10332,114 @@ export function CartBundleOfferCard({ offer, cartId, onAdd, className }: CartBun
|
|
|
10030
10332
|
const t = useTranslations('cart');
|
|
10031
10333
|
const currency = storeInfo?.currency || 'USD';
|
|
10032
10334
|
const [adding, setAdding] = useState(false);
|
|
10335
|
+
const [selectedVariantId, setSelectedVariantId] = useState<string | null>(null);
|
|
10033
10336
|
|
|
10034
10337
|
const product = offer.bundleProduct;
|
|
10338
|
+
const variants = product.variants;
|
|
10339
|
+
const requiresSelection = offer.requiresVariantSelection && variants && variants.length > 0;
|
|
10340
|
+
|
|
10341
|
+
// Build attribute groups from variants
|
|
10342
|
+
const attributeGroups = useMemo(() => {
|
|
10343
|
+
if (!requiresSelection || !variants) return [];
|
|
10344
|
+
const groups = new Map<string, Set<string>>();
|
|
10345
|
+
for (const v of variants) {
|
|
10346
|
+
const opts = getVariantOptions(v as any);
|
|
10347
|
+
for (const opt of opts) {
|
|
10348
|
+
if (!groups.has(opt.name)) groups.set(opt.name, new Set());
|
|
10349
|
+
groups.get(opt.name)!.add(opt.value);
|
|
10350
|
+
}
|
|
10351
|
+
}
|
|
10352
|
+
return Array.from(groups.entries()).map(([name, values]) => ({
|
|
10353
|
+
name,
|
|
10354
|
+
values: Array.from(values),
|
|
10355
|
+
}));
|
|
10356
|
+
}, [requiresSelection, variants]);
|
|
10357
|
+
|
|
10358
|
+
const [selectedAttrs, setSelectedAttrs] = useState<Record<string, string>>({});
|
|
10359
|
+
|
|
10360
|
+
const selectedVariant = useMemo(() => {
|
|
10361
|
+
if (!requiresSelection || !variants) return null;
|
|
10362
|
+
return (
|
|
10363
|
+
variants.find((v) => {
|
|
10364
|
+
const opts = getVariantOptions(v as any);
|
|
10365
|
+
return attributeGroups.every((group) => {
|
|
10366
|
+
const opt = opts.find((o) => o.name === group.name);
|
|
10367
|
+
return opt && selectedAttrs[group.name] === opt.value;
|
|
10368
|
+
});
|
|
10369
|
+
}) ?? null
|
|
10370
|
+
);
|
|
10371
|
+
}, [requiresSelection, variants, selectedAttrs, attributeGroups]);
|
|
10372
|
+
|
|
10373
|
+
const effectiveVariantId = selectedVariant?.id ?? selectedVariantId;
|
|
10374
|
+
|
|
10375
|
+
function handleAttrSelect(attrName: string, value: string) {
|
|
10376
|
+
const next = { ...selectedAttrs, [attrName]: value };
|
|
10377
|
+
setSelectedAttrs(next);
|
|
10378
|
+
if (variants) {
|
|
10379
|
+
const match = variants.find((v) => {
|
|
10380
|
+
const opts = getVariantOptions(v as any);
|
|
10381
|
+
return attributeGroups.every((group) => {
|
|
10382
|
+
const opt = opts.find((o) => o.name === group.name);
|
|
10383
|
+
return opt && next[group.name] === opt.value;
|
|
10384
|
+
});
|
|
10385
|
+
});
|
|
10386
|
+
setSelectedVariantId(match?.id ?? null);
|
|
10387
|
+
}
|
|
10388
|
+
}
|
|
10389
|
+
|
|
10390
|
+
// Compute display prices
|
|
10391
|
+
const { displayOriginal, displayDiscounted, discountLabel } = useMemo(() => {
|
|
10392
|
+
let effectivePrice: number;
|
|
10393
|
+
if (selectedVariant) {
|
|
10394
|
+
const vSale = selectedVariant.salePrice ? parseFloat(selectedVariant.salePrice) : null;
|
|
10395
|
+
const vPrice = selectedVariant.price ? parseFloat(selectedVariant.price) : null;
|
|
10396
|
+
effectivePrice = vSale ?? vPrice ?? parseFloat(offer.originalPrice);
|
|
10397
|
+
} else {
|
|
10398
|
+
effectivePrice = parseFloat(offer.originalPrice);
|
|
10399
|
+
}
|
|
10400
|
+
|
|
10401
|
+
let discounted: number;
|
|
10402
|
+
if (offer.discountType === 'PERCENTAGE') {
|
|
10403
|
+
discounted = effectivePrice * (1 - parseFloat(offer.discountValue) / 100);
|
|
10404
|
+
} else {
|
|
10405
|
+
discounted = Math.max(0, effectivePrice - parseFloat(offer.discountValue));
|
|
10406
|
+
}
|
|
10407
|
+
|
|
10408
|
+
const label =
|
|
10409
|
+
offer.discountType === 'PERCENTAGE'
|
|
10410
|
+
? \`\${offer.discountValue}%\`
|
|
10411
|
+
: (formatPrice(parseFloat(offer.discountValue), { currency }) as string);
|
|
10412
|
+
|
|
10413
|
+
return { displayOriginal: effectivePrice, displayDiscounted: discounted, discountLabel: label };
|
|
10414
|
+
}, [selectedVariant, offer.originalPrice, offer.discountType, offer.discountValue, currency]);
|
|
10415
|
+
|
|
10416
|
+
const isOos =
|
|
10417
|
+
selectedVariant?.inventory?.trackingMode !== 'NOT_TRACKED' &&
|
|
10418
|
+
selectedVariant?.inventory?.available != null &&
|
|
10419
|
+
selectedVariant.inventory.available <= 0;
|
|
10420
|
+
|
|
10421
|
+
const lockedLabel =
|
|
10422
|
+
offer.lockedVariant?.name ??
|
|
10423
|
+
(offer.lockedVariant?.attributes
|
|
10424
|
+
? Object.values(offer.lockedVariant.attributes).join(' / ')
|
|
10425
|
+
: null);
|
|
10426
|
+
|
|
10035
10427
|
const firstImage = product.images?.[0];
|
|
10036
10428
|
const imageUrl = firstImage
|
|
10037
10429
|
? typeof firstImage === 'string'
|
|
10038
10430
|
? firstImage
|
|
10039
10431
|
: firstImage.url
|
|
10040
10432
|
: null;
|
|
10041
|
-
|
|
10042
|
-
const
|
|
10043
|
-
const discountLabel =
|
|
10044
|
-
offer.discountType === 'PERCENTAGE'
|
|
10045
|
-
? \`\${offer.discountValue}%\`
|
|
10046
|
-
: (formatPrice(parseFloat(offer.discountValue), { currency }) as string);
|
|
10433
|
+
|
|
10434
|
+
const canAdd = !requiresSelection || !!effectiveVariantId;
|
|
10047
10435
|
|
|
10048
10436
|
async function handleAdd() {
|
|
10049
|
-
if (adding) return;
|
|
10437
|
+
if (adding || !canAdd || isOos) return;
|
|
10050
10438
|
try {
|
|
10051
10439
|
setAdding(true);
|
|
10052
10440
|
const { getClient } = await import('@/lib/brainerce');
|
|
10053
10441
|
const client = getClient();
|
|
10054
|
-
await client.addBundleToCart(cartId, offer.id);
|
|
10442
|
+
await client.addBundleToCart(cartId, offer.id, effectiveVariantId ?? undefined);
|
|
10055
10443
|
onAdd();
|
|
10056
10444
|
} catch (err) {
|
|
10057
10445
|
console.error('Failed to add bundle item:', err);
|
|
@@ -10061,70 +10449,121 @@ export function CartBundleOfferCard({ offer, cartId, onAdd, className }: CartBun
|
|
|
10061
10449
|
}
|
|
10062
10450
|
|
|
10063
10451
|
return (
|
|
10064
|
-
<div
|
|
10065
|
-
className=
|
|
10066
|
-
|
|
10067
|
-
className
|
|
10068
|
-
|
|
10069
|
-
|
|
10070
|
-
|
|
10071
|
-
|
|
10072
|
-
|
|
10073
|
-
|
|
10074
|
-
|
|
10075
|
-
|
|
10076
|
-
|
|
10077
|
-
|
|
10078
|
-
|
|
10079
|
-
|
|
10080
|
-
|
|
10081
|
-
|
|
10082
|
-
|
|
10083
|
-
</svg>
|
|
10084
|
-
</div>
|
|
10085
|
-
)}
|
|
10086
|
-
</div>
|
|
10452
|
+
<div className={cn('bg-background border-border rounded-lg border p-4', className)}>
|
|
10453
|
+
<div className="flex items-center gap-4">
|
|
10454
|
+
{/* Product image */}
|
|
10455
|
+
<div className="bg-muted relative h-16 w-16 flex-shrink-0 overflow-hidden rounded">
|
|
10456
|
+
{imageUrl ? (
|
|
10457
|
+
<Image src={imageUrl} alt={product.name} fill sizes="64px" className="object-cover" />
|
|
10458
|
+
) : (
|
|
10459
|
+
<div className="text-muted-foreground flex h-full w-full items-center justify-center">
|
|
10460
|
+
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
10461
|
+
<path
|
|
10462
|
+
strokeLinecap="round"
|
|
10463
|
+
strokeLinejoin="round"
|
|
10464
|
+
strokeWidth={1.5}
|
|
10465
|
+
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
|
|
10466
|
+
/>
|
|
10467
|
+
</svg>
|
|
10468
|
+
</div>
|
|
10469
|
+
)}
|
|
10470
|
+
</div>
|
|
10087
10471
|
|
|
10088
|
-
|
|
10089
|
-
|
|
10090
|
-
|
|
10091
|
-
|
|
10092
|
-
|
|
10093
|
-
|
|
10094
|
-
|
|
10095
|
-
<
|
|
10096
|
-
|
|
10097
|
-
|
|
10098
|
-
|
|
10099
|
-
|
|
10100
|
-
|
|
10101
|
-
|
|
10102
|
-
-
|
|
10103
|
-
|
|
10472
|
+
{/* Details */}
|
|
10473
|
+
<div className="min-w-0 flex-1">
|
|
10474
|
+
<p className="text-foreground text-sm font-medium">{offer.name}</p>
|
|
10475
|
+
{offer.description && (
|
|
10476
|
+
<p className="text-muted-foreground mt-0.5 text-xs">{offer.description}</p>
|
|
10477
|
+
)}
|
|
10478
|
+
{lockedLabel && <p className="text-muted-foreground mt-0.5 text-xs">{lockedLabel}</p>}
|
|
10479
|
+
<div className="mt-1 flex items-center gap-2">
|
|
10480
|
+
<span className="text-muted-foreground text-sm line-through">
|
|
10481
|
+
{formatPrice(displayOriginal, { currency }) as string}
|
|
10482
|
+
</span>
|
|
10483
|
+
<span className="text-foreground text-sm font-semibold">
|
|
10484
|
+
{formatPrice(displayDiscounted, { currency }) as string}
|
|
10485
|
+
</span>
|
|
10486
|
+
<span className="bg-destructive/10 text-destructive rounded px-1.5 py-0.5 text-xs font-medium">
|
|
10487
|
+
-{discountLabel}
|
|
10488
|
+
</span>
|
|
10489
|
+
</div>
|
|
10104
10490
|
</div>
|
|
10491
|
+
|
|
10492
|
+
{/* Add button */}
|
|
10493
|
+
<button
|
|
10494
|
+
type="button"
|
|
10495
|
+
onClick={handleAdd}
|
|
10496
|
+
disabled={adding || !canAdd || isOos}
|
|
10497
|
+
className={cn(
|
|
10498
|
+
'bg-primary text-primary-foreground flex-shrink-0 rounded px-4 py-2 text-xs font-medium transition-opacity hover:opacity-90',
|
|
10499
|
+
'disabled:cursor-not-allowed disabled:opacity-50'
|
|
10500
|
+
)}
|
|
10501
|
+
>
|
|
10502
|
+
{adding
|
|
10503
|
+
? t('addingBundle')
|
|
10504
|
+
: requiresSelection && !canAdd
|
|
10505
|
+
? t('selectOptions') || 'Select options'
|
|
10506
|
+
: t('addBundleItem')}
|
|
10507
|
+
</button>
|
|
10105
10508
|
</div>
|
|
10106
10509
|
|
|
10107
|
-
{/*
|
|
10108
|
-
|
|
10109
|
-
|
|
10110
|
-
|
|
10111
|
-
|
|
10112
|
-
|
|
10113
|
-
|
|
10114
|
-
|
|
10115
|
-
|
|
10116
|
-
|
|
10117
|
-
|
|
10118
|
-
|
|
10510
|
+
{/* Compact variant selector */}
|
|
10511
|
+
{requiresSelection && (
|
|
10512
|
+
<div className="mt-3 space-y-1.5 ps-20">
|
|
10513
|
+
{attributeGroups.map((group) => (
|
|
10514
|
+
<div key={group.name} className="flex flex-wrap items-center gap-1.5">
|
|
10515
|
+
<span className="text-muted-foreground text-xs">{group.name}:</span>
|
|
10516
|
+
{group.values.map((value) => {
|
|
10517
|
+
const isSelected = selectedAttrs[group.name] === value;
|
|
10518
|
+
const variantForValue = variants?.find((v) => {
|
|
10519
|
+
const opts = getVariantOptions(v as any);
|
|
10520
|
+
const matchesValue = opts.some((o) => o.name === group.name && o.value === value);
|
|
10521
|
+
if (!matchesValue) return false;
|
|
10522
|
+
return Object.entries(selectedAttrs).every(([k, sv]) => {
|
|
10523
|
+
if (k === group.name) return true;
|
|
10524
|
+
return opts.some((o) => o.name === k && o.value === sv);
|
|
10525
|
+
});
|
|
10526
|
+
});
|
|
10527
|
+
const isVariantOos =
|
|
10528
|
+
variantForValue?.inventory?.trackingMode !== 'NOT_TRACKED' &&
|
|
10529
|
+
variantForValue?.inventory?.available != null &&
|
|
10530
|
+
variantForValue.inventory.available <= 0;
|
|
10531
|
+
|
|
10532
|
+
return (
|
|
10533
|
+
<button
|
|
10534
|
+
key={value}
|
|
10535
|
+
type="button"
|
|
10536
|
+
onClick={() => handleAttrSelect(group.name, value)}
|
|
10537
|
+
disabled={isVariantOos}
|
|
10538
|
+
className={cn(
|
|
10539
|
+
'rounded-full border px-2.5 py-0.5 text-xs transition-colors',
|
|
10540
|
+
isSelected
|
|
10541
|
+
? 'border-primary bg-primary text-primary-foreground'
|
|
10542
|
+
: 'border-border text-foreground hover:border-primary/50',
|
|
10543
|
+
isVariantOos && 'cursor-not-allowed line-through opacity-40'
|
|
10544
|
+
)}
|
|
10545
|
+
>
|
|
10546
|
+
{value}
|
|
10547
|
+
</button>
|
|
10548
|
+
);
|
|
10549
|
+
})}
|
|
10550
|
+
</div>
|
|
10551
|
+
))}
|
|
10552
|
+
{isOos && effectiveVariantId && (
|
|
10553
|
+
<p className="text-destructive text-xs">{t('outOfStock') || 'Out of stock'}</p>
|
|
10554
|
+
)}
|
|
10555
|
+
</div>
|
|
10556
|
+
)}
|
|
10119
10557
|
</div>
|
|
10120
10558
|
);
|
|
10121
10559
|
}
|
|
10122
10560
|
`,
|
|
10123
10561
|
"src/components/checkout/order-bump-card.tsx": `'use client';
|
|
10124
10562
|
|
|
10563
|
+
import { useState, useMemo } from 'react';
|
|
10125
10564
|
import Image from 'next/image';
|
|
10126
|
-
import type { OrderBump } from 'brainerce';
|
|
10127
|
-
import { formatPrice } from 'brainerce';
|
|
10565
|
+
import type { OrderBump, RecommendationVariant } from 'brainerce';
|
|
10566
|
+
import { formatPrice, getVariantOptions } from 'brainerce';
|
|
10128
10567
|
import { useStoreInfo } from '@/providers/store-provider';
|
|
10129
10568
|
import { useTranslations } from '@/lib/translations';
|
|
10130
10569
|
import { cn } from '@/lib/utils';
|
|
@@ -10132,7 +10571,7 @@ import { cn } from '@/lib/utils';
|
|
|
10132
10571
|
interface OrderBumpCardProps {
|
|
10133
10572
|
bump: OrderBump;
|
|
10134
10573
|
isAdded: boolean;
|
|
10135
|
-
onToggle: (bumpId: string, add: boolean) => void;
|
|
10574
|
+
onToggle: (bumpId: string, add: boolean, variantId?: string) => void;
|
|
10136
10575
|
loading: boolean;
|
|
10137
10576
|
className?: string;
|
|
10138
10577
|
}
|
|
@@ -10141,66 +10580,225 @@ export function OrderBumpCard({ bump, isAdded, onToggle, loading, className }: O
|
|
|
10141
10580
|
const { storeInfo } = useStoreInfo();
|
|
10142
10581
|
const t = useTranslations('checkout');
|
|
10143
10582
|
const currency = storeInfo?.currency || 'USD';
|
|
10583
|
+
const [selectedVariantId, setSelectedVariantId] = useState<string | null>(null);
|
|
10144
10584
|
|
|
10145
10585
|
const product = bump.bumpProduct;
|
|
10586
|
+
const variants = product.variants;
|
|
10587
|
+
const requiresSelection = bump.requiresVariantSelection && variants && variants.length > 0;
|
|
10588
|
+
|
|
10589
|
+
// Build attribute groups from variants for pill selector
|
|
10590
|
+
const attributeGroups = useMemo(() => {
|
|
10591
|
+
if (!requiresSelection || !variants) return [];
|
|
10592
|
+
const groups = new Map<string, Set<string>>();
|
|
10593
|
+
for (const v of variants) {
|
|
10594
|
+
const opts = getVariantOptions(v as any);
|
|
10595
|
+
for (const opt of opts) {
|
|
10596
|
+
if (!groups.has(opt.name)) groups.set(opt.name, new Set());
|
|
10597
|
+
groups.get(opt.name)!.add(opt.value);
|
|
10598
|
+
}
|
|
10599
|
+
}
|
|
10600
|
+
return Array.from(groups.entries()).map(([name, values]) => ({
|
|
10601
|
+
name,
|
|
10602
|
+
values: Array.from(values),
|
|
10603
|
+
}));
|
|
10604
|
+
}, [requiresSelection, variants]);
|
|
10605
|
+
|
|
10606
|
+
// Track selected attributes
|
|
10607
|
+
const [selectedAttrs, setSelectedAttrs] = useState<Record<string, string>>({});
|
|
10608
|
+
|
|
10609
|
+
// Find matching variant based on selected attributes
|
|
10610
|
+
const selectedVariant = useMemo(() => {
|
|
10611
|
+
if (!requiresSelection || !variants) return null;
|
|
10612
|
+
return (
|
|
10613
|
+
variants.find((v) => {
|
|
10614
|
+
const opts = getVariantOptions(v as any);
|
|
10615
|
+
return attributeGroups.every((group) => {
|
|
10616
|
+
const opt = opts.find((o) => o.name === group.name);
|
|
10617
|
+
return opt && selectedAttrs[group.name] === opt.value;
|
|
10618
|
+
});
|
|
10619
|
+
}) ?? null
|
|
10620
|
+
);
|
|
10621
|
+
}, [requiresSelection, variants, selectedAttrs, attributeGroups]);
|
|
10622
|
+
|
|
10623
|
+
// Update selectedVariantId when variant match changes
|
|
10624
|
+
const effectiveVariantId = selectedVariant?.id ?? selectedVariantId;
|
|
10625
|
+
|
|
10626
|
+
function handleAttrSelect(attrName: string, value: string) {
|
|
10627
|
+
const next = { ...selectedAttrs, [attrName]: value };
|
|
10628
|
+
setSelectedAttrs(next);
|
|
10629
|
+
// Find matching variant with new selection
|
|
10630
|
+
if (variants) {
|
|
10631
|
+
const match = variants.find((v) => {
|
|
10632
|
+
const opts = getVariantOptions(v as any);
|
|
10633
|
+
return attributeGroups.every((group) => {
|
|
10634
|
+
const opt = opts.find((o) => o.name === group.name);
|
|
10635
|
+
return opt && next[group.name] === opt.value;
|
|
10636
|
+
});
|
|
10637
|
+
});
|
|
10638
|
+
setSelectedVariantId(match?.id ?? null);
|
|
10639
|
+
}
|
|
10640
|
+
}
|
|
10641
|
+
|
|
10642
|
+
// Compute display price
|
|
10643
|
+
const { displayOriginal, displayDiscounted } = useMemo(() => {
|
|
10644
|
+
let effectivePrice: number;
|
|
10645
|
+
if (selectedVariant) {
|
|
10646
|
+
const vSale = selectedVariant.salePrice ? parseFloat(selectedVariant.salePrice) : null;
|
|
10647
|
+
const vPrice = selectedVariant.price ? parseFloat(selectedVariant.price) : null;
|
|
10648
|
+
effectivePrice = vSale ?? vPrice ?? parseFloat(bump.originalPrice);
|
|
10649
|
+
} else {
|
|
10650
|
+
effectivePrice = parseFloat(bump.originalPrice);
|
|
10651
|
+
}
|
|
10652
|
+
|
|
10653
|
+
let discounted: number | null = null;
|
|
10654
|
+
if (bump.discountType && bump.discountValue) {
|
|
10655
|
+
const dv = parseFloat(bump.discountValue);
|
|
10656
|
+
if (bump.discountType === 'PERCENTAGE') {
|
|
10657
|
+
discounted = effectivePrice * (1 - dv / 100);
|
|
10658
|
+
} else {
|
|
10659
|
+
discounted = Math.max(0, effectivePrice - dv);
|
|
10660
|
+
}
|
|
10661
|
+
}
|
|
10662
|
+
|
|
10663
|
+
return { displayOriginal: effectivePrice, displayDiscounted: discounted };
|
|
10664
|
+
}, [selectedVariant, bump.originalPrice, bump.discountType, bump.discountValue]);
|
|
10665
|
+
|
|
10666
|
+
// Check if selected variant is out of stock
|
|
10667
|
+
const isOos =
|
|
10668
|
+
selectedVariant?.inventory?.trackingMode !== 'NOT_TRACKED' &&
|
|
10669
|
+
selectedVariant?.inventory?.available != null &&
|
|
10670
|
+
selectedVariant.inventory.available <= 0;
|
|
10671
|
+
|
|
10672
|
+
// Locked variant label
|
|
10673
|
+
const lockedLabel =
|
|
10674
|
+
bump.lockedVariant?.name ??
|
|
10675
|
+
(bump.lockedVariant?.attributes
|
|
10676
|
+
? Object.values(bump.lockedVariant.attributes).join(' / ')
|
|
10677
|
+
: null);
|
|
10678
|
+
|
|
10146
10679
|
const firstImage = product.images?.[0];
|
|
10147
10680
|
const imageUrl = firstImage
|
|
10148
10681
|
? typeof firstImage === 'string'
|
|
10149
10682
|
? firstImage
|
|
10150
10683
|
: firstImage.url
|
|
10151
10684
|
: null;
|
|
10152
|
-
|
|
10153
|
-
const
|
|
10154
|
-
const discountedPrice = hasDiscount ? parseFloat(bump.discountedPrice!) : null;
|
|
10685
|
+
|
|
10686
|
+
const canToggle = !requiresSelection || !!effectiveVariantId;
|
|
10155
10687
|
|
|
10156
10688
|
return (
|
|
10157
|
-
<
|
|
10689
|
+
<div
|
|
10158
10690
|
className={cn(
|
|
10159
|
-
'border-border hover:border-primary/50
|
|
10691
|
+
'border-border hover:border-primary/50 rounded-lg border p-3 transition-colors',
|
|
10160
10692
|
isAdded && 'border-primary bg-primary/5',
|
|
10161
10693
|
loading && 'pointer-events-none opacity-60',
|
|
10162
10694
|
className
|
|
10163
10695
|
)}
|
|
10164
10696
|
>
|
|
10165
|
-
<
|
|
10166
|
-
|
|
10167
|
-
|
|
10168
|
-
|
|
10169
|
-
|
|
10170
|
-
|
|
10171
|
-
|
|
10172
|
-
|
|
10173
|
-
|
|
10174
|
-
|
|
10175
|
-
|
|
10176
|
-
|
|
10177
|
-
</div>
|
|
10178
|
-
)}
|
|
10697
|
+
<label className="flex cursor-pointer items-start gap-3">
|
|
10698
|
+
<input
|
|
10699
|
+
type="checkbox"
|
|
10700
|
+
checked={isAdded}
|
|
10701
|
+
onChange={() => {
|
|
10702
|
+
if (canToggle) {
|
|
10703
|
+
onToggle(bump.id, !isAdded, effectiveVariantId ?? undefined);
|
|
10704
|
+
}
|
|
10705
|
+
}}
|
|
10706
|
+
disabled={loading || !canToggle || isOos}
|
|
10707
|
+
className="mt-1 h-4 w-4 shrink-0 rounded"
|
|
10708
|
+
/>
|
|
10179
10709
|
|
|
10180
|
-
|
|
10181
|
-
|
|
10182
|
-
|
|
10183
|
-
|
|
10184
|
-
|
|
10710
|
+
{/* Image */}
|
|
10711
|
+
{imageUrl && (
|
|
10712
|
+
<div className="bg-muted relative h-10 w-10 shrink-0 overflow-hidden rounded">
|
|
10713
|
+
<Image src={imageUrl} alt={product.name} fill sizes="40px" className="object-cover" />
|
|
10714
|
+
</div>
|
|
10185
10715
|
)}
|
|
10186
|
-
|
|
10187
|
-
|
|
10188
|
-
|
|
10189
|
-
|
|
10190
|
-
|
|
10191
|
-
|
|
10716
|
+
|
|
10717
|
+
{/* Content */}
|
|
10718
|
+
<div className="min-w-0 flex-1">
|
|
10719
|
+
<p className="text-foreground text-sm font-medium">{bump.title}</p>
|
|
10720
|
+
{bump.description && (
|
|
10721
|
+
<p className="text-muted-foreground mt-0.5 text-xs">{bump.description}</p>
|
|
10722
|
+
)}
|
|
10723
|
+
|
|
10724
|
+
{/* Locked variant label */}
|
|
10725
|
+
{lockedLabel && <p className="text-muted-foreground mt-0.5 text-xs">{lockedLabel}</p>}
|
|
10726
|
+
|
|
10727
|
+
{/* Price */}
|
|
10728
|
+
<div className="mt-1 flex items-center gap-2">
|
|
10729
|
+
{displayDiscounted != null ? (
|
|
10730
|
+
<>
|
|
10731
|
+
<span className="text-muted-foreground text-xs line-through">
|
|
10732
|
+
{formatPrice(displayOriginal, { currency }) as string}
|
|
10733
|
+
</span>
|
|
10734
|
+
<span className="text-foreground text-sm font-semibold">
|
|
10735
|
+
{formatPrice(displayDiscounted, { currency }) as string}
|
|
10736
|
+
</span>
|
|
10737
|
+
</>
|
|
10738
|
+
) : (
|
|
10192
10739
|
<span className="text-foreground text-sm font-semibold">
|
|
10193
|
-
{formatPrice(
|
|
10740
|
+
{formatPrice(displayOriginal, { currency }) as string}
|
|
10194
10741
|
</span>
|
|
10195
|
-
|
|
10196
|
-
|
|
10197
|
-
|
|
10198
|
-
|
|
10199
|
-
|
|
10742
|
+
)}
|
|
10743
|
+
{requiresSelection && !effectiveVariantId && (
|
|
10744
|
+
<span className="text-muted-foreground text-xs">
|
|
10745
|
+
{t('selectOptions') || 'Select options'}
|
|
10746
|
+
</span>
|
|
10747
|
+
)}
|
|
10748
|
+
</div>
|
|
10749
|
+
</div>
|
|
10750
|
+
</label>
|
|
10751
|
+
|
|
10752
|
+
{/* Compact variant selector */}
|
|
10753
|
+
{requiresSelection && !isAdded && (
|
|
10754
|
+
<div className="ms-7 mt-2 space-y-1.5">
|
|
10755
|
+
{attributeGroups.map((group) => (
|
|
10756
|
+
<div key={group.name} className="flex flex-wrap items-center gap-1.5">
|
|
10757
|
+
<span className="text-muted-foreground text-xs">{group.name}:</span>
|
|
10758
|
+
{group.values.map((value) => {
|
|
10759
|
+
const isSelected = selectedAttrs[group.name] === value;
|
|
10760
|
+
// Check if this value leads to any available variant
|
|
10761
|
+
const variantForValue = variants?.find((v) => {
|
|
10762
|
+
const opts = getVariantOptions(v as any);
|
|
10763
|
+
const matchesValue = opts.some((o) => o.name === group.name && o.value === value);
|
|
10764
|
+
if (!matchesValue) return false;
|
|
10765
|
+
// Check other selected attrs
|
|
10766
|
+
return Object.entries(selectedAttrs).every(([k, sv]) => {
|
|
10767
|
+
if (k === group.name) return true;
|
|
10768
|
+
return opts.some((o) => o.name === k && o.value === sv);
|
|
10769
|
+
});
|
|
10770
|
+
});
|
|
10771
|
+
const isVariantOos =
|
|
10772
|
+
variantForValue?.inventory?.trackingMode !== 'NOT_TRACKED' &&
|
|
10773
|
+
variantForValue?.inventory?.available != null &&
|
|
10774
|
+
variantForValue.inventory.available <= 0;
|
|
10775
|
+
|
|
10776
|
+
return (
|
|
10777
|
+
<button
|
|
10778
|
+
key={value}
|
|
10779
|
+
type="button"
|
|
10780
|
+
onClick={() => handleAttrSelect(group.name, value)}
|
|
10781
|
+
disabled={isVariantOos}
|
|
10782
|
+
className={cn(
|
|
10783
|
+
'rounded-full border px-2.5 py-0.5 text-xs transition-colors',
|
|
10784
|
+
isSelected
|
|
10785
|
+
? 'border-primary bg-primary text-primary-foreground'
|
|
10786
|
+
: 'border-border text-foreground hover:border-primary/50',
|
|
10787
|
+
isVariantOos && 'cursor-not-allowed line-through opacity-40'
|
|
10788
|
+
)}
|
|
10789
|
+
>
|
|
10790
|
+
{value}
|
|
10791
|
+
</button>
|
|
10792
|
+
);
|
|
10793
|
+
})}
|
|
10794
|
+
</div>
|
|
10795
|
+
))}
|
|
10796
|
+
{isOos && effectiveVariantId && (
|
|
10797
|
+
<p className="text-destructive text-xs">{t('outOfStock') || 'Out of stock'}</p>
|
|
10200
10798
|
)}
|
|
10201
10799
|
</div>
|
|
10202
|
-
|
|
10203
|
-
</
|
|
10800
|
+
)}
|
|
10801
|
+
</div>
|
|
10204
10802
|
);
|
|
10205
10803
|
}
|
|
10206
10804
|
`,
|
|
@@ -10644,46 +11242,81 @@ async function handleGetRequiredPages(args) {
|
|
|
10644
11242
|
var import_zod5 = require("zod");
|
|
10645
11243
|
|
|
10646
11244
|
// src/utils/fetch-store-info.ts
|
|
10647
|
-
var
|
|
10648
|
-
|
|
10649
|
-
|
|
11245
|
+
var KNOWN_API_URLS = {
|
|
11246
|
+
production: "https://api.brainerce.com",
|
|
11247
|
+
staging: "https://api-staging.brainerce.com"
|
|
11248
|
+
};
|
|
11249
|
+
var ConnectionNotFoundError = class extends Error {
|
|
11250
|
+
constructor(connectionId, baseUrl) {
|
|
11251
|
+
super(`Connection "${connectionId}" not found at ${baseUrl}`);
|
|
11252
|
+
this.code = "NOT_FOUND";
|
|
11253
|
+
}
|
|
11254
|
+
};
|
|
11255
|
+
async function fetchStoreInfo(connectionId, baseUrl = KNOWN_API_URLS.production) {
|
|
10650
11256
|
const url = `${baseUrl}/api/vc/${connectionId}/info`;
|
|
10651
|
-
|
|
10652
|
-
|
|
10653
|
-
|
|
10654
|
-
|
|
10655
|
-
|
|
10656
|
-
|
|
10657
|
-
|
|
10658
|
-
|
|
10659
|
-
|
|
10660
|
-
|
|
10661
|
-
|
|
10662
|
-
|
|
10663
|
-
|
|
10664
|
-
|
|
10665
|
-
|
|
10666
|
-
|
|
10667
|
-
|
|
10668
|
-
|
|
10669
|
-
|
|
10670
|
-
|
|
10671
|
-
|
|
10672
|
-
|
|
10673
|
-
|
|
10674
|
-
|
|
10675
|
-
|
|
10676
|
-
|
|
10677
|
-
|
|
10678
|
-
|
|
10679
|
-
|
|
10680
|
-
|
|
10681
|
-
|
|
10682
|
-
|
|
10683
|
-
|
|
10684
|
-
|
|
10685
|
-
|
|
10686
|
-
|
|
11257
|
+
const controller = new AbortController();
|
|
11258
|
+
const timeout = setTimeout(() => controller.abort(), 1e4);
|
|
11259
|
+
let res;
|
|
11260
|
+
try {
|
|
11261
|
+
res = await fetch(url, { signal: controller.signal });
|
|
11262
|
+
} catch (err) {
|
|
11263
|
+
if (err.name === "AbortError") {
|
|
11264
|
+
throw new Error(`Request to ${baseUrl} timed out`);
|
|
11265
|
+
}
|
|
11266
|
+
throw new Error(`Failed to connect to ${baseUrl}: ${err.message}`);
|
|
11267
|
+
} finally {
|
|
11268
|
+
clearTimeout(timeout);
|
|
11269
|
+
}
|
|
11270
|
+
if (res.status === 404) {
|
|
11271
|
+
throw new ConnectionNotFoundError(connectionId, baseUrl);
|
|
11272
|
+
}
|
|
11273
|
+
if (!res.ok) {
|
|
11274
|
+
throw new Error(`${baseUrl} returned status ${res.status}`);
|
|
11275
|
+
}
|
|
11276
|
+
let json;
|
|
11277
|
+
try {
|
|
11278
|
+
json = await res.json();
|
|
11279
|
+
} catch {
|
|
11280
|
+
throw new Error(`Invalid response from ${baseUrl}`);
|
|
11281
|
+
}
|
|
11282
|
+
return {
|
|
11283
|
+
name: json.name || json.storeName || "My Store",
|
|
11284
|
+
currency: json.currency || "USD",
|
|
11285
|
+
language: json.language || "en",
|
|
11286
|
+
...json.i18n ? { i18n: json.i18n } : {}
|
|
11287
|
+
};
|
|
11288
|
+
}
|
|
11289
|
+
async function resolveStoreInfo(connectionId, candidateUrls) {
|
|
11290
|
+
const urls = candidateUrls.filter((u, i) => u && candidateUrls.indexOf(u) === i);
|
|
11291
|
+
if (urls.length === 0) {
|
|
11292
|
+
throw new Error("No API URLs to try");
|
|
11293
|
+
}
|
|
11294
|
+
let lastError;
|
|
11295
|
+
let allNotFound = true;
|
|
11296
|
+
for (let i = 0; i < urls.length; i++) {
|
|
11297
|
+
const baseUrl = urls[i];
|
|
11298
|
+
try {
|
|
11299
|
+
const info = await fetchStoreInfo(connectionId, baseUrl);
|
|
11300
|
+
return { info, apiBaseUrl: baseUrl, fellBack: i > 0 };
|
|
11301
|
+
} catch (err) {
|
|
11302
|
+
lastError = err;
|
|
11303
|
+
const isNotFound = err.code === "NOT_FOUND";
|
|
11304
|
+
if (!isNotFound) {
|
|
11305
|
+
allNotFound = false;
|
|
11306
|
+
}
|
|
11307
|
+
}
|
|
11308
|
+
}
|
|
11309
|
+
if (allNotFound) {
|
|
11310
|
+
throw new Error(
|
|
11311
|
+
`Connection "${connectionId}" not found in any known environment (${urls.join(", ")}).`
|
|
11312
|
+
);
|
|
11313
|
+
}
|
|
11314
|
+
throw lastError || new Error("Failed to resolve store info");
|
|
11315
|
+
}
|
|
11316
|
+
function getCandidateApiUrls() {
|
|
11317
|
+
const explicit = (process.env.BRAINERCE_API_URL || "").replace(/\/$/, "");
|
|
11318
|
+
if (explicit) return [explicit];
|
|
11319
|
+
return [KNOWN_API_URLS.production, KNOWN_API_URLS.staging];
|
|
10687
11320
|
}
|
|
10688
11321
|
|
|
10689
11322
|
// src/tools/get-store-info.ts
|
|
@@ -10694,17 +11327,18 @@ var GET_STORE_INFO_SCHEMA = {
|
|
|
10694
11327
|
};
|
|
10695
11328
|
async function handleGetStoreInfo(args) {
|
|
10696
11329
|
try {
|
|
10697
|
-
const
|
|
11330
|
+
const resolved = await resolveStoreInfo(args.connectionId, getCandidateApiUrls());
|
|
10698
11331
|
return {
|
|
10699
11332
|
content: [
|
|
10700
11333
|
{
|
|
10701
11334
|
type: "text",
|
|
10702
11335
|
text: JSON.stringify(
|
|
10703
11336
|
{
|
|
10704
|
-
name: info.name,
|
|
10705
|
-
currency: info.currency,
|
|
10706
|
-
language: info.language,
|
|
10707
|
-
connectionId: args.connectionId
|
|
11337
|
+
name: resolved.info.name,
|
|
11338
|
+
currency: resolved.info.currency,
|
|
11339
|
+
language: resolved.info.language,
|
|
11340
|
+
connectionId: args.connectionId,
|
|
11341
|
+
apiBaseUrl: resolved.apiBaseUrl
|
|
10708
11342
|
},
|
|
10709
11343
|
null,
|
|
10710
11344
|
2
|
|
@@ -10725,41 +11359,65 @@ async function handleGetStoreInfo(args) {
|
|
|
10725
11359
|
var import_zod6 = require("zod");
|
|
10726
11360
|
|
|
10727
11361
|
// src/utils/fetch-store-capabilities.ts
|
|
10728
|
-
var
|
|
10729
|
-
|
|
11362
|
+
var ConnectionNotFoundError2 = class extends Error {
|
|
11363
|
+
constructor(connectionId, baseUrl) {
|
|
11364
|
+
super(`Connection "${connectionId}" not found at ${baseUrl}`);
|
|
11365
|
+
this.code = "NOT_FOUND";
|
|
11366
|
+
}
|
|
11367
|
+
};
|
|
10730
11368
|
async function fetchStoreCapabilities(connectionId, baseUrl = "https://api.brainerce.com") {
|
|
10731
11369
|
const url = `${baseUrl}/api/vc/${connectionId}/capabilities`;
|
|
10732
|
-
|
|
10733
|
-
|
|
10734
|
-
|
|
10735
|
-
|
|
10736
|
-
|
|
10737
|
-
|
|
10738
|
-
|
|
10739
|
-
|
|
10740
|
-
|
|
10741
|
-
|
|
10742
|
-
|
|
10743
|
-
|
|
10744
|
-
|
|
10745
|
-
|
|
10746
|
-
|
|
10747
|
-
|
|
10748
|
-
|
|
10749
|
-
|
|
10750
|
-
|
|
10751
|
-
|
|
10752
|
-
|
|
10753
|
-
|
|
10754
|
-
});
|
|
10755
|
-
|
|
10756
|
-
|
|
10757
|
-
|
|
10758
|
-
|
|
10759
|
-
|
|
10760
|
-
|
|
10761
|
-
|
|
10762
|
-
|
|
11370
|
+
const controller = new AbortController();
|
|
11371
|
+
const timeout = setTimeout(() => controller.abort(), 1e4);
|
|
11372
|
+
let res;
|
|
11373
|
+
try {
|
|
11374
|
+
res = await fetch(url, { signal: controller.signal });
|
|
11375
|
+
} catch (err) {
|
|
11376
|
+
if (err.name === "AbortError") {
|
|
11377
|
+
throw new Error(`Request to ${baseUrl} timed out`);
|
|
11378
|
+
}
|
|
11379
|
+
throw new Error(`Failed to connect to ${baseUrl}: ${err.message}`);
|
|
11380
|
+
} finally {
|
|
11381
|
+
clearTimeout(timeout);
|
|
11382
|
+
}
|
|
11383
|
+
if (res.status === 404) {
|
|
11384
|
+
throw new ConnectionNotFoundError2(connectionId, baseUrl);
|
|
11385
|
+
}
|
|
11386
|
+
if (!res.ok) {
|
|
11387
|
+
throw new Error(`${baseUrl} returned status ${res.status}`);
|
|
11388
|
+
}
|
|
11389
|
+
try {
|
|
11390
|
+
return await res.json();
|
|
11391
|
+
} catch {
|
|
11392
|
+
throw new Error(`Invalid response from ${baseUrl}`);
|
|
11393
|
+
}
|
|
11394
|
+
}
|
|
11395
|
+
async function resolveStoreCapabilities(connectionId, candidateUrls) {
|
|
11396
|
+
const urls = candidateUrls.filter((u, i) => u && candidateUrls.indexOf(u) === i);
|
|
11397
|
+
if (urls.length === 0) {
|
|
11398
|
+
throw new Error("No API URLs to try");
|
|
11399
|
+
}
|
|
11400
|
+
let lastError;
|
|
11401
|
+
let allNotFound = true;
|
|
11402
|
+
for (let i = 0; i < urls.length; i++) {
|
|
11403
|
+
const baseUrl = urls[i];
|
|
11404
|
+
try {
|
|
11405
|
+
const capabilities = await fetchStoreCapabilities(connectionId, baseUrl);
|
|
11406
|
+
return { capabilities, apiBaseUrl: baseUrl, fellBack: i > 0 };
|
|
11407
|
+
} catch (err) {
|
|
11408
|
+
lastError = err;
|
|
11409
|
+
const isNotFound = err.code === "NOT_FOUND";
|
|
11410
|
+
if (!isNotFound) {
|
|
11411
|
+
allNotFound = false;
|
|
11412
|
+
}
|
|
11413
|
+
}
|
|
11414
|
+
}
|
|
11415
|
+
if (allNotFound) {
|
|
11416
|
+
throw new Error(
|
|
11417
|
+
`Connection "${connectionId}" not found in any known environment (${urls.join(", ")}).`
|
|
11418
|
+
);
|
|
11419
|
+
}
|
|
11420
|
+
throw lastError || new Error("Failed to resolve store capabilities");
|
|
10763
11421
|
}
|
|
10764
11422
|
|
|
10765
11423
|
// src/tools/get-store-capabilities.ts
|
|
@@ -10772,6 +11430,11 @@ function formatCapabilities(caps) {
|
|
|
10772
11430
|
const lines = [];
|
|
10773
11431
|
lines.push(`## Store: ${caps.store.name} (${caps.store.currency})`);
|
|
10774
11432
|
lines.push(`Language: ${caps.store.language}`);
|
|
11433
|
+
if (caps.store.i18n?.enabled) {
|
|
11434
|
+
lines.push(
|
|
11435
|
+
`Multi-language: ENABLED \u2014 locales: ${caps.store.i18n.supportedLocales.join(", ")} (default: ${caps.store.i18n.defaultLocale})`
|
|
11436
|
+
);
|
|
11437
|
+
}
|
|
10775
11438
|
lines.push("");
|
|
10776
11439
|
lines.push("## Configured Features");
|
|
10777
11440
|
if (caps.features.paymentProviders.length > 0) {
|
|
@@ -10858,6 +11521,11 @@ function formatCapabilities(caps) {
|
|
|
10858
11521
|
'Inventory reservation is active \u2014 show a countdown timer. Use get-code-example("reservation-countdown").'
|
|
10859
11522
|
);
|
|
10860
11523
|
}
|
|
11524
|
+
if (caps.store.i18n?.enabled && caps.store.i18n.supportedLocales.length > 1) {
|
|
11525
|
+
suggestions.push(
|
|
11526
|
+
`Multi-language is enabled (${caps.store.i18n.supportedLocales.join(", ")}). Use client.setLocale(locale) before fetching content. Consider locale-prefixed routes (/he/products, /en/products) and a language switcher in the header.`
|
|
11527
|
+
);
|
|
11528
|
+
}
|
|
10861
11529
|
if (suggestions.length === 0) {
|
|
10862
11530
|
suggestions.push(
|
|
10863
11531
|
"Start by building the required pages. Use get-required-pages() for the checklist."
|
|
@@ -10868,9 +11536,9 @@ function formatCapabilities(caps) {
|
|
|
10868
11536
|
}
|
|
10869
11537
|
async function handleGetStoreCapabilities(args) {
|
|
10870
11538
|
try {
|
|
10871
|
-
const
|
|
11539
|
+
const resolved = await resolveStoreCapabilities(args.connectionId, getCandidateApiUrls());
|
|
10872
11540
|
return {
|
|
10873
|
-
content: [{ type: "text", text: formatCapabilities(
|
|
11541
|
+
content: [{ type: "text", text: formatCapabilities(resolved.capabilities) }]
|
|
10874
11542
|
};
|
|
10875
11543
|
} catch (error) {
|
|
10876
11544
|
const message = error instanceof Error ? error.message : "Failed to fetch store capabilities";
|
|
@@ -10965,6 +11633,20 @@ function formatCapabilitiesSummary(caps) {
|
|
|
10965
11633
|
const lines = [];
|
|
10966
11634
|
lines.push(`## Store: ${caps.store.name}`);
|
|
10967
11635
|
lines.push(`Currency: ${caps.store.currency} | Language: ${caps.store.language}`);
|
|
11636
|
+
if (caps.store.i18n?.enabled && caps.store.i18n.supportedLocales.length > 1) {
|
|
11637
|
+
lines.push(
|
|
11638
|
+
`Multi-language: ${caps.store.i18n.supportedLocales.join(", ")} (default: ${caps.store.i18n.defaultLocale})`
|
|
11639
|
+
);
|
|
11640
|
+
lines.push("");
|
|
11641
|
+
lines.push("### i18n Implementation");
|
|
11642
|
+
lines.push(
|
|
11643
|
+
"- Call `client.setLocale(locale)` at app init based on URL prefix or user preference"
|
|
11644
|
+
);
|
|
11645
|
+
lines.push("- All getProducts/getCategories/getBrands calls auto-include the locale");
|
|
11646
|
+
lines.push("- Use locale-prefixed routes: `/[locale]/products`, `/[locale]/products/[slug]`");
|
|
11647
|
+
lines.push("- Add a language switcher component in the header");
|
|
11648
|
+
lines.push('- RTL locales (he, ar): set `<html dir="rtl">` \u2014 flexbox reversal is automatic');
|
|
11649
|
+
}
|
|
10968
11650
|
lines.push("");
|
|
10969
11651
|
lines.push("### Configured Features");
|
|
10970
11652
|
if (caps.features.paymentProviders.length > 0) {
|
|
@@ -11018,8 +11700,7 @@ function buildStoreBundle(options) {
|
|
|
11018
11700
|
connectionId,
|
|
11019
11701
|
storeName,
|
|
11020
11702
|
currency,
|
|
11021
|
-
language: capabilities?.store.language || "en"
|
|
11022
|
-
apiUrl: "https://api.brainerce.com"
|
|
11703
|
+
language: capabilities?.store.language || "en"
|
|
11023
11704
|
};
|
|
11024
11705
|
const sections = [];
|
|
11025
11706
|
sections.push(`# Build: ${storeName} \u2014 ${storeType}${styleDesc}
|
|
@@ -11122,7 +11803,8 @@ var BUILD_STORE_SCHEMA = {
|
|
|
11122
11803
|
async function handleBuildStore(args) {
|
|
11123
11804
|
let capabilities = null;
|
|
11124
11805
|
try {
|
|
11125
|
-
|
|
11806
|
+
const resolved = await resolveStoreCapabilities(args.connectionId, getCandidateApiUrls());
|
|
11807
|
+
capabilities = resolved.capabilities;
|
|
11126
11808
|
} catch {
|
|
11127
11809
|
}
|
|
11128
11810
|
const bundle = buildStoreBundle({
|
|
@@ -11461,6 +12143,57 @@ ${results.join("\n\n---\n\n")}`
|
|
|
11461
12143
|
};
|
|
11462
12144
|
}
|
|
11463
12145
|
|
|
12146
|
+
// src/tools/get-integration-guide.ts
|
|
12147
|
+
var import_zod10 = require("zod");
|
|
12148
|
+
var GET_INTEGRATION_GUIDE_NAME = "get-integration-guide";
|
|
12149
|
+
var GET_INTEGRATION_GUIDE_DESCRIPTION = 'Get the Brainerce integration guide for connecting any website to Brainerce. Returns step-by-step instructions with full API endpoints, request/response examples, and code snippets. Use "core" for the main guide (products, cart, checkout, payment, orders), "optional" for extra features (accounts, OAuth, promotions), or "rules" for validation, error codes, and edge cases.';
|
|
12150
|
+
var GET_INTEGRATION_GUIDE_SCHEMA = {
|
|
12151
|
+
part: import_zod10.z.enum(["core", "optional", "rules"]).describe(
|
|
12152
|
+
'Which part of the integration guide to retrieve. "core" = products, cart, checkout, payment, orders (start here). "optional" = customer accounts, OAuth, discounts, bundles, downloads. "rules" = validation, error codes, edge cases, decision trees, common mistakes.'
|
|
12153
|
+
)
|
|
12154
|
+
};
|
|
12155
|
+
var GUIDE_URLS = {
|
|
12156
|
+
core: "https://brainerce.com/docs/integration/raw?part=core",
|
|
12157
|
+
optional: "https://brainerce.com/docs/integration/raw?part=optional",
|
|
12158
|
+
rules: "https://brainerce.com/docs/integration/raw?part=rules"
|
|
12159
|
+
};
|
|
12160
|
+
async function handleGetIntegrationGuide(args) {
|
|
12161
|
+
const url = GUIDE_URLS[args.part];
|
|
12162
|
+
if (!url) {
|
|
12163
|
+
return {
|
|
12164
|
+
content: [
|
|
12165
|
+
{
|
|
12166
|
+
type: "text",
|
|
12167
|
+
text: `Unknown part: "${args.part}". Available parts: core, optional, rules.`
|
|
12168
|
+
}
|
|
12169
|
+
]
|
|
12170
|
+
};
|
|
12171
|
+
}
|
|
12172
|
+
try {
|
|
12173
|
+
const response = await fetch(url, {
|
|
12174
|
+
headers: { Accept: "text/plain" },
|
|
12175
|
+
signal: AbortSignal.timeout(15e3)
|
|
12176
|
+
});
|
|
12177
|
+
if (!response.ok) {
|
|
12178
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
12179
|
+
}
|
|
12180
|
+
const content = await response.text();
|
|
12181
|
+
return {
|
|
12182
|
+
content: [{ type: "text", text: content }]
|
|
12183
|
+
};
|
|
12184
|
+
} catch (error) {
|
|
12185
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
12186
|
+
return {
|
|
12187
|
+
content: [
|
|
12188
|
+
{
|
|
12189
|
+
type: "text",
|
|
12190
|
+
text: `Failed to fetch integration guide (${args.part}): ${message}. You can read it directly at: ${url}`
|
|
12191
|
+
}
|
|
12192
|
+
]
|
|
12193
|
+
};
|
|
12194
|
+
}
|
|
12195
|
+
}
|
|
12196
|
+
|
|
11464
12197
|
// src/resources/sdk-types.ts
|
|
11465
12198
|
var SDK_TYPES_URI = "brainerce://sdk/types";
|
|
11466
12199
|
var SDK_TYPES_NAME = "Brainerce SDK Types";
|
|
@@ -11615,15 +12348,15 @@ async function handleProjectTemplate(uri) {
|
|
|
11615
12348
|
}
|
|
11616
12349
|
|
|
11617
12350
|
// src/prompts/create-store.ts
|
|
11618
|
-
var
|
|
12351
|
+
var import_zod11 = require("zod");
|
|
11619
12352
|
var CREATE_STORE_NAME = "create-store";
|
|
11620
12353
|
var CREATE_STORE_DESCRIPTION = "Generate a personalized prompt for building a complete Brainerce e-commerce store. Instructs the AI to call build-store for all templates and validate-store when done.";
|
|
11621
12354
|
var CREATE_STORE_SCHEMA = {
|
|
11622
|
-
connectionId:
|
|
11623
|
-
storeName:
|
|
11624
|
-
storeType:
|
|
11625
|
-
currency:
|
|
11626
|
-
storeStyle:
|
|
12355
|
+
connectionId: import_zod11.z.string().describe("Vibe-coded connection ID (starts with vc_)"),
|
|
12356
|
+
storeName: import_zod11.z.string().describe('Name of the store (e.g., "Urban Threads")'),
|
|
12357
|
+
storeType: import_zod11.z.string().describe('Type of store (e.g., "Clothing store", "Electronics shop", "Bakery")'),
|
|
12358
|
+
currency: import_zod11.z.string().optional().default("USD").describe("Store currency code (e.g., USD, ILS, EUR)"),
|
|
12359
|
+
storeStyle: import_zod11.z.string().optional().describe('Design style (e.g., "minimalist", "luxury", "playful", "dark mode")')
|
|
11627
12360
|
};
|
|
11628
12361
|
function handleCreateStore(args) {
|
|
11629
12362
|
const styleDesc = args.storeStyle ? ` with a ${args.storeStyle} design style` : "";
|
|
@@ -11662,6 +12395,13 @@ DO NOT STOP until all 13 pages + header are implemented:
|
|
|
11662
12395
|
13. \`/account\` \u2014 Account dashboard
|
|
11663
12396
|
14. Header component with cart count + search
|
|
11664
12397
|
|
|
12398
|
+
## Multi-Language
|
|
12399
|
+
If the store has multi-language enabled (check build-store output), implement:
|
|
12400
|
+
- Locale-prefixed routes: /[locale]/products instead of /products
|
|
12401
|
+
- Call client.setLocale(locale) based on URL prefix
|
|
12402
|
+
- Language switcher in header
|
|
12403
|
+
- dir="rtl" on <html> for RTL locales (he, ar)
|
|
12404
|
+
|
|
11665
12405
|
## Step 3: Validate
|
|
11666
12406
|
After building all pages, call \`validate-store\` with the list of files you created to verify nothing is missing.
|
|
11667
12407
|
|
|
@@ -11680,11 +12420,11 @@ After building all pages, call \`validate-store\` with the list of files you cre
|
|
|
11680
12420
|
}
|
|
11681
12421
|
|
|
11682
12422
|
// src/prompts/add-feature.ts
|
|
11683
|
-
var
|
|
12423
|
+
var import_zod12 = require("zod");
|
|
11684
12424
|
var ADD_FEATURE_NAME = "add-feature";
|
|
11685
12425
|
var ADD_FEATURE_DESCRIPTION = "Generate a focused prompt for adding a specific feature to an existing Brainerce store. Returns targeted SDK docs, types, and code examples for just that feature.";
|
|
11686
12426
|
var ADD_FEATURE_SCHEMA = {
|
|
11687
|
-
feature:
|
|
12427
|
+
feature: import_zod12.z.enum([
|
|
11688
12428
|
"products",
|
|
11689
12429
|
"cart",
|
|
11690
12430
|
"checkout",
|
|
@@ -11697,8 +12437,8 @@ var ADD_FEATURE_SCHEMA = {
|
|
|
11697
12437
|
"search",
|
|
11698
12438
|
"tax"
|
|
11699
12439
|
]).describe("The feature to add"),
|
|
11700
|
-
connectionId:
|
|
11701
|
-
currency:
|
|
12440
|
+
connectionId: import_zod12.z.string().optional().describe("Vibe-coded connection ID (starts with vc_)"),
|
|
12441
|
+
currency: import_zod12.z.string().optional().default("USD").describe("Store currency code")
|
|
11702
12442
|
};
|
|
11703
12443
|
var FEATURE_TO_TOPICS = {
|
|
11704
12444
|
products: { topics: ["products"], domains: ["products"] },
|
|
@@ -11813,6 +12553,12 @@ function createServer() {
|
|
|
11813
12553
|
GET_PAGE_CODE_SCHEMA,
|
|
11814
12554
|
handleGetPageCode
|
|
11815
12555
|
);
|
|
12556
|
+
server.tool(
|
|
12557
|
+
GET_INTEGRATION_GUIDE_NAME,
|
|
12558
|
+
GET_INTEGRATION_GUIDE_DESCRIPTION,
|
|
12559
|
+
GET_INTEGRATION_GUIDE_SCHEMA,
|
|
12560
|
+
handleGetIntegrationGuide
|
|
12561
|
+
);
|
|
11816
12562
|
server.resource(
|
|
11817
12563
|
SDK_TYPES_NAME,
|
|
11818
12564
|
SDK_TYPES_URI,
|