@brainerce/mcp-server 2.4.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin/http.js 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!**
@@ -606,6 +585,35 @@ const material = getProductMetafieldValue(product, 'material');
606
585
 
607
586
  **Metafield fields:** \`definitionName\` (display label), \`definitionKey\` (lookup key), \`value\`, \`type\` (IMAGE, GALLERY, URL, COLOR, BOOLEAN, DATE, DATETIME, TEXT, TEXTAREA, NUMBER, DIMENSION, WEIGHT, JSON)
608
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
+
609
617
  ### Downloadable / Digital Products
610
618
 
611
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.
@@ -792,9 +800,14 @@ const { bundles } = await client.getCartBundles(cartId);
792
800
  // bundles[].bundleProduct \u2014 the product to offer
793
801
  // bundles[].originalPrice / bundles[].discountedPrice \u2014 prices
794
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)
795
806
 
796
807
  // Add a bundle to cart (applies the discount automatically):
797
808
  await client.addBundleToCart(cartId, bundleOfferId);
809
+ // For products with variants \u2014 pass the selected variantId:
810
+ await client.addBundleToCart(cartId, bundleOfferId, selectedVariantId);
798
811
  // Remove a bundle from cart:
799
812
  await client.removeBundleFromCart(cartId, bundleOfferId);
800
813
  // Detect already-added bundles: check cart.items for metadata?.isBundleItem === true
@@ -819,9 +832,14 @@ const { bumps } = await client.getCheckoutBumps(checkoutId);
819
832
  // bumps[].bumpProduct \u2014 product to offer
820
833
  // bumps[].originalPrice / discountedPrice \u2014 with optional discount
821
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)
822
838
 
823
839
  // Add a bump to cart:
824
840
  await client.addOrderBump(cartId, bumpId);
841
+ // For products with variants \u2014 pass the selected variantId:
842
+ await client.addOrderBump(cartId, bumpId, selectedVariantId);
825
843
  // Remove a bump:
826
844
  await client.removeOrderBump(cartId, bumpId);
827
845
  // Detect already-added bumps: check cart.items for metadata?.isOrderBump === true
@@ -846,10 +864,11 @@ Always check the flag before rendering: \`if (storeInfo?.upsell?.featureName !==
846
864
  | CartRecommendationSection | products/ | Cross-sell grid at bottom of cart page |
847
865
  | FreeShippingBar | cart/ | Progress bar toward free shipping threshold |
848
866
  | CartUpgradeBanner | cart/ | Inline "Upgrade to X for +$Y" per cart item |
849
- | CartBundleOfferCard | cart/ | Discounted bundle offer card in cart |
850
- | 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) |
851
869
 
852
- 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 }\`).`;
853
872
  }
854
873
  function getInventorySection() {
855
874
  return `## Inventory, Stock Display & Reservation Countdown
@@ -1989,6 +2008,15 @@ export function getClient(): BrainerceClient {
1989
2008
  }
1990
2009
  return clientInstance;
1991
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
+ <% } %>
1992
2020
 
1993
2021
  // Cart ID helpers (not a security token \u2014 safe in localStorage)
1994
2022
  const CART_ID_KEY = 'brainerce_cart_id';
@@ -2187,6 +2215,10 @@ import type { StoreInfo, Cart, CustomerProfile } from 'brainerce';
2187
2215
  import { getCartTotals } from 'brainerce';
2188
2216
  import { getClient, initClient, setStoredCartId } from '@/lib/brainerce';
2189
2217
  import { checkAuthStatus, proxyLogout } from '@/lib/auth';
2218
+ <% if (i18nEnabled) { %>
2219
+ import { MessagesContext } from '@/lib/translations';
2220
+ import { getMessages, defaultLocale } from '@/i18n';
2221
+ <% } %>
2190
2222
 
2191
2223
  // ---- Store Info Context ----
2192
2224
  interface StoreInfoContextValue {
@@ -2246,7 +2278,11 @@ export function useCart() {
2246
2278
  }
2247
2279
 
2248
2280
  // ---- Provider Component ----
2281
+ <% if (i18nEnabled) { %>
2282
+ export function StoreProvider({ children, locale }: { children: React.ReactNode; locale?: string }) {
2283
+ <% } else { %>
2249
2284
  export function StoreProvider({ children }: { children: React.ReactNode }) {
2285
+ <% } %>
2250
2286
  const [storeInfo, setStoreInfo] = useState<StoreInfo | null>(null);
2251
2287
  const [storeLoading, setStoreLoading] = useState(true);
2252
2288
  const [isLoggedIn, setIsLoggedIn] = useState(false);
@@ -2254,6 +2290,9 @@ export function StoreProvider({ children }: { children: React.ReactNode }) {
2254
2290
  const [authLoading, setAuthLoading] = useState(true);
2255
2291
  const [cart, setCart] = useState<Cart | null>(null);
2256
2292
  const [cartLoading, setCartLoading] = useState(true);
2293
+ <% if (i18nEnabled) { %>
2294
+ const [messages, setMessages] = useState<Record<string, Record<string, string>>>({});
2295
+ <% } %>
2257
2296
 
2258
2297
  // Check auth status via httpOnly cookie (server-side validation)
2259
2298
  const refreshAuth = useCallback(async () => {
@@ -2272,6 +2311,16 @@ export function StoreProvider({ children }: { children: React.ReactNode }) {
2272
2311
  // Initialize client, check auth, and fetch store info
2273
2312
  useEffect(() => {
2274
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
+ <% } %>
2275
2324
 
2276
2325
  // Optimistic check: if brainerce_logged_in cookie exists, assume logged in
2277
2326
  // while we validate the actual token server-side
@@ -2288,7 +2337,11 @@ export function StoreProvider({ children }: { children: React.ReactNode }) {
2288
2337
  .then(setStoreInfo)
2289
2338
  .catch(console.error)
2290
2339
  .finally(() => setStoreLoading(false));
2340
+ <% if (i18nEnabled) { %>
2341
+ }, [refreshAuth, locale]);
2342
+ <% } else { %>
2291
2343
  }, [refreshAuth]);
2344
+ <% } %>
2292
2345
 
2293
2346
  // Cart management
2294
2347
  const refreshCart = useCallback(async () => {
@@ -2341,6 +2394,21 @@ export function StoreProvider({ children }: { children: React.ReactNode }) {
2341
2394
 
2342
2395
  const totals = cart ? getCartTotals(cart) : { subtotal: 0, discount: 0, shipping: 0, total: 0 };
2343
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 { %>
2344
2412
  return (
2345
2413
  <StoreInfoContext.Provider value={{ storeInfo, loading: storeLoading }}>
2346
2414
  <AuthContext.Provider value={{ isLoggedIn, authLoading, customer, login, logout }}>
@@ -2352,6 +2420,7 @@ export function StoreProvider({ children }: { children: React.ReactNode }) {
2352
2420
  </AuthContext.Provider>
2353
2421
  </StoreInfoContext.Provider>
2354
2422
  );
2423
+ <% } %>
2355
2424
  }
2356
2425
  `,
2357
2426
  "src/app/page.tsx": `'use client';
@@ -2453,7 +2522,81 @@ export default function HomePage() {
2453
2522
  );
2454
2523
  }
2455
2524
  `,
2456
- "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';
2457
2600
  <%- fontImport %>
2458
2601
  import { StoreProvider } from '@/providers/store-provider';
2459
2602
  import { Header } from '@/components/layout/header';
@@ -2517,6 +2660,7 @@ export default function RootLayout({
2517
2660
  </html>
2518
2661
  );
2519
2662
  }
2663
+ <% } %>
2520
2664
  `,
2521
2665
  "src/app/products/page.tsx": `'use client';
