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