@brainerce/mcp-server 2.3.0 → 2.6.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/stdio.js CHANGED
@@ -1,27 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
- var __create = Object.create;
4
- var __defProp = Object.defineProperty;
5
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
- var __getOwnPropNames = Object.getOwnPropertyNames;
7
- var __getProtoOf = Object.getPrototypeOf;
8
- var __hasOwnProp = Object.prototype.hasOwnProperty;
9
- var __copyProps = (to, from, except, desc) => {
10
- if (from && typeof from === "object" || typeof from === "function") {
11
- for (let key of __getOwnPropNames(from))
12
- if (!__hasOwnProp.call(to, key) && key !== except)
13
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
- }
15
- return to;
16
- };
17
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
- // If the importer is in node compatibility mode or this is not an ESM
19
- // file that has been converted to a CommonJS file using a Babel-
20
- // compatible transform (i.e. "__esModule" has not been set), then set
21
- // "default" to the CommonJS "module.exports" for node compatibility.
22
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
- mod
24
- ));
25
3
 
26
4
  // src/bin/stdio.ts
27
5
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
@@ -180,7 +158,7 @@ import {
180
158
  getVariantPrice, getVariantOptions, getStockStatus,
181
159
  getCartItemName, getCartItemImage,
182
160
  getDescriptionContent, isHtmlDescription,
183
- getProductMetafieldValue,
161
+ getProductMetafieldValue, getProductCustomizationFields,
184
162
  } from 'brainerce';
185
163
  \`\`\``;
186
164
  }