2522
2666
 
@@ -3417,13 +3561,13 @@ function CheckoutContent() {
3417
3561
  }, [checkout?.id, storeInfo?.upsell?.checkoutOrderBumpEnabled]);
3418
3562
 
3419
3563
  // Handle bump toggle
3420
- async function handleBumpToggle(bumpId: string, add: boolean) {
3564
+ async function handleBumpToggle(bumpId: string, add: boolean, variantId?: string) {
3421
3565
  if (!cart?.id || bumpLoading) return;
3422
3566
  try {
3423
3567
  setBumpLoading(bumpId);
3424
3568
  const client = getClient();
3425
3569
  if (add) {
3426
- await client.addOrderBump(cart.id, bumpId);
3570
+ await client.addOrderBump(cart.id, bumpId, variantId);
3427
3571
  setAddedBumpIds((prev) => new Set([...prev, bumpId]));
3428
3572
  } else {
3429
3573
  await client.removeOrderBump(cart.id, bumpId);
@@ -4121,6 +4265,23 @@ function OrderConfirmationContent() {
4121
4265
  client.handlePaymentSuccess(checkoutId!);
4122
4266
  await refreshCart();
4123
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
+
4124
4285
  const orderResult = await client.waitForOrder(checkoutId!, {
4125
4286
  maxWaitMs: 30000,
4126
4287
  });
@@ -5637,7 +5798,62 @@ export async function POST(request: NextRequest) {
5637
5798
  return response;
5638
5799
  }
5639
5800
  `,
5640
- "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';
5641
5857
 
5642
5858
  const TOKEN_COOKIE = 'brainerce_customer_token';
5643
5859
 
@@ -5662,6 +5878,7 @@ export function middleware(request: NextRequest) {
5662
5878
  export const config = {
5663
5879
  matcher: ['/account/:path*'],
5664
5880
  };
5881
+ <% } %>
5665
5882
  `,
5666
5883
  "src/components/products/product-card.tsx": `'use client';
5667
5884
 
@@ -7523,7 +7740,9 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
7523
7740
  initialized.current = true;
7524
7741
 
7525
7742
  const client = getClient();
7526
- 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}\`;
7527
7746
  const cancelUrl = \`\${window.location.origin}/checkout?checkout_id=\${checkoutId}&canceled=true\`;
7528
7747
 
7529
7748
  let sdkInitDone = false;
@@ -7684,8 +7903,19 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
7684
7903
  });
7685
7904
 
7686
7905
  // C) Create payment intent (starts wallet timer)
7687
- const intentPromise = client
7688
- .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
+ })
7689
7919
  .then((intent) => {
7690
7920
  setPaymentIntent(intent);
7691
7921
  return intent;
@@ -7710,6 +7940,36 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
7710
7940
  window.location.href = intent.clientSecret;
7711
7941
  return;
7712
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
+
7713
7973
  if (sdk.renderType !== 'sdk-widget' || !sdk.globalName) return;
7714
7974
 
7715
7975
  // Store for retryRender from onError callback
@@ -7860,15 +8120,45 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
7860
8120
 
7861
8121
  if (sdk.renderType === 'iframe') {
7862
8122
  return (
7863
- <div className={cn('py-4', className)}>
7864
- <iframe
7865
- src={paymentIntent.clientSecret}
7866
- className="w-full border-0"
7867
- style={{ minHeight: '500px' }}
7868
- title={t('payment')}
7869
- allow="payment"
7870
- />
7871
- </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
+ </>
7872
8162
  );
7873
8163
  }
7874
8164
 
@@ -9995,10 +10285,10 @@ export function CartUpgradeBanner({
9995
10285
  `,
9996
10286
  "src/components/cart/cart-bundle-offer.tsx": `'use client';
9997
10287
 
9998
- import { useState } from 'react';
10288
+ import { useState, useMemo } from 'react';
9999
10289
  import Image from 'next/image';
10000
10290
  import type { CartBundleOffer as CartBundleOfferType } from 'brainerce';
10001
- import { formatPrice } from 'brainerce';
10291
+ import { formatPrice, getVariantOptions } from 'brainerce';
10002
10292
  import { useStoreInfo } from '@/providers/store-provider';
10003
10293
  import { useTranslations } from '@/lib/translations';
10004
10294
  import { cn } from '@/lib/utils';
@@ -10015,28 +10305,114 @@ export function CartBundleOfferCard({ offer, cartId, onAdd, className }: CartBun
10015
10305
  const t = useTranslations('cart');
10016
10306
  const currency = storeInfo?.currency || 'USD';
10017
10307
  const [adding, setAdding] = useState(false);
10308
+ const [selectedVariantId, setSelectedVariantId] = useState<string | null>(null);
10018
10309
 
10019
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
+
10020
10400
  const firstImage = product.images?.[0];
10021
10401
  const imageUrl = firstImage
10022
10402
  ? typeof firstImage === 'string'
10023
10403
  ? firstImage
10024
10404
  : firstImage.url
10025
10405
  : null;
10026
- const originalPrice = parseFloat(offer.originalPrice);
10027
- const discountedPrice = parseFloat(offer.discountedPrice);
10028
- const discountLabel =
10029
- offer.discountType === 'PERCENTAGE'
10030
- ? \`\${offer.discountValue}%\`
10031
- : (formatPrice(parseFloat(offer.discountValue), { currency }) as string);
10406
+
10407
+ const canAdd = !requiresSelection || !!effectiveVariantId;
10032
10408
 
10033
10409
  async function handleAdd() {
10034
- if (adding) return;
10410
+ if (adding || !canAdd || isOos) return;
10035
10411
  try {
10036
10412
  setAdding(true);
10037
10413
  const { getClient } = await import('@/lib/brainerce');
10038
10414
  const client = getClient();
10039
- await client.addBundleToCart(cartId, offer.id);
10415
+ await client.addBundleToCart(cartId, offer.id, effectiveVariantId ?? undefined);
10040
10416
  onAdd();
10041
10417
  } catch (err) {
10042
10418
  console.error('Failed to add bundle item:', err);
@@ -10046,70 +10422,121 @@ export function CartBundleOfferCard({ offer, cartId, onAdd, className }: CartBun
10046
10422
  }
10047
10423
 
10048
10424
  return (
10049
- <div
10050
- className={cn(
10051
- 'bg-background border-border flex items-center gap-4 rounded-lg border p-4',
10052
- className
10053
- )}
10054
- >
10055
- {/* Product image */}
10056
- <div className="bg-muted relative h-16 w-16 flex-shrink-0 overflow-hidden rounded">
10057
- {imageUrl ? (
10058
- <Image src={imageUrl} alt={product.name} fill sizes="64px" className="object-cover" />
10059
- ) : (
10060
- <div className="text-muted-foreground flex h-full w-full items-center justify-center">
10061
- <svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
10062
- <path
10063
- strokeLinecap="round"
10064
- strokeLinejoin="round"
10065
- strokeWidth={1.5}
10066
- 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"
10067
- />
10068
- </svg>
10069
- </div>
10070
- )}
10071
- </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>
10072
10444
 
10073
- {/* Details */}
10074
- <div className="min-w-0 flex-1">
10075
- <p className="text-foreground text-sm font-medium">{offer.name}</p>
10076
- {offer.description && (
10077
- <p className="text-muted-foreground mt-0.5 text-xs">{offer.description}</p>
10078
- )}
10079
- <div className="mt-1 flex items-center gap-2">
10080
- <span className="text-muted-foreground text-sm line-through">
10081
- {formatPrice(originalPrice, { currency }) as string}
10082
- </span>
10083
- <span className="text-foreground text-sm font-semibold">
10084
- {formatPrice(discountedPrice, { currency }) as string}
10085
- </span>
10086
- <span className="bg-destructive/10 text-destructive rounded px-1.5 py-0.5 text-xs font-medium">
10087
- -{discountLabel}
10088
- </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>
10089
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>
10090
10481
  </div>
10091
10482
 
10092
- {/* Add button */}
10093
- <button
10094
- type="button"
10095
- onClick={handleAdd}
10096
- disabled={adding}
10097
- className={cn(
10098
- 'bg-primary text-primary-foreground flex-shrink-0 rounded px-4 py-2 text-xs font-medium transition-opacity hover:opacity-90',
10099
- 'disabled:cursor-not-allowed disabled:opacity-50'
10100
- )}
10101
- >
10102
- {adding ? t('addingBundle') : t('addBundleItem')}
10103
- </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
+ )}
10104
10530
  </div>
10105
10531
  );
10106
10532
  }
10107
10533
  `,
10108
10534
  "src/components/checkout/order-bump-card.tsx": `'use client';
10109
10535
 
10536
+ import { useState, useMemo } from 'react';
10110
10537
  import Image from 'next/image';
10111
- import type { OrderBump } from 'brainerce';
10112
- import { formatPrice } from 'brainerce';
10538
+ import type { OrderBump, RecommendationVariant } from 'brainerce';
10539
+ import { formatPrice, getVariantOptions } from 'brainerce';
10113
10540
  import { useStoreInfo } from '@/providers/store-provider';
10114
10541
  import { useTranslations } from '@/lib/translations';
10115
10542
  import { cn } from '@/lib/utils';
@@ -10117,7 +10544,7 @@ import { cn } from '@/lib/utils';
10117
10544
  interface OrderBumpCardProps {
10118
10545
  bump: OrderBump;
10119
10546
  isAdded: boolean;
10120
- onToggle: (bumpId: string, add: boolean) => void;
10547
+ onToggle: (bumpId: string, add: boolean, variantId?: string) => void;
10121
10548
  loading: boolean;
10122
10549
  className?: string;
10123
10550
  }
@@ -10126,66 +10553,225 @@ export function OrderBumpCard({ bump, isAdded, onToggle, loading, className }: O
10126
10553
  const { storeInfo } = useStoreInfo();
10127
10554
  const t = useTranslations('checkout');
10128
10555
  const currency = storeInfo?.currency || 'USD';
10556
+ const [selectedVariantId, setSelectedVariantId] = useState<string | null>(null);
10129
10557
 
10130
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
+
10131
10652
  const firstImage = product.images?.[0];
10132
10653
  const imageUrl = firstImage
10133
10654
  ? typeof firstImage === 'string'
10134
10655
  ? firstImage
10135
10656
  : firstImage.url
10136
10657
  : null;
10137
- const originalPrice = parseFloat(bump.originalPrice);
10138
- const hasDiscount = bump.discountedPrice != null;
10139
- const discountedPrice = hasDiscount ? parseFloat(bump.discountedPrice!) : null;
10658
+
10659
+ const canToggle = !requiresSelection || !!effectiveVariantId;
10140
10660
 
10141
10661
  return (
10142
- <label
10662
+ <div
10143
10663
  className={cn(
10144
- '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',
10145
10665
  isAdded && 'border-primary bg-primary/5',
10146
10666
  loading && 'pointer-events-none opacity-60',
10147
10667
  className
10148
10668
  )}
10149
10669
  >
10150
- <input
10151
- type="checkbox"
10152
- checked={isAdded}
10153
- onChange={() => onToggle(bump.id, !isAdded)}
10154
- disabled={loading}
10155
- className="mt-1 h-4 w-4 shrink-0 rounded"
10156
- />
10157
-
10158
- {/* Image */}
10159
- {imageUrl && (
10160
- <div className="bg-muted relative h-10 w-10 shrink-0 overflow-hidden rounded">
10161
- <Image src={imageUrl} alt={product.name} fill sizes="40px" className="object-cover" />
10162
- </div>
10163
- )}
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
+ />
10164
10682
 
10165
- {/* Content */}
10166
- <div className="min-w-0 flex-1">
10167
- <p className="text-foreground text-sm font-medium">{bump.title}</p>
10168
- {bump.description && (
10169
- <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>
10170
10688
  )}
10171
- <div className="mt-1 flex items-center gap-2">
10172
- {hasDiscount ? (
10173
- <>
10174
- <span className="text-muted-foreground text-xs line-through">
10175
- {formatPrice(originalPrice, { currency }) as string}
10176
- </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
+ ) : (
10177
10712
  <span className="text-foreground text-sm font-semibold">
10178
- {formatPrice(discountedPrice!, { currency }) as string}
10713
+ {formatPrice(displayOriginal, { currency }) as string}
10179
10714
  </span>
10180
- </>
10181
- ) : (
10182
- <span className="text-foreground text-sm font-semibold">
10183
- {formatPrice(originalPrice, { currency }) as string}
10184
- </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>
10185
10771
  )}
10186
10772
  </div>
10187
- </div>
10188
- </label>
10773
+ )}
10774
+ </div>
10189
10775
  );
10190
10776
  }
10191
10777
  `,
@@ -10629,46 +11215,81 @@ async function handleGetRequiredPages(args) {
10629
11215
  var import_zod5 = require("zod");
10630
11216
 
10631
11217
  // src/utils/fetch-store-info.ts
10632
- var import_https = __toESM(require("https"));
10633
- var import_http = __toESM(require("http"));
10634
- async function fetchStoreInfo(connectionId, baseUrl = "https://api.brainerce.com") {
11218
+ var KNOWN_API_URLS = {
11219
+ production: "https://api.brainerce.com",
11220
+ staging: "https://api-staging.brainerce.com"
11221
+ };
11222
+ var ConnectionNotFoundError = class extends Error {
11223
+ constructor(connectionId, baseUrl) {
11224
+ super(`Connection "${connectionId}" not found at ${baseUrl}`);
11225
+ this.code = "NOT_FOUND";
11226
+ }
11227
+ };
11228
+ async function fetchStoreInfo(connectionId, baseUrl = KNOWN_API_URLS.production) {
10635
11229
  const url = `${baseUrl}/api/vc/${connectionId}/info`;
10636
- return new Promise((resolve, reject) => {
10637
- const client = url.startsWith("https") ? import_https.default : import_http.default;
10638
- const req = client.get(url, { timeout: 1e4 }, (res) => {
10639
- let data = "";
10640
- res.on("data", (chunk) => {
10641
- data += chunk;
10642
- });
10643
- res.on("end", () => {
10644
- if (res.statusCode && res.statusCode >= 400) {
10645
- if (res.statusCode === 404) {
10646
- reject(new Error(`Connection ID "${connectionId}" not found. Check your dashboard.`));
10647
- } else {
10648
- reject(new Error(`API returned status ${res.statusCode}`));
10649
- }
10650
- return;
10651
- }
10652
- try {
10653
- const json = JSON.parse(data);
10654
- resolve({
10655
- name: json.name || json.storeName || "My Store",
10656
- currency: json.currency || "USD",
10657
- language: json.language || "en"
10658
- });
10659
- } catch {
10660
- reject(new Error("Invalid response from API"));
10661
- }
10662
- });
10663
- });
10664
- req.on("error", (err) => {
10665
- reject(new Error(`Failed to connect to Brainerce API: ${err.message}`));
10666
- });
10667
- req.on("timeout", () => {
10668
- req.destroy();
10669
- reject(new Error("Request timed out"));
10670
- });
10671
- });
11230
+ const controller = new AbortController();
11231
+ const timeout = setTimeout(() => controller.abort(), 1e4);
11232
+ let res;
11233
+ try {
11234
+ res = await fetch(url, { signal: controller.signal });
11235
+ } catch (err) {
11236
+ if (err.name === "AbortError") {
11237
+ throw new Error(`Request to ${baseUrl} timed out`);
11238
+ }
11239
+ throw new Error(`Failed to connect to ${baseUrl}: ${err.message}`);
11240
+ } finally {
11241
+ clearTimeout(timeout);
11242
+ }
11243
+ if (res.status === 404) {
11244
+ throw new ConnectionNotFoundError(connectionId, baseUrl);
11245
+ }
11246
+ if (!res.ok) {
11247
+ throw new Error(`${baseUrl} returned status ${res.status}`);
11248
+ }
11249
+ let json;
11250
+ try {
11251
+ json = await res.json();
11252
+ } catch {
11253
+ throw new Error(`Invalid response from ${baseUrl}`);
11254
+ }
11255
+ return {
11256
+ name: json.name || json.storeName || "My Store",
11257
+ currency: json.currency || "USD",
11258
+ language: json.language || "en",
11259
+ ...json.i18n ? { i18n: json.i18n } : {}
11260
+ };
11261
+ }
11262
+ async function resolveStoreInfo(connectionId, candidateUrls) {
11263
+ const urls = candidateUrls.filter((u, i) => u && candidateUrls.indexOf(u) === i);
11264
+ if (urls.length === 0) {
11265
+ throw new Error("No API URLs to try");
11266
+ }
11267
+ let lastError;
11268
+ let allNotFound = true;
11269
+ for (let i = 0; i < urls.length; i++) {
11270
+ const baseUrl = urls[i];
11271
+ try {
11272
+ const info = await fetchStoreInfo(connectionId, baseUrl);
11273
+ return { info, apiBaseUrl: baseUrl, fellBack: i > 0 };
11274
+ } catch (err) {
11275
+ lastError = err;
11276
+ const isNotFound = err.code === "NOT_FOUND";
11277
+ if (!isNotFound) {
11278
+ allNotFound = false;
11279
+ }
11280
+ }
11281
+ }
11282
+ if (allNotFound) {
11283
+ throw new Error(
11284
+ `Connection "${connectionId}" not found in any known environment (${urls.join(", ")}).`
11285
+ );
11286
+ }
11287
+ throw lastError || new Error("Failed to resolve store info");
11288
+ }
11289
+ function getCandidateApiUrls() {
11290
+ const explicit = (process.env.BRAINERCE_API_URL || "").replace(/\/$/, "");
11291
+ if (explicit) return [explicit];
11292
+ return [KNOWN_API_URLS.production, KNOWN_API_URLS.staging];
10672
11293
  }
10673
11294
 
10674
11295
  // src/tools/get-store-info.ts
@@ -10679,17 +11300,18 @@ var GET_STORE_INFO_SCHEMA = {
10679
11300
  };
10680
11301
  async function handleGetStoreInfo(args) {
10681
11302
  try {
10682
- const info = await fetchStoreInfo(args.connectionId);
11303
+ const resolved = await resolveStoreInfo(args.connectionId, getCandidateApiUrls());
10683
11304
  return {
10684
11305
  content: [
10685
11306
  {
10686
11307
  type: "text",
10687
11308
  text: JSON.stringify(
10688
11309
  {
10689
- name: info.name,
10690
- currency: info.currency,
10691
- language: info.language,
10692
- connectionId: args.connectionId
11310
+ name: resolved.info.name,
11311
+ currency: resolved.info.currency,
11312
+ language: resolved.info.language,
11313
+ connectionId: args.connectionId,
11314
+ apiBaseUrl: resolved.apiBaseUrl
10693
11315
  },
10694
11316
  null,
10695
11317
  2
@@ -10710,41 +11332,65 @@ async function handleGetStoreInfo(args) {
10710
11332
  var import_zod6 = require("zod");
10711
11333
 
10712
11334
  // src/utils/fetch-store-capabilities.ts
10713
- var import_https2 = __toESM(require("https"));
10714
- var import_http2 = __toESM(require("http"));
11335
+ var ConnectionNotFoundError2 = class extends Error {
11336
+ constructor(connectionId, baseUrl) {
11337
+ super(`Connection "${connectionId}" not found at ${baseUrl}`);
11338
+ this.code = "NOT_FOUND";
11339
+ }
11340
+ };
10715
11341
  async function fetchStoreCapabilities(connectionId, baseUrl = "https://api.brainerce.com") {
10716
11342
  const url = `${baseUrl}/api/vc/${connectionId}/capabilities`;
10717
- return new Promise((resolve, reject) => {
10718
- const client = url.startsWith("https") ? import_https2.default : import_http2.default;
10719
- const req = client.get(url, { timeout: 1e4 }, (res) => {
10720
- let data = "";
10721
- res.on("data", (chunk) => {
10722
- data += chunk;
10723
- });
10724
- res.on("end", () => {
10725
- if (res.statusCode && res.statusCode >= 400) {
10726
- if (res.statusCode === 404) {
10727
- reject(new Error(`Connection ID "${connectionId}" not found. Check your dashboard.`));
10728
- } else {
10729
- reject(new Error(`API returned status ${res.statusCode}`));
10730
- }
10731
- return;
10732
- }
10733
- try {
10734
- resolve(JSON.parse(data));
10735
- } catch {
10736
- reject(new Error("Invalid response from API"));
10737
- }
10738
- });
10739
- });
10740
- req.on("error", (err) => {
10741
- reject(new Error(`Failed to connect to Brainerce API: ${err.message}`));
10742
- });
10743
- req.on("timeout", () => {
10744
- req.destroy();
10745
- reject(new Error("Request timed out"));
10746
- });
10747
- });
11343
+ const controller = new AbortController();
11344
+ const timeout = setTimeout(() => controller.abort(), 1e4);
11345
+ let res;
11346
+ try {
11347
+ res = await fetch(url, { signal: controller.signal });
11348
+ } catch (err) {
11349
+ if (err.name === "AbortError") {
11350
+ throw new Error(`Request to ${baseUrl} timed out`);
11351
+ }
11352
+ throw new Error(`Failed to connect to ${baseUrl}: ${err.message}`);
11353
+ } finally {
11354
+ clearTimeout(timeout);
11355
+ }
11356
+ if (res.status === 404) {
11357
+ throw new ConnectionNotFoundError2(connectionId, baseUrl);
11358
+ }
11359
+ if (!res.ok) {
11360
+ throw new Error(`${baseUrl} returned status ${res.status}`);
11361
+ }
11362
+ try {
11363
+ return await res.json();
11364
+ } catch {
11365
+ throw new Error(`Invalid response from ${baseUrl}`);
11366
+ }
11367
+ }
11368
+ async function resolveStoreCapabilities(connectionId, candidateUrls) {
11369
+ const urls = candidateUrls.filter((u, i) => u && candidateUrls.indexOf(u) === i);
11370
+ if (urls.length === 0) {
11371
+ throw new Error("No API URLs to try");
11372
+ }
11373
+ let lastError;
11374
+ let allNotFound = true;
11375
+ for (let i = 0; i < urls.length; i++) {
11376
+ const baseUrl = urls[i];
11377
+ try {
11378
+ const capabilities = await fetchStoreCapabilities(connectionId, baseUrl);
11379
+ return { capabilities, apiBaseUrl: baseUrl, fellBack: i > 0 };
11380
+ } catch (err) {
11381
+ lastError = err;
11382
+ const isNotFound = err.code === "NOT_FOUND";
11383
+ if (!isNotFound) {
11384
+ allNotFound = false;
11385
+ }
11386
+ }
11387
+ }
11388
+ if (allNotFound) {
11389
+ throw new Error(
11390
+ `Connection "${connectionId}" not found in any known environment (${urls.join(", ")}).`
11391
+ );
11392
+ }
11393
+ throw lastError || new Error("Failed to resolve store capabilities");
10748
11394
  }
10749
11395
 
10750
11396
  // src/tools/get-store-capabilities.ts
@@ -10757,6 +11403,11 @@ function formatCapabilities(caps) {
10757
11403
  const lines = [];
10758
11404
  lines.push(`## Store: ${caps.store.name} (${caps.store.currency})`);
10759
11405
  lines.push(`Language: ${caps.store.language}`);
11406
+ if (caps.store.i18n?.enabled) {
11407
+ lines.push(
11408
+ `Multi-language: ENABLED \u2014 locales: ${caps.store.i18n.supportedLocales.join(", ")} (default: ${caps.store.i18n.defaultLocale})`
11409
+ );
11410
+ }
10760
11411
  lines.push("");
10761
11412
  lines.push("## Configured Features");
10762
11413
  if (caps.features.paymentProviders.length > 0) {
@@ -10843,6 +11494,11 @@ function formatCapabilities(caps) {
10843
11494
  'Inventory reservation is active \u2014 show a countdown timer. Use get-code-example("reservation-countdown").'
10844
11495
  );
10845
11496
  }
11497
+ if (caps.store.i18n?.enabled && caps.store.i18n.supportedLocales.length > 1) {
11498
+ suggestions.push(
11499
+ `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.`
11500
+ );
11501
+ }
10846
11502
  if (suggestions.length === 0) {
10847
11503
  suggestions.push(
10848
11504
  "Start by building the required pages. Use get-required-pages() for the checklist."
@@ -10853,9 +11509,9 @@ function formatCapabilities(caps) {
10853
11509
  }
10854
11510
  async function handleGetStoreCapabilities(args) {
10855
11511
  try {
10856
- const caps = await fetchStoreCapabilities(args.connectionId);
11512
+ const resolved = await resolveStoreCapabilities(args.connectionId, getCandidateApiUrls());
10857
11513
  return {
10858
- content: [{ type: "text", text: formatCapabilities(caps) }]
11514
+ content: [{ type: "text", text: formatCapabilities(resolved.capabilities) }]
10859
11515
  };
10860
11516
  } catch (error) {
10861
11517
  const message = error instanceof Error ? error.message : "Failed to fetch store capabilities";
@@ -10950,6 +11606,20 @@ function formatCapabilitiesSummary(caps) {
10950
11606
  const lines = [];
10951
11607
  lines.push(`## Store: ${caps.store.name}`);
10952
11608
  lines.push(`Currency: ${caps.store.currency} | Language: ${caps.store.language}`);
11609
+ if (caps.store.i18n?.enabled && caps.store.i18n.supportedLocales.length > 1) {
11610
+ lines.push(
11611
+ `Multi-language: ${caps.store.i18n.supportedLocales.join(", ")} (default: ${caps.store.i18n.defaultLocale})`
11612
+ );
11613
+ lines.push("");
11614
+ lines.push("### i18n Implementation");
11615
+ lines.push(
11616
+ "- Call `client.setLocale(locale)` at app init based on URL prefix or user preference"
11617
+ );
11618
+ lines.push("- All getProducts/getCategories/getBrands calls auto-include the locale");
11619
+ lines.push("- Use locale-prefixed routes: `/[locale]/products`, `/[locale]/products/[slug]`");
11620
+ lines.push("- Add a language switcher component in the header");
11621
+ lines.push('- RTL locales (he, ar): set `<html dir="rtl">` \u2014 flexbox reversal is automatic');
11622
+ }
10953
11623
  lines.push("");
10954
11624
  lines.push("### Configured Features");
10955
11625
  if (caps.features.paymentProviders.length > 0) {
@@ -11003,8 +11673,7 @@ function buildStoreBundle(options) {
11003
11673
  connectionId,
11004
11674
  storeName,
11005
11675
  currency,
11006
- language: capabilities?.store.language || "en",
11007
- apiUrl: "https://api.brainerce.com"
11676
+ language: capabilities?.store.language || "en"
11008
11677
  };
11009
11678
  const sections = [];
11010
11679
  sections.push(`# Build: ${storeName} \u2014 ${storeType}${styleDesc}
@@ -11107,7 +11776,8 @@ var BUILD_STORE_SCHEMA = {
11107
11776
  async function handleBuildStore(args) {
11108
11777
  let capabilities = null;
11109
11778
  try {
11110
- capabilities = await fetchStoreCapabilities(args.connectionId);
11779
+ const resolved = await resolveStoreCapabilities(args.connectionId, getCandidateApiUrls());
11780
+ capabilities = resolved.capabilities;
11111
11781
  } catch {
11112
11782
  }
11113
11783
  const bundle = buildStoreBundle({
@@ -11446,6 +12116,57 @@ ${results.join("\n\n---\n\n")}`
11446
12116
  };
11447
12117
  }
11448
12118
 
12119
+ // src/tools/get-integration-guide.ts
12120
+ var import_zod10 = require("zod");
12121
+ var GET_INTEGRATION_GUIDE_NAME = "get-integration-guide";
12122
+ 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.';
12123
+ var GET_INTEGRATION_GUIDE_SCHEMA = {
12124
+ part: import_zod10.z.enum(["core", "optional", "rules"]).describe(
12125
+ '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.'
12126
+ )
12127
+ };
12128
+ var GUIDE_URLS = {
12129
+ core: "https://brainerce.com/docs/integration/raw?part=core",
12130
+ optional: "https://brainerce.com/docs/integration/raw?part=optional",
12131
+ rules: "https://brainerce.com/docs/integration/raw?part=rules"
12132
+ };
12133
+ async function handleGetIntegrationGuide(args) {
12134
+ const url = GUIDE_URLS[args.part];
12135
+ if (!url) {
12136
+ return {
12137
+ content: [
12138
+ {
12139
+ type: "text",
12140
+ text: `Unknown part: "${args.part}". Available parts: core, optional, rules.`
12141
+ }
12142
+ ]
12143
+ };
12144
+ }
12145
+ try {
12146
+ const response = await fetch(url, {
12147
+ headers: { Accept: "text/plain" },
12148
+ signal: AbortSignal.timeout(15e3)
12149
+ });
12150
+ if (!response.ok) {
12151
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
12152
+ }
12153
+ const content = await response.text();
12154
+ return {
12155
+ content: [{ type: "text", text: content }]
12156
+ };
12157
+ } catch (error) {
12158
+ const message = error instanceof Error ? error.message : "Unknown error";
12159
+ return {
12160
+ content: [
12161
+ {
12162
+ type: "text",
12163
+ text: `Failed to fetch integration guide (${args.part}): ${message}. You can read it directly at: ${url}`
12164
+ }
12165
+ ]
12166
+ };
12167
+ }
12168
+ }
12169
+
11449
12170
  // src/resources/sdk-types.ts
11450
12171
  var SDK_TYPES_URI = "brainerce://sdk/types";
11451
12172
  var SDK_TYPES_NAME = "Brainerce SDK Types";
@@ -11600,15 +12321,15 @@ async function handleProjectTemplate(uri) {
11600
12321
  }
11601
12322
 
11602
12323
  // src/prompts/create-store.ts
11603
- var import_zod10 = require("zod");
12324
+ var import_zod11 = require("zod");
11604
12325
  var CREATE_STORE_NAME = "create-store";
11605
12326
  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.";
11606
12327
  var CREATE_STORE_SCHEMA = {
11607
- connectionId: import_zod10.z.string().describe("Vibe-coded connection ID (starts with vc_)"),
11608
- storeName: import_zod10.z.string().describe('Name of the store (e.g., "Urban Threads")'),
11609
- storeType: import_zod10.z.string().describe('Type of store (e.g., "Clothing store", "Electronics shop", "Bakery")'),
11610
- currency: import_zod10.z.string().optional().default("USD").describe("Store currency code (e.g., USD, ILS, EUR)"),
11611
- storeStyle: import_zod10.z.string().optional().describe('Design style (e.g., "minimalist", "luxury", "playful", "dark mode")')
12328
+ connectionId: import_zod11.z.string().describe("Vibe-coded connection ID (starts with vc_)"),
12329
+ storeName: import_zod11.z.string().describe('Name of the store (e.g., "Urban Threads")'),
12330
+ storeType: import_zod11.z.string().describe('Type of store (e.g., "Clothing store", "Electronics shop", "Bakery")'),
12331
+ currency: import_zod11.z.string().optional().default("USD").describe("Store currency code (e.g., USD, ILS, EUR)"),
12332
+ storeStyle: import_zod11.z.string().optional().describe('Design style (e.g., "minimalist", "luxury", "playful", "dark mode")')
11612
12333
  };
11613
12334
  function handleCreateStore(args) {
11614
12335
  const styleDesc = args.storeStyle ? ` with a ${args.storeStyle} design style` : "";
@@ -11647,6 +12368,13 @@ DO NOT STOP until all 13 pages + header are implemented:
11647
12368
  13. \`/account\` \u2014 Account dashboard
11648
12369
  14. Header component with cart count + search
11649
12370
 
12371
+ ## Multi-Language
12372
+ If the store has multi-language enabled (check build-store output), implement:
12373
+ - Locale-prefixed routes: /[locale]/products instead of /products
12374
+ - Call client.setLocale(locale) based on URL prefix
12375
+ - Language switcher in header
12376
+ - dir="rtl" on <html> for RTL locales (he, ar)
12377
+
11650
12378
  ## Step 3: Validate
11651
12379
  After building all pages, call \`validate-store\` with the list of files you created to verify nothing is missing.
11652
12380
 
@@ -11665,11 +12393,11 @@ After building all pages, call \`validate-store\` with the list of files you cre
11665
12393
  }
11666
12394
 
11667
12395
  // src/prompts/add-feature.ts
11668
- var import_zod11 = require("zod");
12396
+ var import_zod12 = require("zod");
11669
12397
  var ADD_FEATURE_NAME = "add-feature";
11670
12398
  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.";
11671
12399
  var ADD_FEATURE_SCHEMA = {
11672
- feature: import_zod11.z.enum([
12400
+ feature: import_zod12.z.enum([
11673
12401
  "products",
11674
12402
  "cart",
11675
12403
  "checkout",
@@ -11682,8 +12410,8 @@ var ADD_FEATURE_SCHEMA = {
11682
12410
  "search",
11683
12411
  "tax"
11684
12412
  ]).describe("The feature to add"),
11685
- connectionId: import_zod11.z.string().optional().describe("Vibe-coded connection ID (starts with vc_)"),
11686
- currency: import_zod11.z.string().optional().default("USD").describe("Store currency code")
12413
+ connectionId: import_zod12.z.string().optional().describe("Vibe-coded connection ID (starts with vc_)"),
12414
+ currency: import_zod12.z.string().optional().default("USD").describe("Store currency code")
11687
12415
  };
11688
12416
  var FEATURE_TO_TOPICS = {
11689
12417
  products: { topics: ["products"], domains: ["products"] },
@@ -11798,6 +12526,12 @@ function createServer() {
11798
12526
  GET_PAGE_CODE_SCHEMA,
11799
12527
  handleGetPageCode
11800
12528
  );
12529
+ server.tool(
12530
+ GET_INTEGRATION_GUIDE_NAME,
12531
+ GET_INTEGRATION_GUIDE_DESCRIPTION,
12532
+ GET_INTEGRATION_GUIDE_SCHEMA,
12533
+ handleGetIntegrationGuide
12534
+ );
11801
12535
  server.resource(
11802
12536
  SDK_TYPES_NAME,
11803
12537
  SDK_TYPES_URI,