@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/index.mjs CHANGED
@@ -152,7 +152,7 @@ import {
152
152
  getVariantPrice, getVariantOptions, getStockStatus,
153
153
  getCartItemName, getCartItemImage,
154
154
  getDescriptionContent, isHtmlDescription,
155
- getProductMetafieldValue,
155
+ getProductMetafieldValue, getProductCustomizationFields,
156
156
  } from 'brainerce';
157
157
  \`\`\``;
158
158
  }
@@ -194,6 +194,7 @@ async function startCheckout() {
194
194
  4. Start checkout session (\`startGuestCheckout()\` or \`createCheckout()\`)
195
195
  5. Set shipping address (includes required email) \u2192 \`setShippingAddress()\`
196
196
  6. Select shipping method \u2192 \`selectShippingMethod()\`
197
+ 6b. (Optional) Set checkout custom fields \u2192 \`setCheckoutCustomFields()\` \u2014 surcharges auto-calculated
197
198
  7. Create payment intent \u2192 \`createPaymentIntent()\` \u2192 returns \`{ clientSecret, provider }\`
198
199
  8. Branch on \`provider\`: \`'stripe'\` \u2192 Stripe Elements, \`'grow'\` \u2192 iframe, \`'paypal'\` \u2192 PayPal Buttons
199
200
  9. **Order is created AUTOMATICALLY after payment succeeds (via webhook) \u2014 for ALL providers!**
@@ -284,6 +285,20 @@ function CheckoutPage() {
284
285
  if (!paymentData) return <div>Unable to initialize payment</div>;
285
286
 
286
287
  // Render payment UI based on provider
288
+ if (paymentData.provider === 'sandbox') {
289
+ return (
290
+ <div className="text-center p-6 bg-amber-50 border border-amber-200 rounded-lg">
291
+ <h3 className="font-semibold mb-2">Test Mode</h3>
292
+ <p className="text-sm text-gray-600 mb-4">No real payment will be charged.</p>
293
+ <button onClick={async () => {
294
+ await client.completeGuestCheckout(checkoutId);
295
+ window.location.href = \\\`/order-confirmation?checkout_id=\\\${checkoutId}\\\`;
296
+ }} className="bg-amber-500 text-white px-6 py-2 rounded">
297
+ Complete Test Order
298
+ </button>
299
+ </div>
300
+ );
301
+ }
287
302
  if (paymentData.provider === 'grow') {
288
303
  return <GrowPaymentForm paymentUrl={paymentData.clientSecret} checkoutId={paymentData.checkoutId} />;
289
304
  }
@@ -402,11 +417,12 @@ const growProvider = providers.find(p => p.provider === 'grow');
402
417
  const paypalProvider = providers.find(p => p.provider === 'paypal');
403
418
  \`\`\`
404
419
 
405
- Each provider has: \`id\`, \`provider\` ('stripe'|'grow'|'paypal'), \`name\`, \`publicKey\`, \`stripeAccountId\` (Stripe only), \`supportedMethods\`, \`testMode\`, \`isDefault\`.
420
+ Each provider has: \`id\`, \`provider\` ('stripe'|'grow'|'paypal'|'sandbox'), \`name\`, \`publicKey\`, \`stripeAccountId\` (Stripe only), \`supportedMethods\`, \`testMode\`, \`isDefault\`.
406
421
 
407
422
  - **Stripe:** \`npm install @stripe/stripe-js @stripe/react-stripe-js\` \u2014 \`loadStripe(publicKey, { stripeAccount })\`
408
423
  - **Grow:** No SDK \u2014 uses iframe with payment URL. Supports credit cards, Bit, Apple Pay, Google Pay.
409
- - **PayPal:** \`npm install @paypal/react-paypal-js\` \u2014 \`PayPalScriptProvider\` + \`PayPalButtons\``;
424
+ - **PayPal:** \`npm install @paypal/react-paypal-js\` \u2014 \`PayPalScriptProvider\` + \`PayPalButtons\`
425
+ - **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\`.`;
410
426
  }
411
427
  function getProductsSection(_currency) {
412
428
  return `## Products & Variants
@@ -561,6 +577,35 @@ const material = getProductMetafieldValue(product, 'material');
561
577
 
562
578
  **Metafield fields:** \`definitionName\` (display label), \`definitionKey\` (lookup key), \`value\`, \`type\` (IMAGE, GALLERY, URL, COLOR, BOOLEAN, DATE, DATETIME, TEXT, TEXTAREA, NUMBER, DIMENSION, WEIGHT, JSON)
563
579
 
580
+ ### Product Customization Fields (Customer Input)
581
+
582
+ 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.
583
+
584
+ \`\`\`typescript
585
+ import { getProductCustomizationFields } from 'brainerce';
586
+ import type { ProductCustomizationField } from 'brainerce';
587
+
588
+ const fields = getProductCustomizationFields(product);
589
+
590
+ // Render input for each field based on field.type:
591
+ // TEXT/TEXTAREA \u2192 text input, NUMBER \u2192 number input, BOOLEAN \u2192 checkbox,
592
+ // COLOR \u2192 color picker, DATE \u2192 date picker, IMAGE \u2192 file upload, etc.
593
+ // Check field.required, field.minLength, field.maxLength, field.enumValues for validation.
594
+
595
+ // Pass customer values in metadata when adding to cart:
596
+ await client.addToCart(cartId, {
597
+ productId: product.id,
598
+ quantity: 1,
599
+ metadata: { cake_text: 'Happy Birthday!' },
600
+ });
601
+
602
+ // For IMAGE fields, upload first:
603
+ const { url } = await client.uploadCustomizationFile(file);
604
+ // Then use the URL in metadata: metadata: { logo: url }
605
+ \`\`\`
606
+
607
+ **Customization field properties:** \`key\`, \`name\` (display label), \`type\`, \`required\`, \`minLength\`, \`maxLength\`, \`minValue\`, \`maxValue\`, \`enumValues\`, \`defaultValue\`, \`position\`
608
+
564
609
  ### Downloadable / Digital Products
565
610
 
566
611
  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.
@@ -747,9 +792,14 @@ const { bundles } = await client.getCartBundles(cartId);
747
792
  // bundles[].bundleProduct \u2014 the product to offer
748
793
  // bundles[].originalPrice / bundles[].discountedPrice \u2014 prices
749
794
  // bundles[].discountType ('PERCENTAGE' | 'FIXED_AMOUNT') + discountValue
795
+ // bundles[].requiresVariantSelection \u2014 true if customer must pick a variant
796
+ // bundles[].lockedVariant \u2014 set if admin pre-selected a specific variant
797
+ // bundles[].bundleProduct.variants \u2014 available variants (only when requiresVariantSelection)
750
798
 
751
799
  // Add a bundle to cart (applies the discount automatically):
752
800
  await client.addBundleToCart(cartId, bundleOfferId);
801
+ // For products with variants \u2014 pass the selected variantId:
802
+ await client.addBundleToCart(cartId, bundleOfferId, selectedVariantId);
753
803
  // Remove a bundle from cart:
754
804
  await client.removeBundleFromCart(cartId, bundleOfferId);
755
805
  // Detect already-added bundles: check cart.items for metadata?.isBundleItem === true
@@ -774,9 +824,14 @@ const { bumps } = await client.getCheckoutBumps(checkoutId);
774
824
  // bumps[].bumpProduct \u2014 product to offer
775
825
  // bumps[].originalPrice / discountedPrice \u2014 with optional discount
776
826
  // bumps[].title / description \u2014 display text
827
+ // bumps[].requiresVariantSelection \u2014 true if customer must pick a variant
828
+ // bumps[].lockedVariant \u2014 set if admin pre-selected a specific variant
829
+ // bumps[].bumpProduct.variants \u2014 available variants (only when requiresVariantSelection)
777
830
 
778
831
  // Add a bump to cart:
779
832
  await client.addOrderBump(cartId, bumpId);
833
+ // For products with variants \u2014 pass the selected variantId:
834
+ await client.addOrderBump(cartId, bumpId, selectedVariantId);
780
835
  // Remove a bump:
781
836
  await client.removeOrderBump(cartId, bumpId);
782
837
  // Detect already-added bumps: check cart.items for metadata?.isOrderBump === true
@@ -801,10 +856,11 @@ Always check the flag before rendering: \`if (storeInfo?.upsell?.featureName !==
801
856
  | CartRecommendationSection | products/ | Cross-sell grid at bottom of cart page |
802
857
  | FreeShippingBar | cart/ | Progress bar toward free shipping threshold |
803
858
  | CartUpgradeBanner | cart/ | Inline "Upgrade to X for +$Y" per cart item |
804
- | CartBundleOfferCard | cart/ | Discounted bundle offer card in cart |
805
- | OrderBumpCard | checkout/ | Checkbox add-on card in checkout sidebar |
859
+ | CartBundleOfferCard | cart/ | Discounted bundle offer card in cart (with inline variant selector for variable products) |
860
+ | OrderBumpCard | checkout/ | Checkbox add-on card in checkout sidebar (with inline variant selector for variable products) |
806
861
 
807
- ProductRecommendation: \`id\`, \`name\`, \`slug\`, \`basePrice\`, \`salePrice\`, \`images\`, \`type\`, \`inventory\`, \`relationType\`.`;
862
+ ProductRecommendation: \`id\`, \`name\`, \`slug\`, \`basePrice\`, \`salePrice\`, \`images\`, \`type\`, \`inventory\`, \`relationType\`, \`variants?\` (when variant selection needed).
863
+ OrderBump/CartBundleOffer: includes \`requiresVariantSelection\` (boolean) and \`lockedVariant?\` (\`{ id, name, attributes }\`).`;
808
864
  }
809
865
  function getInventorySection() {
810
866
  return `## Inventory, Stock Display & Reservation Countdown
@@ -1944,6 +2000,15 @@ export function getClient(): BrainerceClient {
1944
2000
  }
1945
2001
  return clientInstance;
1946
2002
  }
2003
+ <% if (i18nEnabled) { %>
2004
+
2005
+ /** Initialize client with a specific locale for translated content */
2006
+ export function initClientWithLocale(locale: string): BrainerceClient {
2007
+ const client = getClient();
2008
+ client.setLocale(locale);
2009
+ return client;
2010
+ }
2011
+ <% } %>
1947
2012
 
1948
2013
  // Cart ID helpers (not a security token \u2014 safe in localStorage)
1949
2014
  const CART_ID_KEY = 'brainerce_cart_id';
@@ -2142,6 +2207,10 @@ import type { StoreInfo, Cart, CustomerProfile } from 'brainerce';
2142
2207
  import { getCartTotals } from 'brainerce';
2143
2208
  import { getClient, initClient, setStoredCartId } from '@/lib/brainerce';
2144
2209
  import { checkAuthStatus, proxyLogout } from '@/lib/auth';
2210
+ <% if (i18nEnabled) { %>
2211
+ import { MessagesContext } from '@/lib/translations';
2212
+ import { getMessages, defaultLocale } from '@/i18n';
2213
+ <% } %>
2145
2214
 
2146
2215
  // ---- Store Info Context ----
2147
2216
  interface StoreInfoContextValue {
@@ -2201,7 +2270,11 @@ export function useCart() {
2201
2270
  }
2202
2271
 
2203
2272
  // ---- Provider Component ----
2273
+ <% if (i18nEnabled) { %>
2274
+ export function StoreProvider({ children, locale }: { children: React.ReactNode; locale?: string }) {
2275
+ <% } else { %>
2204
2276
  export function StoreProvider({ children }: { children: React.ReactNode }) {
2277
+ <% } %>
2205
2278
  const [storeInfo, setStoreInfo] = useState<StoreInfo | null>(null);
2206
2279
  const [storeLoading, setStoreLoading] = useState(true);
2207
2280
  const [isLoggedIn, setIsLoggedIn] = useState(false);
@@ -2209,6 +2282,9 @@ export function StoreProvider({ children }: { children: React.ReactNode }) {
2209
2282
  const [authLoading, setAuthLoading] = useState(true);
2210
2283
  const [cart, setCart] = useState<Cart | null>(null);
2211
2284
  const [cartLoading, setCartLoading] = useState(true);
2285
+ <% if (i18nEnabled) { %>
2286
+ const [messages, setMessages] = useState<Record<string, Record<string, string>>>({});
2287
+ <% } %>
2212
2288
 
2213
2289
  // Check auth status via httpOnly cookie (server-side validation)
2214
2290
  const refreshAuth = useCallback(async () => {
@@ -2227,6 +2303,16 @@ export function StoreProvider({ children }: { children: React.ReactNode }) {
2227
2303
  // Initialize client, check auth, and fetch store info
2228
2304
  useEffect(() => {
2229
2305
  const client = initClient();
2306
+ <% if (i18nEnabled) { %>
2307
+
2308
+ // Set locale on SDK client for translated product content
2309
+ if (locale) {
2310
+ client.setLocale(locale);
2311
+ }
2312
+
2313
+ // Load UI message strings for current locale
2314
+ getMessages(locale || defaultLocale).then(setMessages);
2315
+ <% } %>
2230
2316
 
2231
2317
  // Optimistic check: if brainerce_logged_in cookie exists, assume logged in
2232
2318
  // while we validate the actual token server-side
@@ -2243,7 +2329,11 @@ export function StoreProvider({ children }: { children: React.ReactNode }) {
2243
2329
  .then(setStoreInfo)
2244
2330
  .catch(console.error)
2245
2331
  .finally(() => setStoreLoading(false));
2332
+ <% if (i18nEnabled) { %>
2333
+ }, [refreshAuth, locale]);
2334
+ <% } else { %>
2246
2335
  }, [refreshAuth]);
2336
+ <% } %>
2247
2337
 
2248
2338
  // Cart management
2249
2339
  const refreshCart = useCallback(async () => {
@@ -2296,6 +2386,21 @@ export function StoreProvider({ children }: { children: React.ReactNode }) {
2296
2386
 
2297
2387
  const totals = cart ? getCartTotals(cart) : { subtotal: 0, discount: 0, shipping: 0, total: 0 };
2298
2388
 
2389
+ <% if (i18nEnabled) { %>
2390
+ return (
2391
+ <MessagesContext.Provider value={messages}>
2392
+ <StoreInfoContext.Provider value={{ storeInfo, loading: storeLoading }}>
2393
+ <AuthContext.Provider value={{ isLoggedIn, authLoading, customer, login, logout }}>
2394
+ <CartContext.Provider
2395
+ value={{ cart, cartLoading, refreshCart, itemCount, totals }}
2396
+ >
2397
+ {children}
2398
+ </CartContext.Provider>
2399
+ </AuthContext.Provider>
2400
+ </StoreInfoContext.Provider>
2401
+ </MessagesContext.Provider>
2402
+ );
2403
+ <% } else { %>
2299
2404
  return (
2300
2405
  <StoreInfoContext.Provider value={{ storeInfo, loading: storeLoading }}>
2301
2406
  <AuthContext.Provider value={{ isLoggedIn, authLoading, customer, login, logout }}>
@@ -2307,6 +2412,7 @@ export function StoreProvider({ children }: { children: React.ReactNode }) {
2307
2412
  </AuthContext.Provider>
2308
2413
  </StoreInfoContext.Provider>
2309
2414
  );
2415
+ <% } %>
2310
2416
  }
2311
2417
  `,
2312
2418
  "src/app/page.tsx": `'use client';
@@ -2408,7 +2514,81 @@ export default function HomePage() {
2408
2514
  );
2409
2515
  }
2410
2516
  `,
2411
- "src/app/layout.tsx.ejs": `import type { Metadata } from 'next';
2517
+ "src/app/layout.tsx.ejs": `<% if (i18nEnabled) { %>
2518
+ import type { Metadata } from 'next';
2519
+ <%- fontImport %>
2520
+ import { StoreProvider } from '@/providers/store-provider';
2521
+ import { Header } from '@/components/layout/header';
2522
+ import { Footer } from '@/components/layout/footer';
2523
+ import { getDirection, supportedLocales } from '@/i18n';
2524
+ import '../globals.css';
2525
+
2526
+ <%- fontVariable %>
2527
+
2528
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com';
2529
+
2530
+ export const metadata: Metadata = {
2531
+ metadataBase: new URL(baseUrl),
2532
+ title: {
2533
+ default: 'My Store',
2534
+ template: \`%s | My Store\`,
2535
+ },
2536
+ description: 'My Store',
2537
+ alternates: {
2538
+ canonical: '/',
2539
+ },
2540
+ openGraph: {
2541
+ siteName: 'My Store',
2542
+ type: 'website',
2543
+ },
2544
+ robots: {
2545
+ index: true,
2546
+ follow: true,
2547
+ },
2548
+ };
2549
+
2550
+ const organizationJsonLd = {
2551
+ '@context': 'https://schema.org',
2552
+ '@type': 'Organization',
2553
+ name: 'My Store',
2554
+ url: baseUrl,
2555
+ };
2556
+
2557
+ export function generateStaticParams() {
2558
+ return supportedLocales.map((locale) => ({ locale }));
2559
+ }
2560
+
2561
+ export default function RootLayout({
2562
+ children,
2563
+ params,
2564
+ }: {
2565
+ children: React.ReactNode;
2566
+ params: { locale: string };
2567
+ }) {
2568
+ const dir = getDirection(params.locale);
2569
+
2570
+ return (
2571
+ <html lang={params.locale} dir={dir}>
2572
+ <head>
2573
+ <script
2574
+ type="application/ld+json"
2575
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(organizationJsonLd) }}
2576
+ />
2577
+ </head>
2578
+ <body className={font.className}>
2579
+ <StoreProvider locale={params.locale}>
2580
+ <div className="min-h-screen flex flex-col">
2581
+ <Header />
2582
+ <main className="flex-1">{children}</main>
2583
+ <Footer />
2584
+ </div>
2585
+ </StoreProvider>
2586
+ </body>
2587
+ </html>
2588
+ );
2589
+ }
2590
+ <% } else { %>
2591
+ import type { Metadata } from 'next';
2412
2592
  <%- fontImport %>
2413
2593
  import { StoreProvider } from '@/providers/store-provider';
2414
2594
  import { Header } from '@/components/layout/header';
@@ -2472,6 +2652,7 @@ export default function RootLayout({
2472
2652
  </html>
2473
2653
  );
2474
2654
  }
2655
+ <% } %>
2475
2656
  `,
2476
2657
  "src/app/products/page.tsx": `'use client';
2477
2658
 
@@ -3372,13 +3553,13 @@ function CheckoutContent() {
3372
3553
  }, [checkout?.id, storeInfo?.upsell?.checkoutOrderBumpEnabled]);
3373
3554
 
3374
3555
  // Handle bump toggle
3375
- async function handleBumpToggle(bumpId: string, add: boolean) {
3556
+ async function handleBumpToggle(bumpId: string, add: boolean, variantId?: string) {
3376
3557
  if (!cart?.id || bumpLoading) return;
3377
3558
  try {
3378
3559
  setBumpLoading(bumpId);
3379
3560
  const client = getClient();
3380
3561
  if (add) {
3381
- await client.addOrderBump(cart.id, bumpId);
3562
+ await client.addOrderBump(cart.id, bumpId, variantId);
3382
3563
  setAddedBumpIds((prev) => new Set([...prev, bumpId]));
3383
3564
  } else {
3384
3565
  await client.removeOrderBump(cart.id, bumpId);
@@ -4076,6 +4257,23 @@ function OrderConfirmationContent() {
4076
4257
  client.handlePaymentSuccess(checkoutId!);
4077
4258
  await refreshCart();
4078
4259
 
4260
+ // For redirect-based payment providers (e.g. CardCom), the customer
4261
+ // returns with provider params in the URL (lowprofilecode, etc.).
4262
+ // Send these to the backend for server-side verification via the
4263
+ // provider's API (e.g. GetLpResult) \u2014 never trust URL params alone.
4264
+ const lowProfileCode =
4265
+ searchParams.get('lowprofilecode') || searchParams.get('LowProfileCode');
4266
+ if (lowProfileCode) {
4267
+ try {
4268
+ await client.confirmSdkPayment(checkoutId!, {
4269
+ paymentIntentId: lowProfileCode,
4270
+ });
4271
+ } catch (err) {
4272
+ console.warn('Redirect payment confirmation failed:', err);
4273
+ // Don't block \u2014 webhook may still process the payment
4274
+ }
4275
+ }
4276
+
4079
4277
  const orderResult = await client.waitForOrder(checkoutId!, {
4080
4278
  maxWaitMs: 30000,
4081
4279
  });
@@ -5592,7 +5790,62 @@ export async function POST(request: NextRequest) {
5592
5790
  return response;
5593
5791
  }
5594
5792
  `,
5595
- "src/middleware.ts": `import { NextRequest, NextResponse } from 'next/server';
5793
+ "src/middleware.ts.ejs": `<% if (i18nEnabled) { %>
5794
+ import { NextRequest, NextResponse } from 'next/server';
5795
+
5796
+ const TOKEN_COOKIE = 'brainerce_customer_token';
5797
+ const PROTECTED_PATHS = ['/account'];
5798
+ const supportedLocales = <%- supportedLocales %>;
5799
+ const defaultLocale = '<%= defaultLocale %>';
5800
+
5801
+ function getLocaleFromPath(pathname: string): string | null {
5802
+ const segment = pathname.split('/')[1];
5803
+ return supportedLocales.includes(segment) ? segment : null;
5804
+ }
5805
+
5806
+ export function middleware(request: NextRequest) {
5807
+ const { pathname } = request.nextUrl;
5808
+
5809
+ // Skip static files and API routes
5810
+ if (
5811
+ pathname.startsWith('/api/') ||
5812
+ pathname.startsWith('/_next/') ||
5813
+ pathname.includes('.')
5814
+ ) {
5815
+ return NextResponse.next();
5816
+ }
5817
+
5818
+ const pathnameLocale = getLocaleFromPath(pathname);
5819
+
5820
+ // Redirect to default locale if no locale prefix
5821
+ if (!pathnameLocale) {
5822
+ const url = request.nextUrl.clone();
5823
+ url.pathname = \`/\${defaultLocale}\${pathname}\`;
5824
+ return NextResponse.redirect(url);
5825
+ }
5826
+
5827
+ // Auth protection (with locale prefix)
5828
+ const pathWithoutLocale = pathname.replace(\`/\${pathnameLocale}\`, '') || '/';
5829
+ const isProtected = PROTECTED_PATHS.some((p) => pathWithoutLocale.startsWith(p));
5830
+ if (isProtected) {
5831
+ const token = request.cookies.get(TOKEN_COOKIE);
5832
+ if (!token?.value) {
5833
+ const loginUrl = new URL(\`/\${pathnameLocale}/login\`, request.url);
5834
+ return NextResponse.redirect(loginUrl);
5835
+ }
5836
+ }
5837
+
5838
+ // Set locale header for server components
5839
+ const response = NextResponse.next();
5840
+ response.headers.set('x-locale', pathnameLocale);
5841
+ return response;
5842
+ }
5843
+
5844
+ export const config = {
5845
+ matcher: ['/((?!_next|api|.*\\\\..*).*)'],
5846
+ };
5847
+ <% } else { %>
5848
+ import { NextRequest, NextResponse } from 'next/server';
5596
5849
 
5597
5850
  const TOKEN_COOKIE = 'brainerce_customer_token';
5598
5851
 
@@ -5617,6 +5870,7 @@ export function middleware(request: NextRequest) {
5617
5870
  export const config = {
5618
5871
  matcher: ['/account/:path*'],
5619
5872
  };
5873
+ <% } %>
5620
5874
  `,
5621
5875
  "src/components/products/product-card.tsx": `'use client';
5622
5876
 
@@ -7478,7 +7732,9 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
7478
7732
  initialized.current = true;
7479
7733
 
7480
7734
  const client = getClient();
7481
- const successUrl = \`\${window.location.origin}/order-confirmation?checkout_id=\${checkoutId}\`;
7735
+ const iframeSuccessUrl = \`\${window.location.origin}/payment-complete?checkout_id=\${checkoutId}\`;
7736
+ const iframeFailedUrl = \`\${window.location.origin}/payment-complete?checkout_id=\${checkoutId}&failed=true\`;
7737
+ const redirectSuccessUrl = \`\${window.location.origin}/order-confirmation?checkout_id=\${checkoutId}\`;
7482
7738
  const cancelUrl = \`\${window.location.origin}/checkout?checkout_id=\${checkoutId}&canceled=true\`;
7483
7739
 
7484
7740
  let sdkInitDone = false;
@@ -7639,8 +7895,19 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
7639
7895
  });
7640
7896
 
7641
7897
  // C) Create payment intent (starts wallet timer)
7642
- const intentPromise = client
7643
- .createPaymentIntent(checkoutId, { successUrl, cancelUrl })
7898
+ // Wait for provider info so we can choose the right success URL:
7899
+ // iframe providers redirect inside the iframe to /payment-complete (postMessage),
7900
+ // redirect providers go straight to /order-confirmation.
7901
+ const intentPromise = providerPromise
7902
+ .then((providerSdk) => {
7903
+ const isIframe = providerSdk?.renderType === 'iframe';
7904
+ const successUrl = isIframe ? iframeSuccessUrl : redirectSuccessUrl;
7905
+ const failedUrl = isIframe ? iframeFailedUrl : cancelUrl;
7906
+ return client.createPaymentIntent(checkoutId, {
7907
+ successUrl,
7908
+ cancelUrl: failedUrl,
7909
+ });
7910
+ })
7644
7911
  .then((intent) => {
7645
7912
  setPaymentIntent(intent);
7646
7913
  return intent;
@@ -7665,6 +7932,36 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
7665
7932
  window.location.href = intent.clientSecret;
7666
7933
  return;
7667
7934
  }
7935
+
7936
+ // Iframe mode: listen for postMessage from the /payment-complete callback
7937
+ // page that loads inside the iframe after the provider redirects on completion.
7938
+ if (sdk.renderType === 'iframe') {
7939
+ const handleMessage = (event: MessageEvent) => {
7940
+ if (event.origin !== window.location.origin) return;
7941
+ if (event.data?.type !== 'brainerce:payment-complete') return;
7942
+
7943
+ const params = event.data.data as Record<string, string> | undefined;
7944
+ if (params?.failed === 'true') {
7945
+ setError(t('paymentError'));
7946
+ return;
7947
+ }
7948
+
7949
+ // Map provider-specific params to normalized format for
7950
+ // server-side verification (e.g. CardCom lowprofilecode \u2192 paymentIntentId)
7951
+ const lowProfileCode = params?.lowprofilecode || params?.LowProfileCode;
7952
+ const normalized: Record<string, unknown> = { ...params };
7953
+ if (lowProfileCode) {
7954
+ normalized.paymentIntentId = lowProfileCode;
7955
+ }
7956
+
7957
+ // Trigger server-side verification + order creation
7958
+ handleSuccess(normalized);
7959
+ };
7960
+ window.addEventListener('message', handleMessage);
7961
+ cleanups.push(() => window.removeEventListener('message', handleMessage));
7962
+ return;
7963
+ }
7964
+
7668
7965
  if (sdk.renderType !== 'sdk-widget' || !sdk.globalName) return;
7669
7966
 
7670
7967
  // Store for retryRender from onError callback
@@ -7815,15 +8112,45 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
7815
8112
 
7816
8113
  if (sdk.renderType === 'iframe') {
7817
8114
  return (
7818
- <div className={cn('py-4', className)}>
7819
- <iframe
7820
- src={paymentIntent.clientSecret}
7821
- className="w-full border-0"
7822
- style={{ minHeight: '500px' }}
7823
- title={t('payment')}
7824
- allow="payment"
7825
- />
7826
- </div>
8115
+ <>
8116
+ {/* Modal overlay */}
8117
+ <div className="fixed inset-0 z-50 flex items-start justify-center overflow-y-auto bg-black/50 py-6 backdrop-blur-sm">
8118
+ <div className="relative mx-4 w-full max-w-md rounded-2xl bg-white shadow-2xl">
8119
+ {/* Close button */}
8120
+ <button
8121
+ onClick={() => {
8122
+ window.location.href = \`/checkout?checkout_id=\${checkoutId}&canceled=true\`;
8123
+ }}
8124
+ 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"
8125
+ aria-label="Close"
8126
+ >
8127
+ <svg
8128
+ width="14"
8129
+ height="14"
8130
+ viewBox="0 0 14 14"
8131
+ fill="none"
8132
+ stroke="currentColor"
8133
+ strokeWidth="2"
8134
+ strokeLinecap="round"
8135
+ >
8136
+ <path d="M1 1l12 12M13 1L1 13" />
8137
+ </svg>
8138
+ </button>
8139
+ <iframe
8140
+ src={paymentIntent.clientSecret}
8141
+ className="w-full rounded-2xl border-0"
8142
+ style={{ height: '80vh' }}
8143
+ title={t('payment')}
8144
+ allow="payment"
8145
+ />
8146
+ </div>
8147
+ </div>
8148
+ {/* Placeholder so the checkout layout doesn't collapse */}
8149
+ <div className={cn('flex flex-col items-center justify-center py-12', className)}>
8150
+ <LoadingSpinner size="lg" />
8151
+ <p className="text-muted-foreground mt-4 text-sm">{t('preparingPayment')}</p>
8152
+ </div>
8153
+ </>
7827
8154
  );
7828
8155
  }
7829
8156
 
@@ -9950,10 +10277,10 @@ export function CartUpgradeBanner({
9950
10277
  `,
9951
10278
  "src/components/cart/cart-bundle-offer.tsx": `'use client';
9952
10279
 
9953
- import { useState } from 'react';
10280
+ import { useState, useMemo } from 'react';
9954
10281
  import Image from 'next/image';
9955
10282
  import type { CartBundleOffer as CartBundleOfferType } from 'brainerce';
9956
- import { formatPrice } from 'brainerce';
10283
+ import { formatPrice, getVariantOptions } from 'brainerce';
9957
10284
  import { useStoreInfo } from '@/providers/store-provider';
9958
10285
  import { useTranslations } from '@/lib/translations';
9959
10286
  import { cn } from '@/lib/utils';
@@ -9970,28 +10297,114 @@ export function CartBundleOfferCard({ offer, cartId, onAdd, className }: CartBun
9970
10297
  const t = useTranslations('cart');
9971
10298
  const currency = storeInfo?.currency || 'USD';
9972
10299
  const [adding, setAdding] = useState(false);
10300
+ const [selectedVariantId, setSelectedVariantId] = useState<string | null>(null);
9973
10301
 
9974
10302
  const product = offer.bundleProduct;
10303
+ const variants = product.variants;
10304
+ const requiresSelection = offer.requiresVariantSelection && variants && variants.length > 0;
10305
+
10306
+ // Build attribute groups from variants
10307
+ const attributeGroups = useMemo(() => {
10308
+ if (!requiresSelection || !variants) return [];
10309
+ const groups = new Map<string, Set<string>>();
10310
+ for (const v of variants) {
10311
+ const opts = getVariantOptions(v as any);
10312
+ for (const opt of opts) {
10313
+ if (!groups.has(opt.name)) groups.set(opt.name, new Set());
10314
+ groups.get(opt.name)!.add(opt.value);
10315
+ }
10316
+ }
10317
+ return Array.from(groups.entries()).map(([name, values]) => ({
10318
+ name,
10319
+ values: Array.from(values),
10320
+ }));
10321
+ }, [requiresSelection, variants]);
10322
+
10323
+ const [selectedAttrs, setSelectedAttrs] = useState<Record<string, string>>({});
10324
+
10325
+ const selectedVariant = useMemo(() => {
10326
+ if (!requiresSelection || !variants) return null;
10327
+ return (
10328
+ variants.find((v) => {
10329
+ const opts = getVariantOptions(v as any);
10330
+ return attributeGroups.every((group) => {
10331
+ const opt = opts.find((o) => o.name === group.name);
10332
+ return opt && selectedAttrs[group.name] === opt.value;
10333
+ });
10334
+ }) ?? null
10335
+ );
10336
+ }, [requiresSelection, variants, selectedAttrs, attributeGroups]);
10337
+
10338
+ const effectiveVariantId = selectedVariant?.id ?? selectedVariantId;
10339
+
10340
+ function handleAttrSelect(attrName: string, value: string) {
10341
+ const next = { ...selectedAttrs, [attrName]: value };
10342
+ setSelectedAttrs(next);
10343
+ if (variants) {
10344
+ const match = variants.find((v) => {
10345
+ const opts = getVariantOptions(v as any);
10346
+ return attributeGroups.every((group) => {
10347
+ const opt = opts.find((o) => o.name === group.name);
10348
+ return opt && next[group.name] === opt.value;
10349
+ });
10350
+ });
10351
+ setSelectedVariantId(match?.id ?? null);
10352
+ }
10353
+ }
10354
+
10355
+ // Compute display prices
10356
+ const { displayOriginal, displayDiscounted, discountLabel } = useMemo(() => {
10357
+ let effectivePrice: number;
10358
+ if (selectedVariant) {
10359
+ const vSale = selectedVariant.salePrice ? parseFloat(selectedVariant.salePrice) : null;
10360
+ const vPrice = selectedVariant.price ? parseFloat(selectedVariant.price) : null;
10361
+ effectivePrice = vSale ?? vPrice ?? parseFloat(offer.originalPrice);
10362
+ } else {
10363
+ effectivePrice = parseFloat(offer.originalPrice);
10364
+ }
10365
+
10366
+ let discounted: number;
10367
+ if (offer.discountType === 'PERCENTAGE') {
10368
+ discounted = effectivePrice * (1 - parseFloat(offer.discountValue) / 100);
10369
+ } else {
10370
+ discounted = Math.max(0, effectivePrice - parseFloat(offer.discountValue));
10371
+ }
10372
+
10373
+ const label =
10374
+ offer.discountType === 'PERCENTAGE'
10375
+ ? \`\${offer.discountValue}%\`
10376
+ : (formatPrice(parseFloat(offer.discountValue), { currency }) as string);
10377
+
10378
+ return { displayOriginal: effectivePrice, displayDiscounted: discounted, discountLabel: label };
10379
+ }, [selectedVariant, offer.originalPrice, offer.discountType, offer.discountValue, currency]);
10380
+
10381
+ const isOos =
10382
+ selectedVariant?.inventory?.trackingMode !== 'NOT_TRACKED' &&
10383
+ selectedVariant?.inventory?.available != null &&
10384
+ selectedVariant.inventory.available <= 0;
10385
+
10386
+ const lockedLabel =
10387
+ offer.lockedVariant?.name ??
10388
+ (offer.lockedVariant?.attributes
10389
+ ? Object.values(offer.lockedVariant.attributes).join(' / ')
10390
+ : null);
10391
+
9975
10392
  const firstImage = product.images?.[0];
9976
10393
  const imageUrl = firstImage
9977
10394
  ? typeof firstImage === 'string'
9978
10395
  ? firstImage
9979
10396
  : firstImage.url
9980
10397
  : null;
9981
- const originalPrice = parseFloat(offer.originalPrice);
9982
- const discountedPrice = parseFloat(offer.discountedPrice);
9983
- const discountLabel =
9984
- offer.discountType === 'PERCENTAGE'
9985
- ? \`\${offer.discountValue}%\`
9986
- : (formatPrice(parseFloat(offer.discountValue), { currency }) as string);
10398
+
10399
+ const canAdd = !requiresSelection || !!effectiveVariantId;
9987
10400
 
9988
10401
  async function handleAdd() {
9989
- if (adding) return;
10402
+ if (adding || !canAdd || isOos) return;
9990
10403
  try {
9991
10404
  setAdding(true);
9992
10405
  const { getClient } = await import('@/lib/brainerce');
9993
10406
  const client = getClient();
9994
- await client.addBundleToCart(cartId, offer.id);
10407
+ await client.addBundleToCart(cartId, offer.id, effectiveVariantId ?? undefined);
9995
10408
  onAdd();
9996
10409
  } catch (err) {
9997
10410
  console.error('Failed to add bundle item:', err);
@@ -10001,70 +10414,121 @@ export function CartBundleOfferCard({ offer, cartId, onAdd, className }: CartBun
10001
10414
  }
10002
10415
 
10003
10416
  return (
10004
- <div
10005
- className={cn(
10006
- 'bg-background border-border flex items-center gap-4 rounded-lg border p-4',
10007
- className
10008
- )}
10009
- >
10010
- {/* Product image */}
10011
- <div className="bg-muted relative h-16 w-16 flex-shrink-0 overflow-hidden rounded">
10012
- {imageUrl ? (
10013
- <Image src={imageUrl} alt={product.name} fill sizes="64px" className="object-cover" />
10014
- ) : (
10015
- <div className="text-muted-foreground flex h-full w-full items-center justify-center">
10016
- <svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
10017
- <path
10018
- strokeLinecap="round"
10019
- strokeLinejoin="round"
10020
- strokeWidth={1.5}
10021
- 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"
10022
- />
10023
- </svg>
10024
- </div>
10025
- )}
10026
- </div>
10417
+ <div className={cn('bg-background border-border rounded-lg border p-4', className)}>
10418
+ <div className="flex items-center gap-4">
10419
+ {/* Product image */}
10420
+ <div className="bg-muted relative h-16 w-16 flex-shrink-0 overflow-hidden rounded">
10421
+ {imageUrl ? (
10422
+ <Image src={imageUrl} alt={product.name} fill sizes="64px" className="object-cover" />
10423
+ ) : (
10424
+ <div className="text-muted-foreground flex h-full w-full items-center justify-center">
10425
+ <svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
10426
+ <path
10427
+ strokeLinecap="round"
10428
+ strokeLinejoin="round"
10429
+ strokeWidth={1.5}
10430
+ 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"
10431
+ />
10432
+ </svg>
10433
+ </div>
10434
+ )}
10435
+ </div>
10027
10436
 
10028
- {/* Details */}
10029
- <div className="min-w-0 flex-1">
10030
- <p className="text-foreground text-sm font-medium">{offer.name}</p>
10031
- {offer.description && (
10032
- <p className="text-muted-foreground mt-0.5 text-xs">{offer.description}</p>
10033
- )}
10034
- <div className="mt-1 flex items-center gap-2">
10035
- <span className="text-muted-foreground text-sm line-through">
10036
- {formatPrice(originalPrice, { currency }) as string}
10037
- </span>
10038
- <span className="text-foreground text-sm font-semibold">
10039
- {formatPrice(discountedPrice, { currency }) as string}
10040
- </span>
10041
- <span className="bg-destructive/10 text-destructive rounded px-1.5 py-0.5 text-xs font-medium">
10042
- -{discountLabel}
10043
- </span>
10437
+ {/* Details */}
10438
+ <div className="min-w-0 flex-1">
10439
+ <p className="text-foreground text-sm font-medium">{offer.name}</p>
10440
+ {offer.description && (
10441
+ <p className="text-muted-foreground mt-0.5 text-xs">{offer.description}</p>
10442
+ )}
10443
+ {lockedLabel && <p className="text-muted-foreground mt-0.5 text-xs">{lockedLabel}</p>}
10444
+ <div className="mt-1 flex items-center gap-2">
10445
+ <span className="text-muted-foreground text-sm line-through">
10446
+ {formatPrice(displayOriginal, { currency }) as string}
10447
+ </span>
10448
+ <span className="text-foreground text-sm font-semibold">
10449
+ {formatPrice(displayDiscounted, { currency }) as string}
10450
+ </span>
10451
+ <span className="bg-destructive/10 text-destructive rounded px-1.5 py-0.5 text-xs font-medium">
10452
+ -{discountLabel}
10453
+ </span>
10454
+ </div>
10044
10455
  </div>
10456
+
10457
+ {/* Add button */}
10458
+ <button
10459
+ type="button"
10460
+ onClick={handleAdd}
10461
+ disabled={adding || !canAdd || isOos}
10462
+ className={cn(
10463
+ 'bg-primary text-primary-foreground flex-shrink-0 rounded px-4 py-2 text-xs font-medium transition-opacity hover:opacity-90',
10464
+ 'disabled:cursor-not-allowed disabled:opacity-50'
10465
+ )}
10466
+ >
10467
+ {adding
10468
+ ? t('addingBundle')
10469
+ : requiresSelection && !canAdd
10470
+ ? t('selectOptions') || 'Select options'
10471
+ : t('addBundleItem')}
10472
+ </button>
10045
10473
  </div>
10046
10474
 
10047
- {/* Add button */}
10048
- <button
10049
- type="button"
10050
- onClick={handleAdd}
10051
- disabled={adding}
10052
- className={cn(
10053
- 'bg-primary text-primary-foreground flex-shrink-0 rounded px-4 py-2 text-xs font-medium transition-opacity hover:opacity-90',
10054
- 'disabled:cursor-not-allowed disabled:opacity-50'
10055
- )}
10056
- >
10057
- {adding ? t('addingBundle') : t('addBundleItem')}
10058
- </button>
10475
+ {/* Compact variant selector */}
10476
+ {requiresSelection && (
10477
+ <div className="mt-3 space-y-1.5 ps-20">
10478
+ {attributeGroups.map((group) => (
10479
+ <div key={group.name} className="flex flex-wrap items-center gap-1.5">
10480
+ <span className="text-muted-foreground text-xs">{group.name}:</span>
10481
+ {group.values.map((value) => {
10482
+ const isSelected = selectedAttrs[group.name] === value;
10483
+ const variantForValue = variants?.find((v) => {
10484
+ const opts = getVariantOptions(v as any);
10485
+ const matchesValue = opts.some((o) => o.name === group.name && o.value === value);
10486
+ if (!matchesValue) return false;
10487
+ return Object.entries(selectedAttrs).every(([k, sv]) => {
10488
+ if (k === group.name) return true;
10489
+ return opts.some((o) => o.name === k && o.value === sv);
10490
+ });
10491
+ });
10492
+ const isVariantOos =
10493
+ variantForValue?.inventory?.trackingMode !== 'NOT_TRACKED' &&
10494
+ variantForValue?.inventory?.available != null &&
10495
+ variantForValue.inventory.available <= 0;
10496
+
10497
+ return (
10498
+ <button
10499
+ key={value}
10500
+ type="button"
10501
+ onClick={() => handleAttrSelect(group.name, value)}
10502
+ disabled={isVariantOos}
10503
+ className={cn(
10504
+ 'rounded-full border px-2.5 py-0.5 text-xs transition-colors',
10505
+ isSelected
10506
+ ? 'border-primary bg-primary text-primary-foreground'
10507
+ : 'border-border text-foreground hover:border-primary/50',
10508
+ isVariantOos && 'cursor-not-allowed line-through opacity-40'
10509
+ )}
10510
+ >
10511
+ {value}
10512
+ </button>
10513
+ );
10514
+ })}
10515
+ </div>
10516
+ ))}
10517
+ {isOos && effectiveVariantId && (
10518
+ <p className="text-destructive text-xs">{t('outOfStock') || 'Out of stock'}</p>
10519
+ )}
10520
+ </div>
10521
+ )}
10059
10522
  </div>
10060
10523
  );
10061
10524
  }
10062
10525
  `,
10063
10526
  "src/components/checkout/order-bump-card.tsx": `'use client';
10064
10527
 
10528
+ import { useState, useMemo } from 'react';
10065
10529
  import Image from 'next/image';
10066
- import type { OrderBump } from 'brainerce';
10067
- import { formatPrice } from 'brainerce';
10530
+ import type { OrderBump, RecommendationVariant } from 'brainerce';
10531
+ import { formatPrice, getVariantOptions } from 'brainerce';
10068
10532
  import { useStoreInfo } from '@/providers/store-provider';
10069
10533
  import { useTranslations } from '@/lib/translations';
10070
10534
  import { cn } from '@/lib/utils';
@@ -10072,7 +10536,7 @@ import { cn } from '@/lib/utils';
10072
10536
  interface OrderBumpCardProps {
10073
10537
  bump: OrderBump;
10074
10538
  isAdded: boolean;
10075
- onToggle: (bumpId: string, add: boolean) => void;
10539
+ onToggle: (bumpId: string, add: boolean, variantId?: string) => void;
10076
10540
  loading: boolean;
10077
10541
  className?: string;
10078
10542
  }
@@ -10081,66 +10545,225 @@ export function OrderBumpCard({ bump, isAdded, onToggle, loading, className }: O
10081
10545
  const { storeInfo } = useStoreInfo();
10082
10546
  const t = useTranslations('checkout');
10083
10547
  const currency = storeInfo?.currency || 'USD';
10548
+ const [selectedVariantId, setSelectedVariantId] = useState<string | null>(null);
10084
10549
 
10085
10550
  const product = bump.bumpProduct;
10551
+ const variants = product.variants;
10552
+ const requiresSelection = bump.requiresVariantSelection && variants && variants.length > 0;
10553
+
10554
+ // Build attribute groups from variants for pill selector
10555
+ const attributeGroups = useMemo(() => {
10556
+ if (!requiresSelection || !variants) return [];
10557
+ const groups = new Map<string, Set<string>>();
10558
+ for (const v of variants) {
10559
+ const opts = getVariantOptions(v as any);
10560
+ for (const opt of opts) {
10561
+ if (!groups.has(opt.name)) groups.set(opt.name, new Set());
10562
+ groups.get(opt.name)!.add(opt.value);
10563
+ }
10564
+ }
10565
+ return Array.from(groups.entries()).map(([name, values]) => ({
10566
+ name,
10567
+ values: Array.from(values),
10568
+ }));
10569
+ }, [requiresSelection, variants]);
10570
+
10571
+ // Track selected attributes
10572
+ const [selectedAttrs, setSelectedAttrs] = useState<Record<string, string>>({});
10573
+
10574
+ // Find matching variant based on selected attributes
10575
+ const selectedVariant = useMemo(() => {
10576
+ if (!requiresSelection || !variants) return null;
10577
+ return (
10578
+ variants.find((v) => {
10579
+ const opts = getVariantOptions(v as any);
10580
+ return attributeGroups.every((group) => {
10581
+ const opt = opts.find((o) => o.name === group.name);
10582
+ return opt && selectedAttrs[group.name] === opt.value;
10583
+ });
10584
+ }) ?? null
10585
+ );
10586
+ }, [requiresSelection, variants, selectedAttrs, attributeGroups]);
10587
+
10588
+ // Update selectedVariantId when variant match changes
10589
+ const effectiveVariantId = selectedVariant?.id ?? selectedVariantId;
10590
+
10591
+ function handleAttrSelect(attrName: string, value: string) {
10592
+ const next = { ...selectedAttrs, [attrName]: value };
10593
+ setSelectedAttrs(next);
10594
+ // Find matching variant with new selection
10595
+ if (variants) {
10596
+ const match = variants.find((v) => {
10597
+ const opts = getVariantOptions(v as any);
10598
+ return attributeGroups.every((group) => {
10599
+ const opt = opts.find((o) => o.name === group.name);
10600
+ return opt && next[group.name] === opt.value;
10601
+ });
10602
+ });
10603
+ setSelectedVariantId(match?.id ?? null);
10604
+ }
10605
+ }
10606
+
10607
+ // Compute display price
10608
+ const { displayOriginal, displayDiscounted } = useMemo(() => {
10609
+ let effectivePrice: number;
10610
+ if (selectedVariant) {
10611
+ const vSale = selectedVariant.salePrice ? parseFloat(selectedVariant.salePrice) : null;
10612
+ const vPrice = selectedVariant.price ? parseFloat(selectedVariant.price) : null;
10613
+ effectivePrice = vSale ?? vPrice ?? parseFloat(bump.originalPrice);
10614
+ } else {
10615
+ effectivePrice = parseFloat(bump.originalPrice);
10616
+ }
10617
+
10618
+ let discounted: number | null = null;
10619
+ if (bump.discountType && bump.discountValue) {
10620
+ const dv = parseFloat(bump.discountValue);
10621
+ if (bump.discountType === 'PERCENTAGE') {
10622
+ discounted = effectivePrice * (1 - dv / 100);
10623
+ } else {
10624
+ discounted = Math.max(0, effectivePrice - dv);
10625
+ }
10626
+ }
10627
+
10628
+ return { displayOriginal: effectivePrice, displayDiscounted: discounted };
10629
+ }, [selectedVariant, bump.originalPrice, bump.discountType, bump.discountValue]);
10630
+
10631
+ // Check if selected variant is out of stock
10632
+ const isOos =
10633
+ selectedVariant?.inventory?.trackingMode !== 'NOT_TRACKED' &&
10634
+ selectedVariant?.inventory?.available != null &&
10635
+ selectedVariant.inventory.available <= 0;
10636
+
10637
+ // Locked variant label
10638
+ const lockedLabel =
10639
+ bump.lockedVariant?.name ??
10640
+ (bump.lockedVariant?.attributes
10641
+ ? Object.values(bump.lockedVariant.attributes).join(' / ')
10642
+ : null);
10643
+
10086
10644
  const firstImage = product.images?.[0];
10087
10645
  const imageUrl = firstImage
10088
10646
  ? typeof firstImage === 'string'
10089
10647
  ? firstImage
10090
10648
  : firstImage.url
10091
10649
  : null;
10092
- const originalPrice = parseFloat(bump.originalPrice);
10093
- const hasDiscount = bump.discountedPrice != null;
10094
- const discountedPrice = hasDiscount ? parseFloat(bump.discountedPrice!) : null;
10650
+
10651
+ const canToggle = !requiresSelection || !!effectiveVariantId;
10095
10652
 
10096
10653
  return (
10097
- <label
10654
+ <div
10098
10655
  className={cn(
10099
- 'border-border hover:border-primary/50 flex cursor-pointer items-start gap-3 rounded-lg border p-3 transition-colors',
10656
+ 'border-border hover:border-primary/50 rounded-lg border p-3 transition-colors',
10100
10657
  isAdded && 'border-primary bg-primary/5',
10101
10658
  loading && 'pointer-events-none opacity-60',
10102
10659
  className
10103
10660
  )}
10104
10661
  >
10105
- <input
10106
- type="checkbox"
10107
- checked={isAdded}
10108
- onChange={() => onToggle(bump.id, !isAdded)}
10109
- disabled={loading}
10110
- className="mt-1 h-4 w-4 shrink-0 rounded"
10111
- />
10112
-
10113
- {/* Image */}
10114
- {imageUrl && (
10115
- <div className="bg-muted relative h-10 w-10 shrink-0 overflow-hidden rounded">
10116
- <Image src={imageUrl} alt={product.name} fill sizes="40px" className="object-cover" />
10117
- </div>
10118
- )}
10662
+ <label className="flex cursor-pointer items-start gap-3">
10663
+ <input
10664
+ type="checkbox"
10665
+ checked={isAdded}
10666
+ onChange={() => {
10667
+ if (canToggle) {
10668
+ onToggle(bump.id, !isAdded, effectiveVariantId ?? undefined);
10669
+ }
10670
+ }}
10671
+ disabled={loading || !canToggle || isOos}
10672
+ className="mt-1 h-4 w-4 shrink-0 rounded"
10673
+ />
10119
10674
 
10120
- {/* Content */}
10121
- <div className="min-w-0 flex-1">
10122
- <p className="text-foreground text-sm font-medium">{bump.title}</p>
10123
- {bump.description && (
10124
- <p className="text-muted-foreground mt-0.5 text-xs">{bump.description}</p>
10675
+ {/* Image */}
10676
+ {imageUrl && (
10677
+ <div className="bg-muted relative h-10 w-10 shrink-0 overflow-hidden rounded">
10678
+ <Image src={imageUrl} alt={product.name} fill sizes="40px" className="object-cover" />
10679
+ </div>
10125
10680
  )}
10126
- <div className="mt-1 flex items-center gap-2">
10127
- {hasDiscount ? (
10128
- <>
10129
- <span className="text-muted-foreground text-xs line-through">
10130
- {formatPrice(originalPrice, { currency }) as string}
10131
- </span>
10681
+
10682
+ {/* Content */}
10683
+ <div className="min-w-0 flex-1">
10684
+ <p className="text-foreground text-sm font-medium">{bump.title}</p>
10685
+ {bump.description && (
10686
+ <p className="text-muted-foreground mt-0.5 text-xs">{bump.description}</p>
10687
+ )}
10688
+
10689
+ {/* Locked variant label */}
10690
+ {lockedLabel && <p className="text-muted-foreground mt-0.5 text-xs">{lockedLabel}</p>}
10691
+
10692
+ {/* Price */}
10693
+ <div className="mt-1 flex items-center gap-2">
10694
+ {displayDiscounted != null ? (
10695
+ <>
10696
+ <span className="text-muted-foreground text-xs line-through">
10697
+ {formatPrice(displayOriginal, { currency }) as string}
10698
+ </span>
10699
+ <span className="text-foreground text-sm font-semibold">
10700
+ {formatPrice(displayDiscounted, { currency }) as string}
10701
+ </span>
10702
+ </>
10703
+ ) : (
10132
10704
  <span className="text-foreground text-sm font-semibold">
10133
- {formatPrice(discountedPrice!, { currency }) as string}
10705
+ {formatPrice(displayOriginal, { currency }) as string}
10134
10706
  </span>
10135
- </>
10136
- ) : (
10137
- <span className="text-foreground text-sm font-semibold">
10138
- {formatPrice(originalPrice, { currency }) as string}
10139
- </span>
10707
+ )}
10708
+ {requiresSelection && !effectiveVariantId && (
10709
+ <span className="text-muted-foreground text-xs">
10710
+ {t('selectOptions') || 'Select options'}
10711
+ </span>
10712
+ )}
10713
+ </div>
10714
+ </div>
10715
+ </label>
10716
+
10717
+ {/* Compact variant selector */}
10718
+ {requiresSelection && !isAdded && (
10719
+ <div className="ms-7 mt-2 space-y-1.5">
10720
+ {attributeGroups.map((group) => (
10721
+ <div key={group.name} className="flex flex-wrap items-center gap-1.5">
10722
+ <span className="text-muted-foreground text-xs">{group.name}:</span>
10723
+ {group.values.map((value) => {
10724
+ const isSelected = selectedAttrs[group.name] === value;
10725
+ // Check if this value leads to any available variant
10726
+ const variantForValue = variants?.find((v) => {
10727
+ const opts = getVariantOptions(v as any);
10728
+ const matchesValue = opts.some((o) => o.name === group.name && o.value === value);
10729
+ if (!matchesValue) return false;
10730
+ // Check other selected attrs
10731
+ return Object.entries(selectedAttrs).every(([k, sv]) => {
10732
+ if (k === group.name) return true;
10733
+ return opts.some((o) => o.name === k && o.value === sv);
10734
+ });
10735
+ });
10736
+ const isVariantOos =
10737
+ variantForValue?.inventory?.trackingMode !== 'NOT_TRACKED' &&
10738
+ variantForValue?.inventory?.available != null &&
10739
+ variantForValue.inventory.available <= 0;
10740
+
10741
+ return (
10742
+ <button
10743
+ key={value}
10744
+ type="button"
10745
+ onClick={() => handleAttrSelect(group.name, value)}
10746
+ disabled={isVariantOos}
10747
+ className={cn(
10748
+ 'rounded-full border px-2.5 py-0.5 text-xs transition-colors',
10749
+ isSelected
10750
+ ? 'border-primary bg-primary text-primary-foreground'
10751
+ : 'border-border text-foreground hover:border-primary/50',
10752
+ isVariantOos && 'cursor-not-allowed line-through opacity-40'
10753
+ )}
10754
+ >
10755
+ {value}
10756
+ </button>
10757
+ );
10758
+ })}
10759
+ </div>
10760
+ ))}
10761
+ {isOos && effectiveVariantId && (
10762
+ <p className="text-destructive text-xs">{t('outOfStock') || 'Out of stock'}</p>
10140
10763
  )}
10141
10764
  </div>
10142
- </div>
10143
- </label>
10765
+ )}
10766
+ </div>
10144
10767
  );
10145
10768
  }
10146
10769
  `,
@@ -10503,7 +11126,8 @@ var DETAILED = `# Required Pages \u2014 Detailed Guide
10503
11126
  - Step 1: Customer info (email, name)
10504
11127
  - Step 2: Shipping address form
10505
11128
  - Step 3: Shipping method selection (from rates returned by \`setShippingAddress()\`)
10506
- - Step 4: Payment (auto-detects Stripe/Grow/PayPal via \`getPaymentProviders()\`)
11129
+ - Step 4: Payment (auto-detects Stripe/Grow/PayPal/Sandbox via \`getPaymentProviders()\`)
11130
+ - 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.
10507
11131
  - Order summary sidebar showing \`checkout.lineItems\` (NOT cart.items!)
10508
11132
  - Tax display after address entry
10509
11133
  - Reservation countdown
@@ -10583,46 +11207,39 @@ async function handleGetRequiredPages(args) {
10583
11207
  import { z as z5 } from "zod";
10584
11208
 
10585
11209
  // src/utils/fetch-store-info.ts
10586
- import https from "https";
10587
- import http from "http";
10588
11210
  async function fetchStoreInfo(connectionId, baseUrl = "https://api.brainerce.com") {
10589
11211
  const url = `${baseUrl}/api/vc/${connectionId}/info`;
10590
- return new Promise((resolve, reject) => {
10591
- const client = url.startsWith("https") ? https : http;
10592
- const req = client.get(url, { timeout: 1e4 }, (res) => {
10593
- let data = "";
10594
- res.on("data", (chunk) => {
10595
- data += chunk;
10596
- });
10597
- res.on("end", () => {
10598
- if (res.statusCode && res.statusCode >= 400) {
10599
- if (res.statusCode === 404) {
10600
- reject(new Error(`Connection ID "${connectionId}" not found. Check your dashboard.`));
10601
- } else {
10602
- reject(new Error(`API returned status ${res.statusCode}`));
10603
- }
10604
- return;
10605
- }
10606
- try {
10607
- const json = JSON.parse(data);
10608
- resolve({
10609
- name: json.name || json.storeName || "My Store",
10610
- currency: json.currency || "USD",
10611
- language: json.language || "en"
10612
- });
10613
- } catch {
10614
- reject(new Error("Invalid response from API"));
10615
- }
10616
- });
10617
- });
10618
- req.on("error", (err) => {
10619
- reject(new Error(`Failed to connect to Brainerce API: ${err.message}`));
10620
- });
10621
- req.on("timeout", () => {
10622
- req.destroy();
10623
- reject(new Error("Request timed out"));
10624
- });
10625
- });
11212
+ const controller = new AbortController();
11213
+ const timeout = setTimeout(() => controller.abort(), 1e4);
11214
+ let res;
11215
+ try {
11216
+ res = await fetch(url, { signal: controller.signal });
11217
+ } catch (err) {
11218
+ if (err.name === "AbortError") {
11219
+ throw new Error("Request timed out");
11220
+ }
11221
+ throw new Error(`Failed to connect to Brainerce API: ${err.message}`);
11222
+ } finally {
11223
+ clearTimeout(timeout);
11224
+ }
11225
+ if (res.status === 404) {
11226
+ throw new Error(`Connection ID "${connectionId}" not found. Check your dashboard.`);
11227
+ }
11228
+ if (!res.ok) {
11229
+ throw new Error(`API returned status ${res.status}`);
11230
+ }
11231
+ let json;
11232
+ try {
11233
+ json = await res.json();
11234
+ } catch {
11235
+ throw new Error("Invalid response from API");
11236
+ }
11237
+ return {
11238
+ name: json.name || json.storeName || "My Store",
11239
+ currency: json.currency || "USD",
11240
+ language: json.language || "en",
11241
+ ...json.i18n ? { i18n: json.i18n } : {}
11242
+ };
10626
11243
  }
10627
11244
 
10628
11245
  // src/tools/get-store-info.ts
@@ -10633,7 +11250,11 @@ var GET_STORE_INFO_SCHEMA = {
10633
11250
  };
10634
11251
  async function handleGetStoreInfo(args) {
10635
11252
  try {
10636
- const info = await fetchStoreInfo(args.connectionId);
11253
+ const baseUrl = (process.env.BRAINERCE_API_URL || "https://api.brainerce.com").replace(
11254
+ /\/$/,
11255
+ ""
11256
+ );
11257
+ const info = await fetchStoreInfo(args.connectionId, baseUrl);
10637
11258
  return {
10638
11259
  content: [
10639
11260
  {
@@ -10664,41 +11285,32 @@ async function handleGetStoreInfo(args) {
10664
11285
  import { z as z6 } from "zod";
10665
11286
 
10666
11287
  // src/utils/fetch-store-capabilities.ts
10667
- import https2 from "https";
10668
- import http2 from "http";
10669
11288
  async function fetchStoreCapabilities(connectionId, baseUrl = "https://api.brainerce.com") {
10670
11289
  const url = `${baseUrl}/api/vc/${connectionId}/capabilities`;
10671
- return new Promise((resolve, reject) => {
10672
- const client = url.startsWith("https") ? https2 : http2;
10673
- const req = client.get(url, { timeout: 1e4 }, (res) => {
10674
- let data = "";
10675
- res.on("data", (chunk) => {
10676
- data += chunk;
10677
- });
10678
- res.on("end", () => {
10679
- if (res.statusCode && res.statusCode >= 400) {
10680
- if (res.statusCode === 404) {
10681
- reject(new Error(`Connection ID "${connectionId}" not found. Check your dashboard.`));
10682
- } else {
10683
- reject(new Error(`API returned status ${res.statusCode}`));
10684
- }
10685
- return;
10686
- }
10687
- try {
10688
- resolve(JSON.parse(data));
10689
- } catch {
10690
- reject(new Error("Invalid response from API"));
10691
- }
10692
- });
10693
- });
10694
- req.on("error", (err) => {
10695
- reject(new Error(`Failed to connect to Brainerce API: ${err.message}`));
10696
- });
10697
- req.on("timeout", () => {
10698
- req.destroy();
10699
- reject(new Error("Request timed out"));
10700
- });
10701
- });
11290
+ const controller = new AbortController();
11291
+ const timeout = setTimeout(() => controller.abort(), 1e4);
11292
+ let res;
11293
+ try {
11294
+ res = await fetch(url, { signal: controller.signal });
11295
+ } catch (err) {
11296
+ if (err.name === "AbortError") {
11297
+ throw new Error("Request timed out");
11298
+ }
11299
+ throw new Error(`Failed to connect to Brainerce API: ${err.message}`);
11300
+ } finally {
11301
+ clearTimeout(timeout);
11302
+ }
11303
+ if (res.status === 404) {
11304
+ throw new Error(`Connection ID "${connectionId}" not found. Check your dashboard.`);
11305
+ }
11306
+ if (!res.ok) {
11307
+ throw new Error(`API returned status ${res.status}`);
11308
+ }
11309
+ try {
11310
+ return await res.json();
11311
+ } catch {
11312
+ throw new Error("Invalid response from API");
11313
+ }
10702
11314
  }
10703
11315
 
10704
11316
  // src/tools/get-store-capabilities.ts
@@ -10711,6 +11323,11 @@ function formatCapabilities(caps) {
10711
11323
  const lines = [];
10712
11324
  lines.push(`## Store: ${caps.store.name} (${caps.store.currency})`);
10713
11325
  lines.push(`Language: ${caps.store.language}`);
11326
+ if (caps.store.i18n?.enabled) {
11327
+ lines.push(
11328
+ `Multi-language: ENABLED \u2014 locales: ${caps.store.i18n.supportedLocales.join(", ")} (default: ${caps.store.i18n.defaultLocale})`
11329
+ );
11330
+ }
10714
11331
  lines.push("");
10715
11332
  lines.push("## Configured Features");
10716
11333
  if (caps.features.paymentProviders.length > 0) {
@@ -10745,6 +11362,9 @@ function formatCapabilities(caps) {
10745
11362
  lines.push(
10746
11363
  `- Email verification: ${caps.connection.requireEmailVerification ? "required" : "not required"}`
10747
11364
  );
11365
+ lines.push(
11366
+ `- Sandbox payments: ${caps.connection.sandboxPaymentsEnabled ? "enabled (test orders without real payment)" : "disabled"}`
11367
+ );
10748
11368
  lines.push(
10749
11369
  `- Inventory reservation: ${caps.connection.reservationStrategy} (${caps.connection.reservationTimeout} min timeout)`
10750
11370
  );
@@ -10784,11 +11404,21 @@ function formatCapabilities(caps) {
10784
11404
  'Email verification is required \u2014 ALWAYS build the verify-email page. Use get-code-example("verify-email").'
10785
11405
  );
10786
11406
  }
11407
+ if (caps.connection.sandboxPaymentsEnabled) {
11408
+ suggestions.push(
11409
+ `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.`
11410
+ );
11411
+ }
10787
11412
  if (caps.connection.reservationStrategy !== "ON_PAYMENT") {
10788
11413
  suggestions.push(
10789
11414
  'Inventory reservation is active \u2014 show a countdown timer. Use get-code-example("reservation-countdown").'
10790
11415
  );
10791
11416
  }
11417
+ if (caps.store.i18n?.enabled && caps.store.i18n.supportedLocales.length > 1) {
11418
+ suggestions.push(
11419
+ `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.`
11420
+ );
11421
+ }
10792
11422
  if (suggestions.length === 0) {
10793
11423
  suggestions.push(
10794
11424
  "Start by building the required pages. Use get-required-pages() for the checklist."
@@ -10799,7 +11429,11 @@ function formatCapabilities(caps) {
10799
11429
  }
10800
11430
  async function handleGetStoreCapabilities(args) {
10801
11431
  try {
10802
- const caps = await fetchStoreCapabilities(args.connectionId);
11432
+ const baseUrl = (process.env.BRAINERCE_API_URL || "https://api.brainerce.com").replace(
11433
+ /\/$/,
11434
+ ""
11435
+ );
11436
+ const caps = await fetchStoreCapabilities(args.connectionId, baseUrl);
10803
11437
  return {
10804
11438
  content: [{ type: "text", text: formatCapabilities(caps) }]
10805
11439
  };
@@ -10896,6 +11530,20 @@ function formatCapabilitiesSummary(caps) {
10896
11530
  const lines = [];
10897
11531
  lines.push(`## Store: ${caps.store.name}`);
10898
11532
  lines.push(`Currency: ${caps.store.currency} | Language: ${caps.store.language}`);
11533
+ if (caps.store.i18n?.enabled && caps.store.i18n.supportedLocales.length > 1) {
11534
+ lines.push(
11535
+ `Multi-language: ${caps.store.i18n.supportedLocales.join(", ")} (default: ${caps.store.i18n.defaultLocale})`
11536
+ );
11537
+ lines.push("");
11538
+ lines.push("### i18n Implementation");
11539
+ lines.push(
11540
+ "- Call `client.setLocale(locale)` at app init based on URL prefix or user preference"
11541
+ );
11542
+ lines.push("- All getProducts/getCategories/getBrands calls auto-include the locale");
11543
+ lines.push("- Use locale-prefixed routes: `/[locale]/products`, `/[locale]/products/[slug]`");
11544
+ lines.push("- Add a language switcher component in the header");
11545
+ lines.push('- RTL locales (he, ar): set `<html dir="rtl">` \u2014 flexbox reversal is automatic');
11546
+ }
10899
11547
  lines.push("");
10900
11548
  lines.push("### Configured Features");
10901
11549
  if (caps.features.paymentProviders.length > 0) {
@@ -10937,6 +11585,9 @@ function formatCapabilitiesSummary(caps) {
10937
11585
  lines.push(
10938
11586
  `- Guest checkout tracking: ${caps.connection.guestCheckoutTracking ? "enabled" : "disabled"}`
10939
11587
  );
11588
+ lines.push(
11589
+ `- Sandbox payments: ${caps.connection.sandboxPaymentsEnabled ? "ENABLED \u2014 payment step shows test order button (no real charges)" : "disabled"}`
11590
+ );
10940
11591
  return lines.join("\n");
10941
11592
  }
10942
11593
  function buildStoreBundle(options) {
@@ -10946,8 +11597,7 @@ function buildStoreBundle(options) {
10946
11597
  connectionId,
10947
11598
  storeName,
10948
11599
  currency,
10949
- language: capabilities?.store.language || "en",
10950
- apiUrl: "https://api.brainerce.com"
11600
+ language: capabilities?.store.language || "en"
10951
11601
  };
10952
11602
  const sections = [];
10953
11603
  sections.push(`# Build: ${storeName} \u2014 ${storeType}${styleDesc}
@@ -11048,9 +11698,13 @@ var BUILD_STORE_SCHEMA = {
11048
11698
  storeStyle: z7.string().optional().describe('Design style (e.g., "minimalist", "luxury", "playful", "dark mode")')
11049
11699
  };
11050
11700
  async function handleBuildStore(args) {
11701
+ const baseUrl = (process.env.BRAINERCE_API_URL || "https://api.brainerce.com").replace(
11702
+ /\/$/,
11703
+ ""
11704
+ );
11051
11705
  let capabilities = null;
11052
11706
  try {
11053
- capabilities = await fetchStoreCapabilities(args.connectionId);
11707
+ capabilities = await fetchStoreCapabilities(args.connectionId, baseUrl);
11054
11708
  } catch {
11055
11709
  }
11056
11710
  const bundle = buildStoreBundle({
@@ -11389,6 +12043,57 @@ ${results.join("\n\n---\n\n")}`
11389
12043
  };
11390
12044
  }
11391
12045
 
12046
+ // src/tools/get-integration-guide.ts
12047
+ import { z as z10 } from "zod";
12048
+ var GET_INTEGRATION_GUIDE_NAME = "get-integration-guide";
12049
+ 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.';
12050
+ var GET_INTEGRATION_GUIDE_SCHEMA = {
12051
+ part: z10.enum(["core", "optional", "rules"]).describe(
12052
+ '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.'
12053
+ )
12054
+ };
12055
+ var GUIDE_URLS = {
12056
+ core: "https://brainerce.com/docs/integration/raw?part=core",
12057
+ optional: "https://brainerce.com/docs/integration/raw?part=optional",
12058
+ rules: "https://brainerce.com/docs/integration/raw?part=rules"
12059
+ };
12060
+ async function handleGetIntegrationGuide(args) {
12061
+ const url = GUIDE_URLS[args.part];
12062
+ if (!url) {
12063
+ return {
12064
+ content: [
12065
+ {
12066
+ type: "text",
12067
+ text: `Unknown part: "${args.part}". Available parts: core, optional, rules.`
12068
+ }
12069
+ ]
12070
+ };
12071
+ }
12072
+ try {
12073
+ const response = await fetch(url, {
12074
+ headers: { Accept: "text/plain" },
12075
+ signal: AbortSignal.timeout(15e3)
12076
+ });
12077
+ if (!response.ok) {
12078
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
12079
+ }
12080
+ const content = await response.text();
12081
+ return {
12082
+ content: [{ type: "text", text: content }]
12083
+ };
12084
+ } catch (error) {
12085
+ const message = error instanceof Error ? error.message : "Unknown error";
12086
+ return {
12087
+ content: [
12088
+ {
12089
+ type: "text",
12090
+ text: `Failed to fetch integration guide (${args.part}): ${message}. You can read it directly at: ${url}`
12091
+ }
12092
+ ]
12093
+ };
12094
+ }
12095
+ }
12096
+
11392
12097
  // src/resources/sdk-types.ts
11393
12098
  var SDK_TYPES_URI = "brainerce://sdk/types";
11394
12099
  var SDK_TYPES_NAME = "Brainerce SDK Types";
@@ -11543,15 +12248,15 @@ async function handleProjectTemplate(uri) {
11543
12248
  }
11544
12249
 
11545
12250
  // src/prompts/create-store.ts
11546
- import { z as z10 } from "zod";
12251
+ import { z as z11 } from "zod";
11547
12252
  var CREATE_STORE_NAME = "create-store";
11548
12253
  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.";
11549
12254
  var CREATE_STORE_SCHEMA = {
11550
- connectionId: z10.string().describe("Vibe-coded connection ID (starts with vc_)"),
11551
- storeName: z10.string().describe('Name of the store (e.g., "Urban Threads")'),
11552
- storeType: z10.string().describe('Type of store (e.g., "Clothing store", "Electronics shop", "Bakery")'),
11553
- currency: z10.string().optional().default("USD").describe("Store currency code (e.g., USD, ILS, EUR)"),
11554
- storeStyle: z10.string().optional().describe('Design style (e.g., "minimalist", "luxury", "playful", "dark mode")')
12255
+ connectionId: z11.string().describe("Vibe-coded connection ID (starts with vc_)"),
12256
+ storeName: z11.string().describe('Name of the store (e.g., "Urban Threads")'),
12257
+ storeType: z11.string().describe('Type of store (e.g., "Clothing store", "Electronics shop", "Bakery")'),
12258
+ currency: z11.string().optional().default("USD").describe("Store currency code (e.g., USD, ILS, EUR)"),
12259
+ storeStyle: z11.string().optional().describe('Design style (e.g., "minimalist", "luxury", "playful", "dark mode")')
11555
12260
  };
11556
12261
  function handleCreateStore(args) {
11557
12262
  const styleDesc = args.storeStyle ? ` with a ${args.storeStyle} design style` : "";
@@ -11590,6 +12295,13 @@ DO NOT STOP until all 13 pages + header are implemented:
11590
12295
  13. \`/account\` \u2014 Account dashboard
11591
12296
  14. Header component with cart count + search
11592
12297
 
12298
+ ## Multi-Language
12299
+ If the store has multi-language enabled (check build-store output), implement:
12300
+ - Locale-prefixed routes: /[locale]/products instead of /products
12301
+ - Call client.setLocale(locale) based on URL prefix
12302
+ - Language switcher in header
12303
+ - dir="rtl" on <html> for RTL locales (he, ar)
12304
+
11593
12305
  ## Step 3: Validate
11594
12306
  After building all pages, call \`validate-store\` with the list of files you created to verify nothing is missing.
11595
12307
 
@@ -11608,11 +12320,11 @@ After building all pages, call \`validate-store\` with the list of files you cre
11608
12320
  }
11609
12321
 
11610
12322
  // src/prompts/add-feature.ts
11611
- import { z as z11 } from "zod";
12323
+ import { z as z12 } from "zod";
11612
12324
  var ADD_FEATURE_NAME = "add-feature";
11613
12325
  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.";
11614
12326
  var ADD_FEATURE_SCHEMA = {
11615
- feature: z11.enum([
12327
+ feature: z12.enum([
11616
12328
  "products",
11617
12329
  "cart",
11618
12330
  "checkout",
@@ -11625,8 +12337,8 @@ var ADD_FEATURE_SCHEMA = {
11625
12337
  "search",
11626
12338
  "tax"
11627
12339
  ]).describe("The feature to add"),
11628
- connectionId: z11.string().optional().describe("Vibe-coded connection ID (starts with vc_)"),
11629
- currency: z11.string().optional().default("USD").describe("Store currency code")
12340
+ connectionId: z12.string().optional().describe("Vibe-coded connection ID (starts with vc_)"),
12341
+ currency: z12.string().optional().default("USD").describe("Store currency code")
11630
12342
  };
11631
12343
  var FEATURE_TO_TOPICS = {
11632
12344
  products: { topics: ["products"], domains: ["products"] },
@@ -11741,6 +12453,12 @@ function createServer() {
11741
12453
  GET_PAGE_CODE_SCHEMA,
11742
12454
  handleGetPageCode
11743
12455
  );
12456
+ server.tool(
12457
+ GET_INTEGRATION_GUIDE_NAME,
12458
+ GET_INTEGRATION_GUIDE_DESCRIPTION,
12459
+ GET_INTEGRATION_GUIDE_SCHEMA,
12460
+ handleGetIntegrationGuide
12461
+ );
11744
12462
  server.resource(
11745
12463
  SDK_TYPES_NAME,
11746
12464
  SDK_TYPES_URI,