@@ -222,6 +200,7 @@ async function startCheckout() {
222
200
  4. Start checkout session (\`startGuestCheckout()\` or \`createCheckout()\`)
223
201
  5. Set shipping address (includes required email) \u2192 \`setShippingAddress()\`
224
202
  6. Select shipping method \u2192 \`selectShippingMethod()\`
203
+ 6b. (Optional) Set checkout custom fields \u2192 \`setCheckoutCustomFields()\` \u2014 surcharges auto-calculated
225
204
  7. Create payment intent \u2192 \`createPaymentIntent()\` \u2192 returns \`{ clientSecret, provider }\`
226
205
  8. Branch on \`provider\`: \`'stripe'\` \u2192 Stripe Elements, \`'grow'\` \u2192 iframe, \`'paypal'\` \u2192 PayPal Buttons
227
206
  9. **Order is created AUTOMATICALLY after payment succeeds (via webhook) \u2014 for ALL providers!**
@@ -312,6 +291,20 @@ function CheckoutPage() {
312
291
  if (!paymentData) return <div>Unable to initialize payment</div>;
313
292
 
314
293
  // Render payment UI based on provider
294
+ if (paymentData.provider === 'sandbox') {
295
+ return (
296
+ <div className="text-center p-6 bg-amber-50 border border-amber-200 rounded-lg">
297
+ <h3 className="font-semibold mb-2">Test Mode</h3>
298
+ <p className="text-sm text-gray-600 mb-4">No real payment will be charged.</p>
299
+ <button onClick={async () => {
300
+ await client.completeGuestCheckout(checkoutId);
301
+ window.location.href = \\\`/order-confirmation?checkout_id=\\\${checkoutId}\\\`;
302
+ }} className="bg-amber-500 text-white px-6 py-2 rounded">
303
+ Complete Test Order
304
+ </button>
305
+ </div>
306
+ );
307
+ }
315
308
  if (paymentData.provider === 'grow') {
316
309
  return <GrowPaymentForm paymentUrl={paymentData.clientSecret} checkoutId={paymentData.checkoutId} />;
317
310
  }
@@ -430,11 +423,12 @@ const growProvider = providers.find(p => p.provider === 'grow');
430
423
  const paypalProvider = providers.find(p => p.provider === 'paypal');
431
424
  \`\`\`
432
425
 
433
- Each provider has: \`id\`, \`provider\` ('stripe'|'grow'|'paypal'), \`name\`, \`publicKey\`, \`stripeAccountId\` (Stripe only), \`supportedMethods\`, \`testMode\`, \`isDefault\`.
426
+ Each provider has: \`id\`, \`provider\` ('stripe'|'grow'|'paypal'|'sandbox'), \`name\`, \`publicKey\`, \`stripeAccountId\` (Stripe only), \`supportedMethods\`, \`testMode\`, \`isDefault\`.
434
427
 
435
428
  - **Stripe:** \`npm install @stripe/stripe-js @stripe/react-stripe-js\` \u2014 \`loadStripe(publicKey, { stripeAccount })\`
436
429
  - **Grow:** No SDK \u2014 uses iframe with payment URL. Supports credit cards, Bit, Apple Pay, Google Pay.
437
- - **PayPal:** \`npm install @paypal/react-paypal-js\` \u2014 \`PayPalScriptProvider\` + \`PayPalButtons\``;
430
+ - **PayPal:** \`npm install @paypal/react-paypal-js\` \u2014 \`PayPalScriptProvider\` + \`PayPalButtons\`
431
+ - **Sandbox:** No SDK needed \u2014 when \`sandboxPaymentsEnabled\` is true, \`getPaymentProviders()\` includes a sandbox provider with \`renderType: 'sandbox'\`. Show a "Complete Test Order" button. Call \`completeGuestCheckout(checkoutId)\` to finalize. Orders are marked \`isTestOrder: true\`.`;
438
432
  }
439
433
  function getProductsSection(_currency) {
440
434
  return `## Products & Variants
@@ -589,6 +583,35 @@ const material = getProductMetafieldValue(product, 'material');
589
583
 
590
584
  **Metafield fields:** \`definitionName\` (display label), \`definitionKey\` (lookup key), \`value\`, \`type\` (IMAGE, GALLERY, URL, COLOR, BOOLEAN, DATE, DATETIME, TEXT, TEXTAREA, NUMBER, DIMENSION, WEIGHT, JSON)
591
585
 
586
+ ### Product Customization Fields (Customer Input)
587
+
588
+ 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.
589
+
590
+ \`\`\`typescript
591
+ import { getProductCustomizationFields } from 'brainerce';
592
+ import type { ProductCustomizationField } from 'brainerce';
593
+
594
+ const fields = getProductCustomizationFields(product);
595
+
596
+ // Render input for each field based on field.type:
597
+ // TEXT/TEXTAREA \u2192 text input, NUMBER \u2192 number input, BOOLEAN \u2192 checkbox,
598
+ // COLOR \u2192 color picker, DATE \u2192 date picker, IMAGE \u2192 file upload, etc.
599
+ // Check field.required, field.minLength, field.maxLength, field.enumValues for validation.
600
+
601
+ // Pass customer values in metadata when adding to cart:
602
+ await client.addToCart(cartId, {
603
+ productId: product.id,
604
+ quantity: 1,
605
+ metadata: { cake_text: 'Happy Birthday!' },
606
+ });
607
+
608
+ // For IMAGE fields, upload first:
609
+ const { url } = await client.uploadCustomizationFile(file);
610
+ // Then use the URL in metadata: metadata: { logo: url }
611
+ \`\`\`
612
+
613
+ **Customization field properties:** \`key\`, \`name\` (display label), \`type\`, \`required\`, \`minLength\`, \`maxLength\`, \`minValue\`, \`maxValue\`, \`enumValues\`, \`defaultValue\`, \`position\`
614
+
592
615
  ### Downloadable / Digital Products
593
616
 
594
617
  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.
@@ -775,9 +798,14 @@ const { bundles } = await client.getCartBundles(cartId);
775
798
  // bundles[].bundleProduct \u2014 the product to offer
776
799
  // bundles[].originalPrice / bundles[].discountedPrice \u2014 prices
777
800
  // bundles[].discountType ('PERCENTAGE' | 'FIXED_AMOUNT') + discountValue
801
+ // bundles[].requiresVariantSelection \u2014 true if customer must pick a variant
802
+ // bundles[].lockedVariant \u2014 set if admin pre-selected a specific variant
803
+ // bundles[].bundleProduct.variants \u2014 available variants (only when requiresVariantSelection)
778
804
 
779
805
  // Add a bundle to cart (applies the discount automatically):
780
806
  await client.addBundleToCart(cartId, bundleOfferId);
807
+ // For products with variants \u2014 pass the selected variantId:
808
+ await client.addBundleToCart(cartId, bundleOfferId, selectedVariantId);
781
809
  // Remove a bundle from cart:
782
810
  await client.removeBundleFromCart(cartId, bundleOfferId);
783
811
  // Detect already-added bundles: check cart.items for metadata?.isBundleItem === true
@@ -802,9 +830,14 @@ const { bumps } = await client.getCheckoutBumps(checkoutId);
802
830
  // bumps[].bumpProduct \u2014 product to offer
803
831
  // bumps[].originalPrice / discountedPrice \u2014 with optional discount
804
832
  // bumps[].title / description \u2014 display text
833
+ // bumps[].requiresVariantSelection \u2014 true if customer must pick a variant
834
+ // bumps[].lockedVariant \u2014 set if admin pre-selected a specific variant
835
+ // bumps[].bumpProduct.variants \u2014 available variants (only when requiresVariantSelection)
805
836
 
806
837
  // Add a bump to cart:
807
838
  await client.addOrderBump(cartId, bumpId);
839
+ // For products with variants \u2014 pass the selected variantId:
840
+ await client.addOrderBump(cartId, bumpId, selectedVariantId);
808
841
  // Remove a bump:
809
842
  await client.removeOrderBump(cartId, bumpId);
810
843
  // Detect already-added bumps: check cart.items for metadata?.isOrderBump === true
@@ -829,10 +862,11 @@ Always check the flag before rendering: \`if (storeInfo?.upsell?.featureName !==
829
862
  | CartRecommendationSection | products/ | Cross-sell grid at bottom of cart page |
830
863
  | FreeShippingBar | cart/ | Progress bar toward free shipping threshold |
831
864
  | CartUpgradeBanner | cart/ | Inline "Upgrade to X for +$Y" per cart item |
832
- | CartBundleOfferCard | cart/ | Discounted bundle offer card in cart |
833
- | OrderBumpCard | checkout/ | Checkbox add-on card in checkout sidebar |
865
+ | CartBundleOfferCard | cart/ | Discounted bundle offer card in cart (with inline variant selector for variable products) |
866
+ | OrderBumpCard | checkout/ | Checkbox add-on card in checkout sidebar (with inline variant selector for variable products) |
834
867
 
835
- ProductRecommendation: \`id\`, \`name\`, \`slug\`, \`basePrice\`, \`salePrice\`, \`images\`, \`type\`, \`inventory\`, \`relationType\`.`;
868
+ ProductRecommendation: \`id\`, \`name\`, \`slug\`, \`basePrice\`, \`salePrice\`, \`images\`, \`type\`, \`inventory\`, \`relationType\`, \`variants?\` (when variant selection needed).
869
+ OrderBump/CartBundleOffer: includes \`requiresVariantSelection\` (boolean) and \`lockedVariant?\` (\`{ id, name, attributes }\`).`;
836
870
  }
837
871
  function getInventorySection() {
838
872
  return `## Inventory, Stock Display & Reservation Countdown
@@ -1972,6 +2006,15 @@ export function getClient(): BrainerceClient {
1972
2006
  }
1973
2007
  return clientInstance;
1974
2008
  }
2009
+ <% if (i18nEnabled) { %>
2010
+
2011
+ /** Initialize client with a specific locale for translated content */
2012
+ export function initClientWithLocale(locale: string): BrainerceClient {
2013
+ const client = getClient();
2014
+ client.setLocale(locale);
2015
+ return client;
2016
+ }
2017
+ <% } %>
1975
2018
 
1976
2019
  // Cart ID helpers (not a security token \u2014 safe in localStorage)
1977
2020
  const CART_ID_KEY = 'brainerce_cart_id';
@@ -2170,6 +2213,10 @@ import type { StoreInfo, Cart, CustomerProfile } from 'brainerce';
2170
2213
  import { getCartTotals } from 'brainerce';
2171
2214
  import { getClient, initClient, setStoredCartId } from '@/lib/brainerce';
2172
2215
  import { checkAuthStatus, proxyLogout } from '@/lib/auth';
2216
+ <% if (i18nEnabled) { %>
2217
+ import { MessagesContext } from '@/lib/translations';
2218
+ import { getMessages, defaultLocale } from '@/i18n';
2219
+ <% } %>
2173
2220
 
2174
2221
  // ---- Store Info Context ----
2175
2222
  interface StoreInfoContextValue {
@@ -2229,7 +2276,11 @@ export function useCart() {
2229
2276
  }
2230
2277
 
2231
2278
  // ---- Provider Component ----
2279
+ <% if (i18nEnabled) { %>
2280
+ export function StoreProvider({ children, locale }: { children: React.ReactNode; locale?: string }) {
2281
+ <% } else { %>
2232
2282
  export function StoreProvider({ children }: { children: React.ReactNode }) {
2283
+ <% } %>
2233
2284
  const [storeInfo, setStoreInfo] = useState<StoreInfo | null>(null);
2234
2285
  const [storeLoading, setStoreLoading] = useState(true);
2235
2286
  const [isLoggedIn, setIsLoggedIn] = useState(false);
@@ -2237,6 +2288,9 @@ export function StoreProvider({ children }: { children: React.ReactNode }) {
2237
2288
  const [authLoading, setAuthLoading] = useState(true);
2238
2289
  const [cart, setCart] = useState<Cart | null>(null);
2239
2290
  const [cartLoading, setCartLoading] = useState(true);
2291
+ <% if (i18nEnabled) { %>
2292
+ const [messages, setMessages] = useState<Record<string, Record<string, string>>>({});
2293
+ <% } %>
2240
2294
 
2241
2295
  // Check auth status via httpOnly cookie (server-side validation)
2242
2296
  const refreshAuth = useCallback(async () => {
@@ -2255,6 +2309,16 @@ export function StoreProvider({ children }: { children: React.ReactNode }) {
2255
2309
  // Initialize client, check auth, and fetch store info
2256
2310
  useEffect(() => {
2257
2311
  const client = initClient();
2312
+ <% if (i18nEnabled) { %>
2313
+
2314
+ // Set locale on SDK client for translated product content
2315
+ if (locale) {
2316
+ client.setLocale(locale);
2317
+ }
2318
+
2319
+ // Load UI message strings for current locale
2320
+ getMessages(locale || defaultLocale).then(setMessages);
2321
+ <% } %>
2258
2322
 
2259
2323
  // Optimistic check: if brainerce_logged_in cookie exists, assume logged in
2260
2324
  // while we validate the actual token server-side
@@ -2271,7 +2335,11 @@ export function StoreProvider({ children }: { children: React.ReactNode }) {
2271
2335
  .then(setStoreInfo)
2272
2336
  .catch(console.error)
2273
2337
  .finally(() => setStoreLoading(false));
2338
+ <% if (i18nEnabled) { %>
2339
+ }, [refreshAuth, locale]);
2340
+ <% } else { %>
2274
2341
  }, [refreshAuth]);
2342
+ <% } %>
2275
2343
 
2276
2344
  // Cart management
2277
2345
  const refreshCart = useCallback(async () => {
@@ -2324,6 +2392,21 @@ export function StoreProvider({ children }: { children: React.ReactNode }) {
2324
2392
 
2325
2393
  const totals = cart ? getCartTotals(cart) : { subtotal: 0, discount: 0, shipping: 0, total: 0 };
2326
2394
 
2395
+ <% if (i18nEnabled) { %>
2396
+ return (
2397
+ <MessagesContext.Provider value={messages}>
2398
+ <StoreInfoContext.Provider value={{ storeInfo, loading: storeLoading }}>
2399
+ <AuthContext.Provider value={{ isLoggedIn, authLoading, customer, login, logout }}>
2400
+ <CartContext.Provider
2401
+ value={{ cart, cartLoading, refreshCart, itemCount, totals }}
2402
+ >
2403
+ {children}
2404
+ </CartContext.Provider>
2405
+ </AuthContext.Provider>
2406
+ </StoreInfoContext.Provider>
2407
+ </MessagesContext.Provider>
2408
+ );
2409
+ <% } else { %>
2327
2410
  return (
2328
2411
  <StoreInfoContext.Provider value={{ storeInfo, loading: storeLoading }}>
2329
2412
  <AuthContext.Provider value={{ isLoggedIn, authLoading, customer, login, logout }}>
@@ -2335,6 +2418,7 @@ export function StoreProvider({ children }: { children: React.ReactNode }) {
2335
2418
  </AuthContext.Provider>
2336
2419
  </StoreInfoContext.Provider>
2337
2420
  );
2421
+ <% } %>
2338
2422
  }
2339
2423
  `,
2340
2424
  "src/app/page.tsx": `'use client';
@@ -2436,7 +2520,81 @@ export default function HomePage() {
2436
2520
  );
2437
2521
  }
2438
2522
  `,
2439
- "src/app/layout.tsx.ejs": `import type { Metadata } from 'next';
2523
+ "src/app/layout.tsx.ejs": `<% if (i18nEnabled) { %>
2524
+ import type { Metadata } from 'next';
2525
+ <%- fontImport %>
2526
+ import { StoreProvider } from '@/providers/store-provider';
2527
+ import { Header } from '@/components/layout/header';
2528
+ import { Footer } from '@/components/layout/footer';
2529
+ import { getDirection, supportedLocales } from '@/i18n';
2530
+ import '../globals.css';
2531
+
2532
+ <%- fontVariable %>
2533
+
2534
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com';
2535
+
2536
+ export const metadata: Metadata = {
2537
+ metadataBase: new URL(baseUrl),
2538
+ title: {
2539
+ default: 'My Store',
2540
+ template: \`%s | My Store\`,
2541
+ },
2542
+ description: 'My Store',
2543
+ alternates: {
2544
+ canonical: '/',
2545
+ },
2546
+ openGraph: {
2547
+ siteName: 'My Store',
2548
+ type: 'website',
2549
+ },
2550
+ robots: {
2551
+ index: true,
2552
+ follow: true,
2553
+ },
2554
+ };
2555
+
2556
+ const organizationJsonLd = {
2557
+ '@context': 'https://schema.org',
2558
+ '@type': 'Organization',
2559
+ name: 'My Store',
2560
+ url: baseUrl,
2561
+ };
2562
+
2563
+ export function generateStaticParams() {
2564
+ return supportedLocales.map((locale) => ({ locale }));
2565
+ }
2566
+
2567
+ export default function RootLayout({
2568
+ children,
2569
+ params,
2570
+ }: {
2571
+ children: React.ReactNode;
2572
+ params: { locale: string };
2573
+ }) {
2574
+ const dir = getDirection(params.locale);
2575
+
2576
+ return (
2577
+ <html lang={params.locale} dir={dir}>
2578
+ <head>
2579
+ <script
2580
+ type="application/ld+json"
2581
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(organizationJsonLd) }}
2582
+ />
2583
+ </head>
2584
+ <body className={font.className}>
2585
+ <StoreProvider locale={params.locale}>
2586
+ <div className="min-h-screen flex flex-col">
2587
+ <Header />
2588
+ <main className="flex-1">{children}</main>
2589
+ <Footer />
2590
+ </div>
2591
+ </StoreProvider>
2592
+ </body>
2593
+ </html>
2594
+ );
2595
+ }
2596
+ <% } else { %>
2597
+ import type { Metadata } from 'next';
2440
2598
  <%- fontImport %>
2441
2599
  import { StoreProvider } from '@/providers/store-provider';
2442
2600
  import { Header } from '@/components/layout/header';
@@ -2500,6 +2658,7 @@ export default function RootLayout({
2500
2658
  </html>
2501
2659
  );
2502
2660
  }
2661
+ <% } %>
2503
2662
  `,
2504
2663
  "src/app/products/page.tsx": `'use client';
2505
2664
 
@@ -3400,13 +3559,13 @@ function CheckoutContent() {
3400
3559
  }, [checkout?.id, storeInfo?.upsell?.checkoutOrderBumpEnabled]);
3401
3560
 
3402
3561
  // Handle bump toggle
3403
- async function handleBumpToggle(bumpId: string, add: boolean) {
3562
+ async function handleBumpToggle(bumpId: string, add: boolean, variantId?: string) {
3404
3563
  if (!cart?.id || bumpLoading) return;
3405
3564
  try {
3406
3565
  setBumpLoading(bumpId);
3407
3566
  const client = getClient();
3408
3567
  if (add) {
3409
- await client.addOrderBump(cart.id, bumpId);
3568
+ await client.addOrderBump(cart.id, bumpId, variantId);
3410
3569
  setAddedBumpIds((prev) => new Set([...prev, bumpId]));
3411
3570
  } else {
3412
3571
  await client.removeOrderBump(cart.id, bumpId);
@@ -4104,6 +4263,23 @@ function OrderConfirmationContent() {
4104
4263
  client.handlePaymentSuccess(checkoutId!);
4105
4264
  await refreshCart();
4106
4265
 
4266
+ // For redirect-based payment providers (e.g. CardCom), the customer
4267
+ // returns with provider params in the URL (lowprofilecode, etc.).
4268
+ // Send these to the backend for server-side verification via the
4269
+ // provider's API (e.g. GetLpResult) \u2014 never trust URL params alone.
4270
+ const lowProfileCode =
4271
+ searchParams.get('lowprofilecode') || searchParams.get('LowProfileCode');
4272
+ if (lowProfileCode) {
4273
+ try {
4274
+ await client.confirmSdkPayment(checkoutId!, {
4275
+ paymentIntentId: lowProfileCode,
4276
+ });
4277
+ } catch (err) {
4278
+ console.warn('Redirect payment confirmation failed:', err);
4279
+ // Don't block \u2014 webhook may still process the payment
4280
+ }
4281
+ }
4282
+
4107
4283
  const orderResult = await client.waitForOrder(checkoutId!, {
4108
4284
  maxWaitMs: 30000,
4109
4285
  });
@@ -5620,7 +5796,62 @@ export async function POST(request: NextRequest) {
5620
5796
  return response;
5621
5797
  }
5622
5798
  `,
5623
- "src/middleware.ts": `import { NextRequest, NextResponse } from 'next/server';
5799
+ "src/middleware.ts.ejs": `<% if (i18nEnabled) { %>
5800
+ import { NextRequest, NextResponse } from 'next/server';
5801
+
5802
+ const TOKEN_COOKIE = 'brainerce_customer_token';
5803
+ const PROTECTED_PATHS = ['/account'];
5804
+ const supportedLocales = <%- supportedLocales %>;
5805
+ const defaultLocale = '<%= defaultLocale %>';
5806
+
5807
+ function getLocaleFromPath(pathname: string): string | null {
5808
+ const segment = pathname.split('/')[1];
5809
+ return supportedLocales.includes(segment) ? segment : null;
5810
+ }
5811
+
5812
+ export function middleware(request: NextRequest) {
5813
+ const { pathname } = request.nextUrl;
5814
+
5815
+ // Skip static files and API routes
5816
+ if (
5817
+ pathname.startsWith('/api/') ||
5818
+ pathname.startsWith('/_next/') ||
5819
+ pathname.includes('.')
5820
+ ) {
5821
+ return NextResponse.next();
5822
+ }
5823
+
5824
+ const pathnameLocale = getLocaleFromPath(pathname);
5825
+
5826
+ // Redirect to default locale if no locale prefix
5827
+ if (!pathnameLocale) {
5828
+ const url = request.nextUrl.clone();
5829
+ url.pathname = \`/\${defaultLocale}\${pathname}\`;
5830
+ return NextResponse.redirect(url);
5831
+ }
5832
+
5833
+ // Auth protection (with locale prefix)
5834
+ const pathWithoutLocale = pathname.replace(\`/\${pathnameLocale}\`, '') || '/';
5835
+ const isProtected = PROTECTED_PATHS.some((p) => pathWithoutLocale.startsWith(p));
5836
+ if (isProtected) {
5837
+ const token = request.cookies.get(TOKEN_COOKIE);
5838
+ if (!token?.value) {
5839
+ const loginUrl = new URL(\`/\${pathnameLocale}/login\`, request.url);
5840
+ return NextResponse.redirect(loginUrl);
5841
+ }
5842
+ }
5843
+
5844
+ // Set locale header for server components
5845
+ const response = NextResponse.next();
5846
+ response.headers.set('x-locale', pathnameLocale);
5847
+ return response;
5848
+ }
5849
+
5850
+ export const config = {
5851
+ matcher: ['/((?!_next|api|.*\\\\..*).*)'],
5852
+ };
5853
+ <% } else { %>
5854
+ import { NextRequest, NextResponse } from 'next/server';
5624
5855
 
5625
5856
  const TOKEN_COOKIE = 'brainerce_customer_token';
5626
5857
 
@@ -5645,6 +5876,7 @@ export function middleware(request: NextRequest) {
5645
5876
  export const config = {
5646
5877
  matcher: ['/account/:path*'],
5647
5878
  };
5879
+ <% } %>
5648
5880
  `,
5649
5881
  "src/components/products/product-card.tsx": `'use client';
5650
5882
 
@@ -7506,7 +7738,9 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
7506
7738
  initialized.current = true;
7507
7739
 
7508
7740
  const client = getClient();
7509
- const successUrl = \`\${window.location.origin}/order-confirmation?checkout_id=\${checkoutId}\`;
7741
+ const iframeSuccessUrl = \`\${window.location.origin}/payment-complete?checkout_id=\${checkoutId}\`;
7742
+ const iframeFailedUrl = \`\${window.location.origin}/payment-complete?checkout_id=\${checkoutId}&failed=true\`;
7743
+ const redirectSuccessUrl = \`\${window.location.origin}/order-confirmation?checkout_id=\${checkoutId}\`;
7510
7744
  const cancelUrl = \`\${window.location.origin}/checkout?checkout_id=\${checkoutId}&canceled=true\`;
7511
7745
 
7512
7746
  let sdkInitDone = false;
@@ -7667,8 +7901,19 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
7667
7901
  });
7668
7902
 
7669
7903
  // C) Create payment intent (starts wallet timer)
7670
- const intentPromise = client
7671
- .createPaymentIntent(checkoutId, { successUrl, cancelUrl })
7904
+ // Wait for provider info so we can choose the right success URL:
7905
+ // iframe providers redirect inside the iframe to /payment-complete (postMessage),
7906
+ // redirect providers go straight to /order-confirmation.
7907
+ const intentPromise = providerPromise
7908
+ .then((providerSdk) => {
7909
+ const isIframe = providerSdk?.renderType === 'iframe';
7910
+ const successUrl = isIframe ? iframeSuccessUrl : redirectSuccessUrl;
7911
+ const failedUrl = isIframe ? iframeFailedUrl : cancelUrl;
7912
+ return client.createPaymentIntent(checkoutId, {
7913
+ successUrl,
7914
+ cancelUrl: failedUrl,
7915
+ });
7916
+ })
7672
7917
  .then((intent) => {
7673
7918
  setPaymentIntent(intent);
7674
7919
  return intent;
@@ -7693,6 +7938,36 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
7693
7938
  window.location.href = intent.clientSecret;
7694
7939
  return;
7695
7940
  }
7941
+
7942
+ // Iframe mode: listen for postMessage from the /payment-complete callback
7943
+ // page that loads inside the iframe after the provider redirects on completion.
7944
+ if (sdk.renderType === 'iframe') {
7945
+ const handleMessage = (event: MessageEvent) => {
7946
+ if (event.origin !== window.location.origin) return;
7947
+ if (event.data?.type !== 'brainerce:payment-complete') return;
7948
+
7949
+ const params = event.data.data as Record<string, string> | undefined;
7950
+ if (params?.failed === 'true') {
7951
+ setError(t('paymentError'));
7952
+ return;
7953
+ }
7954
+
7955
+ // Map provider-specific params to normalized format for
7956
+ // server-side verification (e.g. CardCom lowprofilecode \u2192 paymentIntentId)
7957
+ const lowProfileCode = params?.lowprofilecode || params?.LowProfileCode;
7958
+ const normalized: Record<string, unknown> = { ...params };
7959
+ if (lowProfileCode) {
7960
+ normalized.paymentIntentId = lowProfileCode;
7961
+ }
7962
+
7963
+ // Trigger server-side verification + order creation
7964
+ handleSuccess(normalized);
7965
+ };
7966
+ window.addEventListener('message', handleMessage);
7967
+ cleanups.push(() => window.removeEventListener('message', handleMessage));
7968
+ return;
7969
+ }
7970
+
7696
7971
  if (sdk.renderType !== 'sdk-widget' || !sdk.globalName) return;
7697
7972
 
7698
7973
  // Store for retryRender from onError callback
@@ -7843,15 +8118,45 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
7843
8118
 
7844
8119
  if (sdk.renderType === 'iframe') {
7845
8120
  return (
7846
- <div className={cn('py-4', className)}>
7847
- <iframe
7848
- src={paymentIntent.clientSecret}
7849
- className="w-full border-0"
7850
- style={{ minHeight: '500px' }}
7851
- title={t('payment')}
7852
- allow="payment"
7853
- />
7854
- </div>
8121
+ <>
8122
+ {/* Modal overlay */}
8123
+ <div className="fixed inset-0 z-50 flex items-start justify-center overflow-y-auto bg-black/50 py-6 backdrop-blur-sm">
8124
+ <div className="relative mx-4 w-full max-w-md rounded-2xl bg-white shadow-2xl">
8125
+ {/* Close button */}
8126
+ <button
8127
+ onClick={() => {
8128
+ window.location.href = \`/checkout?checkout_id=\${checkoutId}&canceled=true\`;
8129
+ }}
8130
+ 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"
8131
+ aria-label="Close"
8132
+ >
8133
+ <svg
8134
+ width="14"
8135
+ height="14"
8136
+ viewBox="0 0 14 14"
8137
+ fill="none"
8138
+ stroke="currentColor"
8139
+ strokeWidth="2"
8140
+ strokeLinecap="round"
8141
+ >
8142
+ <path d="M1 1l12 12M13 1L1 13" />
8143
+ </svg>
8144
+ </button>
8145
+ <iframe
8146
+ src={paymentIntent.clientSecret}
8147
+ className="w-full rounded-2xl border-0"
8148
+ style={{ height: '80vh' }}
8149
+ title={t('payment')}
8150
+ allow="payment"
8151
+ />
8152
+ </div>
8153
+ </div>
8154
+ {/* Placeholder so the checkout layout doesn't collapse */}
8155
+ <div className={cn('flex flex-col items-center justify-center py-12', className)}>
8156
+ <LoadingSpinner size="lg" />
8157
+ <p className="text-muted-foreground mt-4 text-sm">{t('preparingPayment')}</p>
8158
+ </div>
8159
+ </>
7855
8160
  );
7856
8161
  }
7857
8162
 
@@ -9978,10 +10283,10 @@ export function CartUpgradeBanner({
9978
10283
  `,
9979
10284
  "src/components/cart/cart-bundle-offer.tsx": `'use client';
9980
10285
 
9981
- import { useState } from 'react';
10286
+ import { useState, useMemo } from 'react';
9982
10287
  import Image from 'next/image';
9983
10288
  import type { CartBundleOffer as CartBundleOfferType } from 'brainerce';
9984
- import { formatPrice } from 'brainerce';
10289
+ import { formatPrice, getVariantOptions } from 'brainerce';
9985
10290
  import { useStoreInfo } from '@/providers/store-provider';
9986
10291
  import { useTranslations } from '@/lib/translations';
9987
10292
  import { cn } from '@/lib/utils';
@@ -9998,28 +10303,114 @@ export function CartBundleOfferCard({ offer, cartId, onAdd, className }: CartBun
9998
10303
  const t = useTranslations('cart');
9999
10304
  const currency = storeInfo?.currency || 'USD';
10000
10305
  const [adding, setAdding] = useState(false);
10306
+ const [selectedVariantId, setSelectedVariantId] = useState<string | null>(null);
10001
10307
 
10002
10308
  const product = offer.bundleProduct;
10309
+ const variants = product.variants;
10310
+ const requiresSelection = offer.requiresVariantSelection && variants && variants.length > 0;
10311
+
10312
+ // Build attribute groups from variants
10313
+ const attributeGroups = useMemo(() => {
10314
+ if (!requiresSelection || !variants) return [];
10315
+ const groups = new Map<string, Set<string>>();
10316
+ for (const v of variants) {
10317
+ const opts = getVariantOptions(v as any);
10318
+ for (const opt of opts) {
10319
+ if (!groups.has(opt.name)) groups.set(opt.name, new Set());
10320
+ groups.get(opt.name)!.add(opt.value);
10321
+ }
10322
+ }
10323
+ return Array.from(groups.entries()).map(([name, values]) => ({
10324
+ name,
10325
+ values: Array.from(values),
10326
+ }));
10327
+ }, [requiresSelection, variants]);
10328
+
10329
+ const [selectedAttrs, setSelectedAttrs] = useState<Record<string, string>>({});
10330
+
10331
+ const selectedVariant = useMemo(() => {
10332
+ if (!requiresSelection || !variants) return null;
10333
+ return (
10334
+ variants.find((v) => {
10335
+ const opts = getVariantOptions(v as any);
10336
+ return attributeGroups.every((group) => {
10337
+ const opt = opts.find((o) => o.name === group.name);
10338
+ return opt && selectedAttrs[group.name] === opt.value;
10339
+ });
10340
+ }) ?? null
10341
+ );
10342
+ }, [requiresSelection, variants, selectedAttrs, attributeGroups]);
10343
+
10344
+ const effectiveVariantId = selectedVariant?.id ?? selectedVariantId;
10345
+
10346
+ function handleAttrSelect(attrName: string, value: string) {
10347
+ const next = { ...selectedAttrs, [attrName]: value };
10348
+ setSelectedAttrs(next);
10349
+ if (variants) {
10350
+ const match = variants.find((v) => {
10351
+ const opts = getVariantOptions(v as any);
10352
+ return attributeGroups.every((group) => {
10353
+ const opt = opts.find((o) => o.name === group.name);
10354
+ return opt && next[group.name] === opt.value;
10355
+ });
10356
+ });
10357
+ setSelectedVariantId(match?.id ?? null);
10358
+ }
10359
+ }
10360
+
10361
+ // Compute display prices
10362
+ const { displayOriginal, displayDiscounted, discountLabel } = useMemo(() => {
10363
+ let effectivePrice: number;
10364
+ if (selectedVariant) {
10365
+ const vSale = selectedVariant.salePrice ? parseFloat(selectedVariant.salePrice) : null;
10366
+ const vPrice = selectedVariant.price ? parseFloat(selectedVariant.price) : null;
10367
+ effectivePrice = vSale ?? vPrice ?? parseFloat(offer.originalPrice);
10368
+ } else {
10369
+ effectivePrice = parseFloat(offer.originalPrice);
10370
+ }
10371
+
10372
+ let discounted: number;
10373
+ if (offer.discountType === 'PERCENTAGE') {
10374
+ discounted = effectivePrice * (1 - parseFloat(offer.discountValue) / 100);
10375
+ } else {
10376
+ discounted = Math.max(0, effectivePrice - parseFloat(offer.discountValue));
10377
+ }
10378
+
10379
+ const label =
10380
+ offer.discountType === 'PERCENTAGE'
10381
+ ? \`\${offer.discountValue}%\`
10382
+ : (formatPrice(parseFloat(offer.discountValue), { currency }) as string);
10383
+
10384
+ return { displayOriginal: effectivePrice, displayDiscounted: discounted, discountLabel: label };
10385
+ }, [selectedVariant, offer.originalPrice, offer.discountType, offer.discountValue, currency]);
10386
+
10387
+ const isOos =
10388
+ selectedVariant?.inventory?.trackingMode !== 'NOT_TRACKED' &&
10389
+ selectedVariant?.inventory?.available != null &&
10390
+ selectedVariant.inventory.available <= 0;
10391
+
10392
+ const lockedLabel =
10393
+ offer.lockedVariant?.name ??
10394
+ (offer.lockedVariant?.attributes
10395
+ ? Object.values(offer.lockedVariant.attributes).join(' / ')
10396
+ : null);
10397
+
10003
10398
  const firstImage = product.images?.[0];
10004
10399
  const imageUrl = firstImage
10005
10400
  ? typeof firstImage === 'string'
10006
10401
  ? firstImage
10007
10402
  : firstImage.url
10008
10403
  : null;
10009
- const originalPrice = parseFloat(offer.originalPrice);
10010
- const discountedPrice = parseFloat(offer.discountedPrice);
10011
- const discountLabel =
10012
- offer.discountType === 'PERCENTAGE'
10013
- ? \`\${offer.discountValue}%\`
10014
- : (formatPrice(parseFloat(offer.discountValue), { currency }) as string);
10404
+
10405
+ const canAdd = !requiresSelection || !!effectiveVariantId;
10015
10406
 
10016
10407
  async function handleAdd() {
10017
- if (adding) return;
10408
+ if (adding || !canAdd || isOos) return;
10018
10409
  try {
10019
10410
  setAdding(true);
10020
10411
  const { getClient } = await import('@/lib/brainerce');
10021
10412
  const client = getClient();
10022
- await client.addBundleToCart(cartId, offer.id);
10413
+ await client.addBundleToCart(cartId, offer.id, effectiveVariantId ?? undefined);
10023
10414
  onAdd();
10024
10415
  } catch (err) {
10025
10416
  console.error('Failed to add bundle item:', err);
@@ -10029,70 +10420,121 @@ export function CartBundleOfferCard({ offer, cartId, onAdd, className }: CartBun
10029
10420
  }
10030
10421
 
10031
10422
  return (
10032
- <div
10033
- className={cn(
10034
- 'bg-background border-border flex items-center gap-4 rounded-lg border p-4',
10035
- className
10036
- )}
10037
- >
10038
- {/* Product image */}
10039
- <div className="bg-muted relative h-16 w-16 flex-shrink-0 overflow-hidden rounded">
10040
- {imageUrl ? (
10041
- <Image src={imageUrl} alt={product.name} fill sizes="64px" className="object-cover" />
10042
- ) : (
10043
- <div className="text-muted-foreground flex h-full w-full items-center justify-center">
10044
- <svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
10045
- <path
10046
- strokeLinecap="round"
10047
- strokeLinejoin="round"
10048
- strokeWidth={1.5}
10049
- 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"
10050
- />
10051
- </svg>
10052
- </div>
10053
- )}
10054
- </div>
10423
+ <div className={cn('bg-background border-border rounded-lg border p-4', className)}>
10424
+ <div className="flex items-center gap-4">
10425
+ {/* Product image */}
10426
+ <div className="bg-muted relative h-16 w-16 flex-shrink-0 overflow-hidden rounded">
10427
+ {imageUrl ? (
10428
+ <Image src={imageUrl} alt={product.name} fill sizes="64px" className="object-cover" />
10429
+ ) : (
10430
+ <div className="text-muted-foreground flex h-full w-full items-center justify-center">
10431
+ <svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
10432
+ <path
10433
+ strokeLinecap="round"
10434
+ strokeLinejoin="round"
10435
+ strokeWidth={1.5}
10436
+ 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"
10437
+ />
10438
+ </svg>
10439
+ </div>
10440
+ )}
10441
+ </div>
10055
10442
 
10056
- {/* Details */}
10057
- <div className="min-w-0 flex-1">
10058
- <p className="text-foreground text-sm font-medium">{offer.name}</p>
10059
- {offer.description && (
10060
- <p className="text-muted-foreground mt-0.5 text-xs">{offer.description}</p>
10061
- )}
10062
- <div className="mt-1 flex items-center gap-2">
10063
- <span className="text-muted-foreground text-sm line-through">
10064
- {formatPrice(originalPrice, { currency }) as string}
10065
- </span>
10066
- <span className="text-foreground text-sm font-semibold">
10067
- {formatPrice(discountedPrice, { currency }) as string}
10068
- </span>
10069
- <span className="bg-destructive/10 text-destructive rounded px-1.5 py-0.5 text-xs font-medium">
10070
- -{discountLabel}
10071
- </span>
10443
+ {/* Details */}
10444
+ <div className="min-w-0 flex-1">
10445
+ <p className="text-foreground text-sm font-medium">{offer.name}</p>
10446
+ {offer.description && (
10447
+ <p className="text-muted-foreground mt-0.5 text-xs">{offer.description}</p>
10448
+ )}
10449
+ {lockedLabel && <p className="text-muted-foreground mt-0.5 text-xs">{lockedLabel}</p>}
10450
+ <div className="mt-1 flex items-center gap-2">
10451
+ <span className="text-muted-foreground text-sm line-through">
10452
+ {formatPrice(displayOriginal, { currency }) as string}
10453
+ </span>
10454
+ <span className="text-foreground text-sm font-semibold">
10455
+ {formatPrice(displayDiscounted, { currency }) as string}
10456
+ </span>
10457
+ <span className="bg-destructive/10 text-destructive rounded px-1.5 py-0.5 text-xs font-medium">
10458
+ -{discountLabel}
10459
+ </span>
10460
+ </div>
10072
10461
  </div>
10462
+
10463
+ {/* Add button */}
10464
+ <button
10465
+ type="button"
10466
+ onClick={handleAdd}
10467
+ disabled={adding || !canAdd || isOos}
10468
+ className={cn(
10469
+ 'bg-primary text-primary-foreground flex-shrink-0 rounded px-4 py-2 text-xs font-medium transition-opacity hover:opacity-90',
10470
+ 'disabled:cursor-not-allowed disabled:opacity-50'
10471
+ )}
10472
+ >
10473
+ {adding
10474
+ ? t('addingBundle')
10475
+ : requiresSelection && !canAdd
10476
+ ? t('selectOptions') || 'Select options'
10477
+ : t('addBundleItem')}
10478
+ </button>
10073
10479
  </div>
10074
10480
 
10075
- {/* Add button */}
10076
- <button
10077
- type="button"
10078
- onClick={handleAdd}
10079
- disabled={adding}
10080
- className={cn(
10081
- 'bg-primary text-primary-foreground flex-shrink-0 rounded px-4 py-2 text-xs font-medium transition-opacity hover:opacity-90',
10082
- 'disabled:cursor-not-allowed disabled:opacity-50'
10083
- )}
10084
- >
10085
- {adding ? t('addingBundle') : t('addBundleItem')}
10086
- </button>
10481
+ {/* Compact variant selector */}
10482
+ {requiresSelection && (
10483
+ <div className="mt-3 space-y-1.5 ps-20">
10484
+ {attributeGroups.map((group) => (
10485
+ <div key={group.name} className="flex flex-wrap items-center gap-1.5">
10486
+ <span className="text-muted-foreground text-xs">{group.name}:</span>
10487
+ {group.values.map((value) => {
10488
+ const isSelected = selectedAttrs[group.name] === value;
10489
+ const variantForValue = variants?.find((v) => {
10490
+ const opts = getVariantOptions(v as any);
10491
+ const matchesValue = opts.some((o) => o.name === group.name && o.value === value);
10492
+ if (!matchesValue) return false;
10493
+ return Object.entries(selectedAttrs).every(([k, sv]) => {
10494
+ if (k === group.name) return true;
10495
+ return opts.some((o) => o.name === k && o.value === sv);
10496
+ });
10497
+ });
10498
+ const isVariantOos =
10499
+ variantForValue?.inventory?.trackingMode !== 'NOT_TRACKED' &&
10500
+ variantForValue?.inventory?.available != null &&
10501
+ variantForValue.inventory.available <= 0;
10502
+
10503
+ return (
10504
+ <button
10505
+ key={value}
10506
+ type="button"
10507
+ onClick={() => handleAttrSelect(group.name, value)}
10508
+ disabled={isVariantOos}
10509
+ className={cn(
10510
+ 'rounded-full border px-2.5 py-0.5 text-xs transition-colors',
10511
+ isSelected
10512
+ ? 'border-primary bg-primary text-primary-foreground'
10513
+ : 'border-border text-foreground hover:border-primary/50',
10514
+ isVariantOos && 'cursor-not-allowed line-through opacity-40'
10515
+ )}
10516
+ >
10517
+ {value}
10518
+ </button>
10519
+ );
10520
+ })}
10521
+ </div>
10522
+ ))}
10523
+ {isOos && effectiveVariantId && (
10524
+ <p className="text-destructive text-xs">{t('outOfStock') || 'Out of stock'}</p>
10525
+ )}
10526
+ </div>
10527
+ )}
10087
10528
  </div>
10088
10529
  );
10089
10530
  }
10090
10531
  `,
10091
10532
  "src/components/checkout/order-bump-card.tsx": `'use client';
10092
10533
 
10534
+ import { useState, useMemo } from 'react';
10093
10535
  import Image from 'next/image';
10094
- import type { OrderBump } from 'brainerce';
10095
- import { formatPrice } from 'brainerce';
10536
+ import type { OrderBump, RecommendationVariant } from 'brainerce';
10537
+ import { formatPrice, getVariantOptions } from 'brainerce';
10096
10538
  import { useStoreInfo } from '@/providers/store-provider';
10097
10539
  import { useTranslations } from '@/lib/translations';
10098
10540
  import { cn } from '@/lib/utils';
@@ -10100,7 +10542,7 @@ import { cn } from '@/lib/utils';
10100
10542
  interface OrderBumpCardProps {
10101
10543
  bump: OrderBump;
10102
10544
  isAdded: boolean;
10103
- onToggle: (bumpId: string, add: boolean) => void;
10545
+ onToggle: (bumpId: string, add: boolean, variantId?: string) => void;
10104
10546
  loading: boolean;
10105
10547
  className?: string;
10106
10548
  }
@@ -10109,66 +10551,225 @@ export function OrderBumpCard({ bump, isAdded, onToggle, loading, className }: O
10109
10551
  const { storeInfo } = useStoreInfo();
10110
10552
  const t = useTranslations('checkout');
10111
10553
  const currency = storeInfo?.currency || 'USD';
10554
+ const [selectedVariantId, setSelectedVariantId] = useState<string | null>(null);
10112
10555
 
10113
10556
  const product = bump.bumpProduct;
10557
+ const variants = product.variants;
10558
+ const requiresSelection = bump.requiresVariantSelection && variants && variants.length > 0;
10559
+
10560
+ // Build attribute groups from variants for pill selector
10561
+ const attributeGroups = useMemo(() => {
10562
+ if (!requiresSelection || !variants) return [];
10563
+ const groups = new Map<string, Set<string>>();
10564
+ for (const v of variants) {
10565
+ const opts = getVariantOptions(v as any);
10566
+ for (const opt of opts) {
10567
+ if (!groups.has(opt.name)) groups.set(opt.name, new Set());
10568
+ groups.get(opt.name)!.add(opt.value);
10569
+ }
10570
+ }
10571
+ return Array.from(groups.entries()).map(([name, values]) => ({
10572
+ name,
10573
+ values: Array.from(values),
10574
+ }));
10575
+ }, [requiresSelection, variants]);
10576
+
10577
+ // Track selected attributes
10578
+ const [selectedAttrs, setSelectedAttrs] = useState<Record<string, string>>({});
10579
+
10580
+ // Find matching variant based on selected attributes
10581
+ const selectedVariant = useMemo(() => {
10582
+ if (!requiresSelection || !variants) return null;
10583
+ return (
10584
+ variants.find((v) => {
10585
+ const opts = getVariantOptions(v as any);
10586
+ return attributeGroups.every((group) => {
10587
+ const opt = opts.find((o) => o.name === group.name);
10588
+ return opt && selectedAttrs[group.name] === opt.value;
10589
+ });
10590
+ }) ?? null
10591
+ );
10592
+ }, [requiresSelection, variants, selectedAttrs, attributeGroups]);
10593
+
10594
+ // Update selectedVariantId when variant match changes
10595
+ const effectiveVariantId = selectedVariant?.id ?? selectedVariantId;
10596
+
10597
+ function handleAttrSelect(attrName: string, value: string) {
10598
+ const next = { ...selectedAttrs, [attrName]: value };
10599
+ setSelectedAttrs(next);
10600
+ // Find matching variant with new selection
10601
+ if (variants) {
10602
+ const match = variants.find((v) => {
10603
+ const opts = getVariantOptions(v as any);
10604
+ return attributeGroups.every((group) => {
10605
+ const opt = opts.find((o) => o.name === group.name);
10606
+ return opt && next[group.name] === opt.value;
10607
+ });
10608
+ });
10609
+ setSelectedVariantId(match?.id ?? null);
10610
+ }
10611
+ }
10612
+
10613
+ // Compute display price
10614
+ const { displayOriginal, displayDiscounted } = useMemo(() => {
10615
+ let effectivePrice: number;
10616
+ if (selectedVariant) {
10617
+ const vSale = selectedVariant.salePrice ? parseFloat(selectedVariant.salePrice) : null;
10618
+ const vPrice = selectedVariant.price ? parseFloat(selectedVariant.price) : null;
10619
+ effectivePrice = vSale ?? vPrice ?? parseFloat(bump.originalPrice);
10620
+ } else {
10621
+ effectivePrice = parseFloat(bump.originalPrice);
10622
+ }
10623
+
10624
+ let discounted: number | null = null;
10625
+ if (bump.discountType && bump.discountValue) {
10626
+ const dv = parseFloat(bump.discountValue);
10627
+ if (bump.discountType === 'PERCENTAGE') {
10628
+ discounted = effectivePrice * (1 - dv / 100);
10629
+ } else {
10630
+ discounted = Math.max(0, effectivePrice - dv);
10631
+ }
10632
+ }
10633
+
10634
+ return { displayOriginal: effectivePrice, displayDiscounted: discounted };
10635
+ }, [selectedVariant, bump.originalPrice, bump.discountType, bump.discountValue]);
10636
+
10637
+ // Check if selected variant is out of stock
10638
+ const isOos =
10639
+ selectedVariant?.inventory?.trackingMode !== 'NOT_TRACKED' &&
10640
+ selectedVariant?.inventory?.available != null &&
10641
+ selectedVariant.inventory.available <= 0;
10642
+
10643
+ // Locked variant label
10644
+ const lockedLabel =
10645
+ bump.lockedVariant?.name ??
10646
+ (bump.lockedVariant?.attributes
10647
+ ? Object.values(bump.lockedVariant.attributes).join(' / ')
10648
+ : null);
10649
+
10114
10650
  const firstImage = product.images?.[0];
10115
10651
  const imageUrl = firstImage
10116
10652
  ? typeof firstImage === 'string'
10117
10653
  ? firstImage
10118
10654
  : firstImage.url
10119
10655
  : null;
10120
- const originalPrice = parseFloat(bump.originalPrice);
10121
- const hasDiscount = bump.discountedPrice != null;
10122
- const discountedPrice = hasDiscount ? parseFloat(bump.discountedPrice!) : null;
10656
+
10657
+ const canToggle = !requiresSelection || !!effectiveVariantId;
10123
10658
 
10124
10659
  return (
10125
- <label
10660
+ <div
10126
10661
  className={cn(
10127
- 'border-border hover:border-primary/50 flex cursor-pointer items-start gap-3 rounded-lg border p-3 transition-colors',
10662
+ 'border-border hover:border-primary/50 rounded-lg border p-3 transition-colors',
10128
10663
  isAdded && 'border-primary bg-primary/5',
10129
10664
  loading && 'pointer-events-none opacity-60',
10130
10665
  className
10131
10666
  )}
10132
10667
  >
10133
- <input
10134
- type="checkbox"
10135
- checked={isAdded}
10136
- onChange={() => onToggle(bump.id, !isAdded)}
10137
- disabled={loading}
10138
- className="mt-1 h-4 w-4 shrink-0 rounded"
10139
- />
10140
-
10141
- {/* Image */}
10142
- {imageUrl && (
10143
- <div className="bg-muted relative h-10 w-10 shrink-0 overflow-hidden rounded">
10144
- <Image src={imageUrl} alt={product.name} fill sizes="40px" className="object-cover" />
10145
- </div>
10146
- )}
10668
+ <label className="flex cursor-pointer items-start gap-3">
10669
+ <input
10670
+ type="checkbox"
10671
+ checked={isAdded}
10672
+ onChange={() => {
10673
+ if (canToggle) {
10674
+ onToggle(bump.id, !isAdded, effectiveVariantId ?? undefined);
10675
+ }
10676
+ }}
10677
+ disabled={loading || !canToggle || isOos}
10678
+ className="mt-1 h-4 w-4 shrink-0 rounded"
10679
+ />
10147
10680
 
10148
- {/* Content */}
10149
- <div className="min-w-0 flex-1">
10150
- <p className="text-foreground text-sm font-medium">{bump.title}</p>
10151
- {bump.description && (
10152
- <p className="text-muted-foreground mt-0.5 text-xs">{bump.description}</p>
10681
+ {/* Image */}
10682
+ {imageUrl && (
10683
+ <div className="bg-muted relative h-10 w-10 shrink-0 overflow-hidden rounded">
10684
+ <Image src={imageUrl} alt={product.name} fill sizes="40px" className="object-cover" />
10685
+ </div>
10153
10686
  )}
10154
- <div className="mt-1 flex items-center gap-2">
10155
- {hasDiscount ? (
10156
- <>
10157
- <span className="text-muted-foreground text-xs line-through">
10158
- {formatPrice(originalPrice, { currency }) as string}
10159
- </span>
10687
+
10688
+ {/* Content */}
10689
+ <div className="min-w-0 flex-1">
10690
+ <p className="text-foreground text-sm font-medium">{bump.title}</p>
10691
+ {bump.description && (
10692
+ <p className="text-muted-foreground mt-0.5 text-xs">{bump.description}</p>
10693
+ )}
10694
+
10695
+ {/* Locked variant label */}
10696
+ {lockedLabel && <p className="text-muted-foreground mt-0.5 text-xs">{lockedLabel}</p>}
10697
+
10698
+ {/* Price */}
10699
+ <div className="mt-1 flex items-center gap-2">
10700
+ {displayDiscounted != null ? (
10701
+ <>
10702
+ <span className="text-muted-foreground text-xs line-through">
10703
+ {formatPrice(displayOriginal, { currency }) as string}
10704
+ </span>
10705
+ <span className="text-foreground text-sm font-semibold">
10706
+ {formatPrice(displayDiscounted, { currency }) as string}
10707
+ </span>
10708
+ </>
10709
+ ) : (
10160
10710
  <span className="text-foreground text-sm font-semibold">
10161
- {formatPrice(discountedPrice!, { currency }) as string}
10711
+ {formatPrice(displayOriginal, { currency }) as string}
10162
10712
  </span>
10163
- </>
10164
- ) : (
10165
- <span className="text-foreground text-sm font-semibold">
10166
- {formatPrice(originalPrice, { currency }) as string}
10167
- </span>
10713
+ )}
10714
+ {requiresSelection && !effectiveVariantId && (
10715
+ <span className="text-muted-foreground text-xs">
10716
+ {t('selectOptions') || 'Select options'}
10717
+ </span>
10718
+ )}
10719
+ </div>
10720
+ </div>
10721
+ </label>
10722
+
10723
+ {/* Compact variant selector */}
10724
+ {requiresSelection && !isAdded && (
10725
+ <div className="ms-7 mt-2 space-y-1.5">
10726
+ {attributeGroups.map((group) => (
10727
+ <div key={group.name} className="flex flex-wrap items-center gap-1.5">
10728
+ <span className="text-muted-foreground text-xs">{group.name}:</span>
10729
+ {group.values.map((value) => {
10730
+ const isSelected = selectedAttrs[group.name] === value;
10731
+ // Check if this value leads to any available variant
10732
+ const variantForValue = variants?.find((v) => {
10733
+ const opts = getVariantOptions(v as any);
10734
+ const matchesValue = opts.some((o) => o.name === group.name && o.value === value);
10735
+ if (!matchesValue) return false;
10736
+ // Check other selected attrs
10737
+ return Object.entries(selectedAttrs).every(([k, sv]) => {
10738
+ if (k === group.name) return true;
10739
+ return opts.some((o) => o.name === k && o.value === sv);
10740
+ });
10741
+ });
10742
+ const isVariantOos =
10743
+ variantForValue?.inventory?.trackingMode !== 'NOT_TRACKED' &&
10744
+ variantForValue?.inventory?.available != null &&
10745
+ variantForValue.inventory.available <= 0;
10746
+
10747
+ return (
10748
+ <button
10749
+ key={value}
10750
+ type="button"
10751
+ onClick={() => handleAttrSelect(group.name, value)}
10752
+ disabled={isVariantOos}
10753
+ className={cn(
10754
+ 'rounded-full border px-2.5 py-0.5 text-xs transition-colors',
10755
+ isSelected
10756
+ ? 'border-primary bg-primary text-primary-foreground'
10757
+ : 'border-border text-foreground hover:border-primary/50',
10758
+ isVariantOos && 'cursor-not-allowed line-through opacity-40'
10759
+ )}
10760
+ >
10761
+ {value}
10762
+ </button>
10763
+ );
10764
+ })}
10765
+ </div>
10766
+ ))}
10767
+ {isOos && effectiveVariantId && (
10768
+ <p className="text-destructive text-xs">{t('outOfStock') || 'Out of stock'}</p>
10168
10769
  )}
10169
10770
  </div>
10170
- </div>
10171
- </label>
10771
+ )}
10772
+ </div>
10172
10773
  );
10173
10774
  }
10174
10775
  `,
@@ -10531,7 +11132,8 @@ var DETAILED = `# Required Pages \u2014 Detailed Guide
10531
11132
  - Step 1: Customer info (email, name)
10532
11133
  - Step 2: Shipping address form
10533
11134
  - Step 3: Shipping method selection (from rates returned by \`setShippingAddress()\`)
10534
- - Step 4: Payment (auto-detects Stripe/Grow/PayPal via \`getPaymentProviders()\`)
11135
+ - Step 4: Payment (auto-detects Stripe/Grow/PayPal/Sandbox via \`getPaymentProviders()\`)
11136
+ - If \`sandboxPaymentsEnabled\` is on, a sandbox provider with \`renderType: 'sandbox'\` is included \u2014 show a "Complete Test Order" button instead of real payment UI. Call \`completeGuestCheckout(checkoutId)\` to finalize.
10535
11137
  - Order summary sidebar showing \`checkout.lineItems\` (NOT cart.items!)
10536
11138
  - Tax display after address entry
10537
11139
  - Reservation countdown
@@ -10611,46 +11213,39 @@ async function handleGetRequiredPages(args) {
10611
11213
  var import_zod5 = require("zod");
10612
11214
 
10613
11215
  // src/utils/fetch-store-info.ts
10614
- var import_https = __toESM(require("https"));
10615
- var import_http = __toESM(require("http"));
10616
11216
  async function fetchStoreInfo(connectionId, baseUrl = "https://api.brainerce.com") {
10617
11217
  const url = `${baseUrl}/api/vc/${connectionId}/info`;
10618
- return new Promise((resolve, reject) => {
10619
- const client = url.startsWith("https") ? import_https.default : import_http.default;
10620
- const req = client.get(url, { timeout: 1e4 }, (res) => {
10621
- let data = "";
10622
- res.on("data", (chunk) => {
10623
- data += chunk;
10624
- });
10625
- res.on("end", () => {
10626
- if (res.statusCode && res.statusCode >= 400) {
10627
- if (res.statusCode === 404) {
10628
- reject(new Error(`Connection ID "${connectionId}" not found. Check your dashboard.`));
10629
- } else {
10630
- reject(new Error(`API returned status ${res.statusCode}`));
10631
- }
10632
- return;
10633
- }
10634
- try {
10635
- const json = JSON.parse(data);
10636
- resolve({
10637
- name: json.name || json.storeName || "My Store",
10638
- currency: json.currency || "USD",
10639
- language: json.language || "en"
10640
- });
10641
- } catch {
10642
- reject(new Error("Invalid response from API"));
10643
- }
10644
- });
10645
- });
10646
- req.on("error", (err) => {
10647
- reject(new Error(`Failed to connect to Brainerce API: ${err.message}`));
10648
- });
10649
- req.on("timeout", () => {
10650
- req.destroy();
10651
- reject(new Error("Request timed out"));
10652
- });
10653
- });
11218
+ const controller = new AbortController();
11219
+ const timeout = setTimeout(() => controller.abort(), 1e4);
11220
+ let res;
11221
+ try {
11222
+ res = await fetch(url, { signal: controller.signal });
11223
+ } catch (err) {
11224
+ if (err.name === "AbortError") {
11225
+ throw new Error("Request timed out");
11226
+ }
11227
+ throw new Error(`Failed to connect to Brainerce API: ${err.message}`);
11228
+ } finally {
11229
+ clearTimeout(timeout);
11230
+ }
11231
+ if (res.status === 404) {
11232
+ throw new Error(`Connection ID "${connectionId}" not found. Check your dashboard.`);
11233
+ }
11234
+ if (!res.ok) {
11235
+ throw new Error(`API returned status ${res.status}`);
11236
+ }
11237
+ let json;
11238
+ try {
11239
+ json = await res.json();
11240
+ } catch {
11241
+ throw new Error("Invalid response from API");
11242
+ }
11243
+ return {
11244
+ name: json.name || json.storeName || "My Store",
11245
+ currency: json.currency || "USD",
11246
+ language: json.language || "en",
11247
+ ...json.i18n ? { i18n: json.i18n } : {}
11248
+ };
10654
11249
  }
10655
11250
 
10656
11251
  // src/tools/get-store-info.ts
@@ -10661,7 +11256,11 @@ var GET_STORE_INFO_SCHEMA = {
10661
11256
  };
10662
11257
  async function handleGetStoreInfo(args) {
10663
11258
  try {
10664
- const info = await fetchStoreInfo(args.connectionId);
11259
+ const baseUrl = (process.env.BRAINERCE_API_URL || "https://api.brainerce.com").replace(
11260
+ /\/$/,
11261
+ ""
11262
+ );
11263
+ const info = await fetchStoreInfo(args.connectionId, baseUrl);
10665
11264
  return {
10666
11265
  content: [
10667
11266
  {
@@ -10692,41 +11291,32 @@ async function handleGetStoreInfo(args) {
10692
11291
  var import_zod6 = require("zod");
10693
11292
 
10694
11293
  // src/utils/fetch-store-capabilities.ts
10695
- var import_https2 = __toESM(require("https"));
10696
- var import_http2 = __toESM(require("http"));
10697
11294
  async function fetchStoreCapabilities(connectionId, baseUrl = "https://api.brainerce.com") {
10698
11295
  const url = `${baseUrl}/api/vc/${connectionId}/capabilities`;
10699
- return new Promise((resolve, reject) => {
10700
- const client = url.startsWith("https") ? import_https2.default : import_http2.default;
10701
- const req = client.get(url, { timeout: 1e4 }, (res) => {
10702
- let data = "";
10703
- res.on("data", (chunk) => {
10704
- data += chunk;
10705
- });
10706
- res.on("end", () => {
10707
- if (res.statusCode && res.statusCode >= 400) {
10708
- if (res.statusCode === 404) {
10709
- reject(new Error(`Connection ID "${connectionId}" not found. Check your dashboard.`));
10710
- } else {
10711
- reject(new Error(`API returned status ${res.statusCode}`));
10712
- }
10713
- return;
10714
- }
10715
- try {
10716
- resolve(JSON.parse(data));
10717
- } catch {
10718
- reject(new Error("Invalid response from API"));
10719
- }
10720
- });
10721
- });
10722
- req.on("error", (err) => {
10723
- reject(new Error(`Failed to connect to Brainerce API: ${err.message}`));
10724
- });
10725
- req.on("timeout", () => {
10726
- req.destroy();
10727
- reject(new Error("Request timed out"));
10728
- });
10729
- });
11296
+ const controller = new AbortController();
11297
+ const timeout = setTimeout(() => controller.abort(), 1e4);
11298
+ let res;
11299
+ try {
11300
+ res = await fetch(url, { signal: controller.signal });
11301
+ } catch (err) {
11302
+ if (err.name === "AbortError") {
11303
+ throw new Error("Request timed out");
11304
+ }
11305
+ throw new Error(`Failed to connect to Brainerce API: ${err.message}`);
11306
+ } finally {
11307
+ clearTimeout(timeout);
11308
+ }
11309
+ if (res.status === 404) {
11310
+ throw new Error(`Connection ID "${connectionId}" not found. Check your dashboard.`);
11311
+ }
11312
+ if (!res.ok) {
11313
+ throw new Error(`API returned status ${res.status}`);
11314
+ }
11315
+ try {
11316
+ return await res.json();
11317
+ } catch {
11318
+ throw new Error("Invalid response from API");
11319
+ }
10730
11320
  }
10731
11321
 
10732
11322
  // src/tools/get-store-capabilities.ts
@@ -10739,6 +11329,11 @@ function formatCapabilities(caps) {
10739
11329
  const lines = [];
10740
11330
  lines.push(`## Store: ${caps.store.name} (${caps.store.currency})`);
10741
11331
  lines.push(`Language: ${caps.store.language}`);
11332
+ if (caps.store.i18n?.enabled) {
11333
+ lines.push(
11334
+ `Multi-language: ENABLED \u2014 locales: ${caps.store.i18n.supportedLocales.join(", ")} (default: ${caps.store.i18n.defaultLocale})`
11335
+ );
11336
+ }
10742
11337
  lines.push("");
10743
11338
  lines.push("## Configured Features");
10744
11339
  if (caps.features.paymentProviders.length > 0) {
@@ -10773,6 +11368,9 @@ function formatCapabilities(caps) {
10773
11368
  lines.push(
10774
11369
  `- Email verification: ${caps.connection.requireEmailVerification ? "required" : "not required"}`
10775
11370
  );
11371
+ lines.push(
11372
+ `- Sandbox payments: ${caps.connection.sandboxPaymentsEnabled ? "enabled (test orders without real payment)" : "disabled"}`
11373
+ );
10776
11374
  lines.push(
10777
11375
  `- Inventory reservation: ${caps.connection.reservationStrategy} (${caps.connection.reservationTimeout} min timeout)`
10778
11376
  );
@@ -10812,11 +11410,21 @@ function formatCapabilities(caps) {
10812
11410
  'Email verification is required \u2014 ALWAYS build the verify-email page. Use get-code-example("verify-email").'
10813
11411
  );
10814
11412
  }
11413
+ if (caps.connection.sandboxPaymentsEnabled) {
11414
+ suggestions.push(
11415
+ `Sandbox payments are enabled \u2014 the PaymentStep component shows a "Complete Test Order" button when renderType is 'sandbox'. No real charges. Test orders are marked isTestOrder: true.`
11416
+ );
11417
+ }
10815
11418
  if (caps.connection.reservationStrategy !== "ON_PAYMENT") {
10816
11419
  suggestions.push(
10817
11420
  'Inventory reservation is active \u2014 show a countdown timer. Use get-code-example("reservation-countdown").'
10818
11421
  );
10819
11422
  }
11423
+ if (caps.store.i18n?.enabled && caps.store.i18n.supportedLocales.length > 1) {
11424
+ suggestions.push(
11425
+ `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.`
11426
+ );
11427
+ }
10820
11428
  if (suggestions.length === 0) {
10821
11429
  suggestions.push(
10822
11430
  "Start by building the required pages. Use get-required-pages() for the checklist."
@@ -10827,7 +11435,11 @@ function formatCapabilities(caps) {
10827
11435
  }
10828
11436
  async function handleGetStoreCapabilities(args) {
10829
11437
  try {
10830
- const caps = await fetchStoreCapabilities(args.connectionId);
11438
+ const baseUrl = (process.env.BRAINERCE_API_URL || "https://api.brainerce.com").replace(
11439
+ /\/$/,
11440
+ ""
11441
+ );
11442
+ const caps = await fetchStoreCapabilities(args.connectionId, baseUrl);
10831
11443
  return {
10832
11444
  content: [{ type: "text", text: formatCapabilities(caps) }]
10833
11445
  };
@@ -10924,6 +11536,20 @@ function formatCapabilitiesSummary(caps) {
10924
11536
  const lines = [];
10925
11537
  lines.push(`## Store: ${caps.store.name}`);
10926
11538
  lines.push(`Currency: ${caps.store.currency} | Language: ${caps.store.language}`);
11539
+ if (caps.store.i18n?.enabled && caps.store.i18n.supportedLocales.length > 1) {
11540
+ lines.push(
11541
+ `Multi-language: ${caps.store.i18n.supportedLocales.join(", ")} (default: ${caps.store.i18n.defaultLocale})`
11542
+ );
11543
+ lines.push("");
11544
+ lines.push("### i18n Implementation");
11545
+ lines.push(
11546
+ "- Call `client.setLocale(locale)` at app init based on URL prefix or user preference"
11547
+ );
11548
+ lines.push("- All getProducts/getCategories/getBrands calls auto-include the locale");
11549
+ lines.push("- Use locale-prefixed routes: `/[locale]/products`, `/[locale]/products/[slug]`");
11550
+ lines.push("- Add a language switcher component in the header");
11551
+ lines.push('- RTL locales (he, ar): set `<html dir="rtl">` \u2014 flexbox reversal is automatic');
11552
+ }
10927
11553
  lines.push("");
10928
11554
  lines.push("### Configured Features");
10929
11555
  if (caps.features.paymentProviders.length > 0) {
@@ -10965,6 +11591,9 @@ function formatCapabilitiesSummary(caps) {
10965
11591
  lines.push(
10966
11592
  `- Guest checkout tracking: ${caps.connection.guestCheckoutTracking ? "enabled" : "disabled"}`
10967
11593
  );
11594
+ lines.push(
11595
+ `- Sandbox payments: ${caps.connection.sandboxPaymentsEnabled ? "ENABLED \u2014 payment step shows test order button (no real charges)" : "disabled"}`
11596
+ );
10968
11597
  return lines.join("\n");
10969
11598
  }
10970
11599
  function buildStoreBundle(options) {
@@ -10974,8 +11603,7 @@ function buildStoreBundle(options) {
10974
11603
  connectionId,
10975
11604
  storeName,
10976
11605
  currency,
10977
- language: capabilities?.store.language || "en",
10978
- apiUrl: "https://api.brainerce.com"
11606
+ language: capabilities?.store.language || "en"
10979
11607
  };
10980
11608
  const sections = [];
10981
11609
  sections.push(`# Build: ${storeName} \u2014 ${storeType}${styleDesc}
@@ -11076,9 +11704,13 @@ var BUILD_STORE_SCHEMA = {
11076
11704
  storeStyle: import_zod7.z.string().optional().describe('Design style (e.g., "minimalist", "luxury", "playful", "dark mode")')
11077
11705
  };
11078
11706
  async function handleBuildStore(args) {
11707
+ const baseUrl = (process.env.BRAINERCE_API_URL || "https://api.brainerce.com").replace(
11708
+ /\/$/,
11709
+ ""
11710
+ );
11079
11711
  let capabilities = null;
11080
11712
  try {
11081
- capabilities = await fetchStoreCapabilities(args.connectionId);
11713
+ capabilities = await fetchStoreCapabilities(args.connectionId, baseUrl);
11082
11714
  } catch {
11083
11715
  }
11084
11716
  const bundle = buildStoreBundle({
@@ -11417,6 +12049,57 @@ ${results.join("\n\n---\n\n")}`
11417
12049
  };
11418
12050
  }
11419
12051
 
12052
+ // src/tools/get-integration-guide.ts
12053
+ var import_zod10 = require("zod");
12054
+ var GET_INTEGRATION_GUIDE_NAME = "get-integration-guide";
12055
+ 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.';
12056
+ var GET_INTEGRATION_GUIDE_SCHEMA = {
12057
+ part: import_zod10.z.enum(["core", "optional", "rules"]).describe(
12058
+ '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.'
12059
+ )
12060
+ };
12061
+ var GUIDE_URLS = {
12062
+ core: "https://brainerce.com/docs/integration/raw?part=core",
12063
+ optional: "https://brainerce.com/docs/integration/raw?part=optional",
12064
+ rules: "https://brainerce.com/docs/integration/raw?part=rules"
12065
+ };
12066
+ async function handleGetIntegrationGuide(args) {
12067
+ const url = GUIDE_URLS[args.part];
12068
+ if (!url) {
12069
+ return {
12070
+ content: [
12071
+ {
12072
+ type: "text",
12073
+ text: `Unknown part: "${args.part}". Available parts: core, optional, rules.`
12074
+ }
12075
+ ]
12076
+ };
12077
+ }
12078
+ try {
12079
+ const response = await fetch(url, {
12080
+ headers: { Accept: "text/plain" },
12081
+ signal: AbortSignal.timeout(15e3)
12082
+ });
12083
+ if (!response.ok) {
12084
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
12085
+ }
12086
+ const content = await response.text();
12087
+ return {
12088
+ content: [{ type: "text", text: content }]
12089
+ };
12090
+ } catch (error) {
12091
+ const message = error instanceof Error ? error.message : "Unknown error";
12092
+ return {
12093
+ content: [
12094
+ {
12095
+ type: "text",
12096
+ text: `Failed to fetch integration guide (${args.part}): ${message}. You can read it directly at: ${url}`
12097
+ }
12098
+ ]
12099
+ };
12100
+ }
12101
+ }
12102
+
11420
12103
  // src/resources/sdk-types.ts
11421
12104
  var SDK_TYPES_URI = "brainerce://sdk/types";
11422
12105
  var SDK_TYPES_NAME = "Brainerce SDK Types";
@@ -11571,15 +12254,15 @@ async function handleProjectTemplate(uri) {
11571
12254
  }
11572
12255
 
11573
12256
  // src/prompts/create-store.ts
11574
- var import_zod10 = require("zod");
12257
+ var import_zod11 = require("zod");
11575
12258
  var CREATE_STORE_NAME = "create-store";
11576
12259
  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.";
11577
12260
  var CREATE_STORE_SCHEMA = {
11578
- connectionId: import_zod10.z.string().describe("Vibe-coded connection ID (starts with vc_)"),
11579
- storeName: import_zod10.z.string().describe('Name of the store (e.g., "Urban Threads")'),
11580
- storeType: import_zod10.z.string().describe('Type of store (e.g., "Clothing store", "Electronics shop", "Bakery")'),
11581
- currency: import_zod10.z.string().optional().default("USD").describe("Store currency code (e.g., USD, ILS, EUR)"),
11582
- storeStyle: import_zod10.z.string().optional().describe('Design style (e.g., "minimalist", "luxury", "playful", "dark mode")')
12261
+ connectionId: import_zod11.z.string().describe("Vibe-coded connection ID (starts with vc_)"),
12262
+ storeName: import_zod11.z.string().describe('Name of the store (e.g., "Urban Threads")'),
12263
+ storeType: import_zod11.z.string().describe('Type of store (e.g., "Clothing store", "Electronics shop", "Bakery")'),
12264
+ currency: import_zod11.z.string().optional().default("USD").describe("Store currency code (e.g., USD, ILS, EUR)"),
12265
+ storeStyle: import_zod11.z.string().optional().describe('Design style (e.g., "minimalist", "luxury", "playful", "dark mode")')
11583
12266
  };
11584
12267
  function handleCreateStore(args) {
11585
12268
  const styleDesc = args.storeStyle ? ` with a ${args.storeStyle} design style` : "";
@@ -11618,6 +12301,13 @@ DO NOT STOP until all 13 pages + header are implemented:
11618
12301
  13. \`/account\` \u2014 Account dashboard
11619
12302
  14. Header component with cart count + search
11620
12303
 
12304
+ ## Multi-Language
12305
+ If the store has multi-language enabled (check build-store output), implement:
12306
+ - Locale-prefixed routes: /[locale]/products instead of /products
12307
+ - Call client.setLocale(locale) based on URL prefix
12308
+ - Language switcher in header
12309
+ - dir="rtl" on <html> for RTL locales (he, ar)
12310
+
11621
12311
  ## Step 3: Validate
11622
12312
  After building all pages, call \`validate-store\` with the list of files you created to verify nothing is missing.
11623
12313
 
@@ -11636,11 +12326,11 @@ After building all pages, call \`validate-store\` with the list of files you cre
11636
12326
  }
11637
12327
 
11638
12328
  // src/prompts/add-feature.ts
11639
- var import_zod11 = require("zod");
12329
+ var import_zod12 = require("zod");
11640
12330
  var ADD_FEATURE_NAME = "add-feature";
11641
12331
  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.";
11642
12332
  var ADD_FEATURE_SCHEMA = {
11643
- feature: import_zod11.z.enum([
12333
+ feature: import_zod12.z.enum([
11644
12334
  "products",
11645
12335
  "cart",
11646
12336
  "checkout",
@@ -11653,8 +12343,8 @@ var ADD_FEATURE_SCHEMA = {
11653
12343
  "search",
11654
12344
  "tax"
11655
12345
  ]).describe("The feature to add"),
11656
- connectionId: import_zod11.z.string().optional().describe("Vibe-coded connection ID (starts with vc_)"),
11657
- currency: import_zod11.z.string().optional().default("USD").describe("Store currency code")
12346
+ connectionId: import_zod12.z.string().optional().describe("Vibe-coded connection ID (starts with vc_)"),
12347
+ currency: import_zod12.z.string().optional().default("USD").describe("Store currency code")
11658
12348
  };
11659
12349
  var FEATURE_TO_TOPICS = {
11660
12350
  products: { topics: ["products"], domains: ["products"] },
@@ -11769,6 +12459,12 @@ function createServer() {
11769
12459
  GET_PAGE_CODE_SCHEMA,
11770
12460
  handleGetPageCode
11771
12461
  );
12462
+ server.tool(
12463
+ GET_INTEGRATION_GUIDE_NAME,
12464
+ GET_INTEGRATION_GUIDE_DESCRIPTION,
12465
+ GET_INTEGRATION_GUIDE_SCHEMA,
12466
+ handleGetIntegrationGuide
12467
+ );
11772
12468
  server.resource(
11773
12469
  SDK_TYPES_NAME,
11774
12470
  SDK_TYPES_URI,