@brainerce/mcp-server 2.3.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/http.js +929 -233
- package/dist/bin/stdio.js +929 -233
- package/dist/index.d.mts +11 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +929 -221
- package/dist/index.mjs +929 -211
- package/package.json +53 -53
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":
|
|
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":
|
|
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
|
|
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
|
-
|
|
7643
|
-
|
|
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
|
-
|
|
7819
|
-
|
|
7820
|
-
|
|
7821
|
-
className="w-full
|
|
7822
|
-
|
|
7823
|
-
|
|
7824
|
-
|
|
7825
|
-
|
|
7826
|
-
|
|
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
|
-
|
|
9982
|
-
const
|
|
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=
|
|
10006
|
-
|
|
10007
|
-
className
|
|
10008
|
-
|
|
10009
|
-
|
|
10010
|
-
|
|
10011
|
-
|
|
10012
|
-
|
|
10013
|
-
|
|
10014
|
-
|
|
10015
|
-
|
|
10016
|
-
|
|
10017
|
-
|
|
10018
|
-
|
|
10019
|
-
|
|
10020
|
-
|
|
10021
|
-
|
|
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
|
-
|
|
10029
|
-
|
|
10030
|
-
|
|
10031
|
-
|
|
10032
|
-
|
|
10033
|
-
|
|
10034
|
-
|
|
10035
|
-
<
|
|
10036
|
-
|
|
10037
|
-
|
|
10038
|
-
|
|
10039
|
-
|
|
10040
|
-
|
|
10041
|
-
|
|
10042
|
-
-
|
|
10043
|
-
|
|
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
|
-
{/*
|
|
10048
|
-
|
|
10049
|
-
|
|
10050
|
-
|
|
10051
|
-
|
|
10052
|
-
|
|
10053
|
-
|
|
10054
|
-
|
|
10055
|
-
|
|
10056
|
-
|
|
10057
|
-
|
|
10058
|
-
|
|
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
|
-
|
|
10093
|
-
const
|
|
10094
|
-
const discountedPrice = hasDiscount ? parseFloat(bump.discountedPrice!) : null;
|
|
10650
|
+
|
|
10651
|
+
const canToggle = !requiresSelection || !!effectiveVariantId;
|
|
10095
10652
|
|
|
10096
10653
|
return (
|
|
10097
|
-
<
|
|
10654
|
+
<div
|
|
10098
10655
|
className={cn(
|
|
10099
|
-
'border-border hover:border-primary/50
|
|
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
|
-
<
|
|
10106
|
-
|
|
10107
|
-
|
|
10108
|
-
|
|
10109
|
-
|
|
10110
|
-
|
|
10111
|
-
|
|
10112
|
-
|
|
10113
|
-
|
|
10114
|
-
|
|
10115
|
-
|
|
10116
|
-
|
|
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
|
-
|
|
10121
|
-
|
|
10122
|
-
|
|
10123
|
-
|
|
10124
|
-
|
|
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
|
-
|
|
10127
|
-
|
|
10128
|
-
|
|
10129
|
-
|
|
10130
|
-
|
|
10131
|
-
|
|
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(
|
|
10705
|
+
{formatPrice(displayOriginal, { currency }) as string}
|
|
10134
10706
|
</span>
|
|
10135
|
-
|
|
10136
|
-
|
|
10137
|
-
|
|
10138
|
-
|
|
10139
|
-
|
|
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
|
-
|
|
10143
|
-
</
|
|
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
|
-
|
|
10591
|
-
|
|
10592
|
-
|
|
10593
|
-
|
|
10594
|
-
|
|
10595
|
-
|
|
10596
|
-
|
|
10597
|
-
|
|
10598
|
-
|
|
10599
|
-
|
|
10600
|
-
|
|
10601
|
-
|
|
10602
|
-
|
|
10603
|
-
|
|
10604
|
-
|
|
10605
|
-
|
|
10606
|
-
|
|
10607
|
-
|
|
10608
|
-
|
|
10609
|
-
|
|
10610
|
-
|
|
10611
|
-
|
|
10612
|
-
|
|
10613
|
-
|
|
10614
|
-
|
|
10615
|
-
|
|
10616
|
-
|
|
10617
|
-
|
|
10618
|
-
|
|
10619
|
-
|
|
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
|
|
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
|
-
|
|
10672
|
-
|
|
10673
|
-
|
|
10674
|
-
|
|
10675
|
-
|
|
10676
|
-
|
|
10677
|
-
|
|
10678
|
-
|
|
10679
|
-
|
|
10680
|
-
|
|
10681
|
-
|
|
10682
|
-
|
|
10683
|
-
|
|
10684
|
-
|
|
10685
|
-
|
|
10686
|
-
|
|
10687
|
-
|
|
10688
|
-
|
|
10689
|
-
|
|
10690
|
-
|
|
10691
|
-
|
|
10692
|
-
|
|
10693
|
-
|
|
10694
|
-
|
|
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
|
|
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
|
|
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:
|
|
11551
|
-
storeName:
|
|
11552
|
-
storeType:
|
|
11553
|
-
currency:
|
|
11554
|
-
storeStyle:
|
|
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
|
|
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:
|
|
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:
|
|
11629
|
-
currency:
|
|
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,
|