@brainerce/mcp-server 2.4.0 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/http.js +970 -236
- package/dist/bin/stdio.js +970 -236
- package/dist/index.d.mts +10 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +970 -224
- package/dist/index.mjs +970 -214
- package/package.json +3 -2
package/dist/bin/stdio.js
CHANGED
|
@@ -1,27 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
-
var __create = Object.create;
|
|
4
|
-
var __defProp = Object.defineProperty;
|
|
5
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
-
var __copyProps = (to, from, except, desc) => {
|
|
10
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
-
for (let key of __getOwnPropNames(from))
|
|
12
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
-
}
|
|
15
|
-
return to;
|
|
16
|
-
};
|
|
17
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
-
mod
|
|
24
|
-
));
|
|
25
3
|
|
|
26
4
|
// src/bin/stdio.ts
|
|
27
5
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
@@ -180,7 +158,7 @@ import {
|
|
|
180
158
|
getVariantPrice, getVariantOptions, getStockStatus,
|
|
181
159
|
getCartItemName, getCartItemImage,
|
|
182
160
|
getDescriptionContent, isHtmlDescription,
|
|
183
|
-
getProductMetafieldValue,
|
|
161
|
+
getProductMetafieldValue, getProductCustomizationFields,
|
|
184
162
|
} from 'brainerce';
|
|
185
163
|
\`\`\``;
|
|
186
164
|
}
|
|
@@ -222,6 +200,7 @@ async function startCheckout() {
|
|
|
222
200
|
4. Start checkout session (\`startGuestCheckout()\` or \`createCheckout()\`)
|
|
223
201
|
5. Set shipping address (includes required email) \u2192 \`setShippingAddress()\`
|
|
224
202
|
6. Select shipping method \u2192 \`selectShippingMethod()\`
|
|
203
|
+
6b. (Optional) Set checkout custom fields \u2192 \`setCheckoutCustomFields()\` \u2014 surcharges auto-calculated
|
|
225
204
|
7. Create payment intent \u2192 \`createPaymentIntent()\` \u2192 returns \`{ clientSecret, provider }\`
|
|
226
205
|
8. Branch on \`provider\`: \`'stripe'\` \u2192 Stripe Elements, \`'grow'\` \u2192 iframe, \`'paypal'\` \u2192 PayPal Buttons
|
|
227
206
|
9. **Order is created AUTOMATICALLY after payment succeeds (via webhook) \u2014 for ALL providers!**
|
|
@@ -604,6 +583,35 @@ const material = getProductMetafieldValue(product, 'material');
|
|
|
604
583
|
|
|
605
584
|
**Metafield fields:** \`definitionName\` (display label), \`definitionKey\` (lookup key), \`value\`, \`type\` (IMAGE, GALLERY, URL, COLOR, BOOLEAN, DATE, DATETIME, TEXT, TEXTAREA, NUMBER, DIMENSION, WEIGHT, JSON)
|
|
606
585
|
|
|
586
|
+
### Product Customization Fields (Customer Input)
|
|
587
|
+
|
|
588
|
+
Some products allow customers to provide input at purchase time (e.g., text on a cake, upload a logo). Check \`product.customizationFields\` and render input fields accordingly.
|
|
589
|
+
|
|
590
|
+
\`\`\`typescript
|
|
591
|
+
import { getProductCustomizationFields } from 'brainerce';
|
|
592
|
+
import type { ProductCustomizationField } from 'brainerce';
|
|
593
|
+
|
|
594
|
+
const fields = getProductCustomizationFields(product);
|
|
595
|
+
|
|
596
|
+
// Render input for each field based on field.type:
|
|
597
|
+
// TEXT/TEXTAREA \u2192 text input, NUMBER \u2192 number input, BOOLEAN \u2192 checkbox,
|
|
598
|
+
// COLOR \u2192 color picker, DATE \u2192 date picker, IMAGE \u2192 file upload, etc.
|
|
599
|
+
// Check field.required, field.minLength, field.maxLength, field.enumValues for validation.
|
|
600
|
+
|
|
601
|
+
// Pass customer values in metadata when adding to cart:
|
|
602
|
+
await client.addToCart(cartId, {
|
|
603
|
+
productId: product.id,
|
|
604
|
+
quantity: 1,
|
|
605
|
+
metadata: { cake_text: 'Happy Birthday!' },
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
// For IMAGE fields, upload first:
|
|
609
|
+
const { url } = await client.uploadCustomizationFile(file);
|
|
610
|
+
// Then use the URL in metadata: metadata: { logo: url }
|
|
611
|
+
\`\`\`
|
|
612
|
+
|
|
613
|
+
**Customization field properties:** \`key\`, \`name\` (display label), \`type\`, \`required\`, \`minLength\`, \`maxLength\`, \`minValue\`, \`maxValue\`, \`enumValues\`, \`defaultValue\`, \`position\`
|
|
614
|
+
|
|
607
615
|
### Downloadable / Digital Products
|
|
608
616
|
|
|
609
617
|
Products with \`isDownloadable: true\` are digital products. Show a digital badge instead of stock badge, list included files, and note that download links appear after purchase.
|
|
@@ -790,9 +798,14 @@ const { bundles } = await client.getCartBundles(cartId);
|
|
|
790
798
|
// bundles[].bundleProduct \u2014 the product to offer
|
|
791
799
|
// bundles[].originalPrice / bundles[].discountedPrice \u2014 prices
|
|
792
800
|
// bundles[].discountType ('PERCENTAGE' | 'FIXED_AMOUNT') + discountValue
|
|
801
|
+
// bundles[].requiresVariantSelection \u2014 true if customer must pick a variant
|
|
802
|
+
// bundles[].lockedVariant \u2014 set if admin pre-selected a specific variant
|
|
803
|
+
// bundles[].bundleProduct.variants \u2014 available variants (only when requiresVariantSelection)
|
|
793
804
|
|
|
794
805
|
// Add a bundle to cart (applies the discount automatically):
|
|
795
806
|
await client.addBundleToCart(cartId, bundleOfferId);
|
|
807
|
+
// For products with variants \u2014 pass the selected variantId:
|
|
808
|
+
await client.addBundleToCart(cartId, bundleOfferId, selectedVariantId);
|
|
796
809
|
// Remove a bundle from cart:
|
|
797
810
|
await client.removeBundleFromCart(cartId, bundleOfferId);
|
|
798
811
|
// Detect already-added bundles: check cart.items for metadata?.isBundleItem === true
|
|
@@ -817,9 +830,14 @@ const { bumps } = await client.getCheckoutBumps(checkoutId);
|
|
|
817
830
|
// bumps[].bumpProduct \u2014 product to offer
|
|
818
831
|
// bumps[].originalPrice / discountedPrice \u2014 with optional discount
|
|
819
832
|
// bumps[].title / description \u2014 display text
|
|
833
|
+
// bumps[].requiresVariantSelection \u2014 true if customer must pick a variant
|
|
834
|
+
// bumps[].lockedVariant \u2014 set if admin pre-selected a specific variant
|
|
835
|
+
// bumps[].bumpProduct.variants \u2014 available variants (only when requiresVariantSelection)
|
|
820
836
|
|
|
821
837
|
// Add a bump to cart:
|
|
822
838
|
await client.addOrderBump(cartId, bumpId);
|
|
839
|
+
// For products with variants \u2014 pass the selected variantId:
|
|
840
|
+
await client.addOrderBump(cartId, bumpId, selectedVariantId);
|
|
823
841
|
// Remove a bump:
|
|
824
842
|
await client.removeOrderBump(cartId, bumpId);
|
|
825
843
|
// Detect already-added bumps: check cart.items for metadata?.isOrderBump === true
|
|
@@ -844,10 +862,11 @@ Always check the flag before rendering: \`if (storeInfo?.upsell?.featureName !==
|
|
|
844
862
|
| CartRecommendationSection | products/ | Cross-sell grid at bottom of cart page |
|
|
845
863
|
| FreeShippingBar | cart/ | Progress bar toward free shipping threshold |
|
|
846
864
|
| CartUpgradeBanner | cart/ | Inline "Upgrade to X for +$Y" per cart item |
|
|
847
|
-
| CartBundleOfferCard | cart/ | Discounted bundle offer card in cart |
|
|
848
|
-
| OrderBumpCard | checkout/ | Checkbox add-on card in checkout sidebar |
|
|
865
|
+
| CartBundleOfferCard | cart/ | Discounted bundle offer card in cart (with inline variant selector for variable products) |
|
|
866
|
+
| OrderBumpCard | checkout/ | Checkbox add-on card in checkout sidebar (with inline variant selector for variable products) |
|
|
849
867
|
|
|
850
|
-
ProductRecommendation: \`id\`, \`name\`, \`slug\`, \`basePrice\`, \`salePrice\`, \`images\`, \`type\`, \`inventory\`, \`relationType
|
|
868
|
+
ProductRecommendation: \`id\`, \`name\`, \`slug\`, \`basePrice\`, \`salePrice\`, \`images\`, \`type\`, \`inventory\`, \`relationType\`, \`variants?\` (when variant selection needed).
|
|
869
|
+
OrderBump/CartBundleOffer: includes \`requiresVariantSelection\` (boolean) and \`lockedVariant?\` (\`{ id, name, attributes }\`).`;
|
|
851
870
|
}
|
|
852
871
|
function getInventorySection() {
|
|
853
872
|
return `## Inventory, Stock Display & Reservation Countdown
|
|
@@ -1987,6 +2006,15 @@ export function getClient(): BrainerceClient {
|
|
|
1987
2006
|
}
|
|
1988
2007
|
return clientInstance;
|
|
1989
2008
|
}
|
|
2009
|
+
<% if (i18nEnabled) { %>
|
|
2010
|
+
|
|
2011
|
+
/** Initialize client with a specific locale for translated content */
|
|
2012
|
+
export function initClientWithLocale(locale: string): BrainerceClient {
|
|
2013
|
+
const client = getClient();
|
|
2014
|
+
client.setLocale(locale);
|
|
2015
|
+
return client;
|
|
2016
|
+
}
|
|
2017
|
+
<% } %>
|
|
1990
2018
|
|
|
1991
2019
|
// Cart ID helpers (not a security token \u2014 safe in localStorage)
|
|
1992
2020
|
const CART_ID_KEY = 'brainerce_cart_id';
|
|
@@ -2185,6 +2213,10 @@ import type { StoreInfo, Cart, CustomerProfile } from 'brainerce';
|
|
|
2185
2213
|
import { getCartTotals } from 'brainerce';
|
|
2186
2214
|
import { getClient, initClient, setStoredCartId } from '@/lib/brainerce';
|
|
2187
2215
|
import { checkAuthStatus, proxyLogout } from '@/lib/auth';
|
|
2216
|
+
<% if (i18nEnabled) { %>
|
|
2217
|
+
import { MessagesContext } from '@/lib/translations';
|
|
2218
|
+
import { getMessages, defaultLocale } from '@/i18n';
|
|
2219
|
+
<% } %>
|
|
2188
2220
|
|
|
2189
2221
|
// ---- Store Info Context ----
|
|
2190
2222
|
interface StoreInfoContextValue {
|
|
@@ -2244,7 +2276,11 @@ export function useCart() {
|
|
|
2244
2276
|
}
|
|
2245
2277
|
|
|
2246
2278
|
// ---- Provider Component ----
|
|
2279
|
+
<% if (i18nEnabled) { %>
|
|
2280
|
+
export function StoreProvider({ children, locale }: { children: React.ReactNode; locale?: string }) {
|
|
2281
|
+
<% } else { %>
|
|
2247
2282
|
export function StoreProvider({ children }: { children: React.ReactNode }) {
|
|
2283
|
+
<% } %>
|
|
2248
2284
|
const [storeInfo, setStoreInfo] = useState<StoreInfo | null>(null);
|
|
2249
2285
|
const [storeLoading, setStoreLoading] = useState(true);
|
|
2250
2286
|
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
|
@@ -2252,6 +2288,9 @@ export function StoreProvider({ children }: { children: React.ReactNode }) {
|
|
|
2252
2288
|
const [authLoading, setAuthLoading] = useState(true);
|
|
2253
2289
|
const [cart, setCart] = useState<Cart | null>(null);
|
|
2254
2290
|
const [cartLoading, setCartLoading] = useState(true);
|
|
2291
|
+
<% if (i18nEnabled) { %>
|
|
2292
|
+
const [messages, setMessages] = useState<Record<string, Record<string, string>>>({});
|
|
2293
|
+
<% } %>
|
|
2255
2294
|
|
|
2256
2295
|
// Check auth status via httpOnly cookie (server-side validation)
|
|
2257
2296
|
const refreshAuth = useCallback(async () => {
|
|
@@ -2270,6 +2309,16 @@ export function StoreProvider({ children }: { children: React.ReactNode }) {
|
|
|
2270
2309
|
// Initialize client, check auth, and fetch store info
|
|
2271
2310
|
useEffect(() => {
|
|
2272
2311
|
const client = initClient();
|
|
2312
|
+
<% if (i18nEnabled) { %>
|
|
2313
|
+
|
|
2314
|
+
// Set locale on SDK client for translated product content
|
|
2315
|
+
if (locale) {
|
|
2316
|
+
client.setLocale(locale);
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
// Load UI message strings for current locale
|
|
2320
|
+
getMessages(locale || defaultLocale).then(setMessages);
|
|
2321
|
+
<% } %>
|
|
2273
2322
|
|
|
2274
2323
|
// Optimistic check: if brainerce_logged_in cookie exists, assume logged in
|
|
2275
2324
|
// while we validate the actual token server-side
|
|
@@ -2286,7 +2335,11 @@ export function StoreProvider({ children }: { children: React.ReactNode }) {
|
|
|
2286
2335
|
.then(setStoreInfo)
|
|
2287
2336
|
.catch(console.error)
|
|
2288
2337
|
.finally(() => setStoreLoading(false));
|
|
2338
|
+
<% if (i18nEnabled) { %>
|
|
2339
|
+
}, [refreshAuth, locale]);
|
|
2340
|
+
<% } else { %>
|
|
2289
2341
|
}, [refreshAuth]);
|
|
2342
|
+
<% } %>
|
|
2290
2343
|
|
|
2291
2344
|
// Cart management
|
|
2292
2345
|
const refreshCart = useCallback(async () => {
|
|
@@ -2339,6 +2392,21 @@ export function StoreProvider({ children }: { children: React.ReactNode }) {
|
|
|
2339
2392
|
|
|
2340
2393
|
const totals = cart ? getCartTotals(cart) : { subtotal: 0, discount: 0, shipping: 0, total: 0 };
|
|
2341
2394
|
|
|
2395
|
+
<% if (i18nEnabled) { %>
|
|
2396
|
+
return (
|
|
2397
|
+
<MessagesContext.Provider value={messages}>
|
|
2398
|
+
<StoreInfoContext.Provider value={{ storeInfo, loading: storeLoading }}>
|
|
2399
|
+
<AuthContext.Provider value={{ isLoggedIn, authLoading, customer, login, logout }}>
|
|
2400
|
+
<CartContext.Provider
|
|
2401
|
+
value={{ cart, cartLoading, refreshCart, itemCount, totals }}
|
|
2402
|
+
>
|
|
2403
|
+
{children}
|
|
2404
|
+
</CartContext.Provider>
|
|
2405
|
+
</AuthContext.Provider>
|
|
2406
|
+
</StoreInfoContext.Provider>
|
|
2407
|
+
</MessagesContext.Provider>
|
|
2408
|
+
);
|
|
2409
|
+
<% } else { %>
|
|
2342
2410
|
return (
|
|
2343
2411
|
<StoreInfoContext.Provider value={{ storeInfo, loading: storeLoading }}>
|
|
2344
2412
|
<AuthContext.Provider value={{ isLoggedIn, authLoading, customer, login, logout }}>
|
|
@@ -2350,6 +2418,7 @@ export function StoreProvider({ children }: { children: React.ReactNode }) {
|
|
|
2350
2418
|
</AuthContext.Provider>
|
|
2351
2419
|
</StoreInfoContext.Provider>
|
|
2352
2420
|
);
|
|
2421
|
+
<% } %>
|
|
2353
2422
|
}
|
|
2354
2423
|
`,
|
|
2355
2424
|
"src/app/page.tsx": `'use client';
|
|
@@ -2451,7 +2520,81 @@ export default function HomePage() {
|
|
|
2451
2520
|
);
|
|
2452
2521
|
}
|
|
2453
2522
|
`,
|
|
2454
|
-
"src/app/layout.tsx.ejs":
|
|
2523
|
+
"src/app/layout.tsx.ejs": `<% if (i18nEnabled) { %>
|
|
2524
|
+
import type { Metadata } from 'next';
|
|
2525
|
+
<%- fontImport %>
|
|
2526
|
+
import { StoreProvider } from '@/providers/store-provider';
|
|
2527
|
+
import { Header } from '@/components/layout/header';
|
|
2528
|
+
import { Footer } from '@/components/layout/footer';
|
|
2529
|
+
import { getDirection, supportedLocales } from '@/i18n';
|
|
2530
|
+
import '../globals.css';
|
|
2531
|
+
|
|
2532
|
+
<%- fontVariable %>
|
|
2533
|
+
|
|
2534
|
+
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com';
|
|
2535
|
+
|
|
2536
|
+
export const metadata: Metadata = {
|
|
2537
|
+
metadataBase: new URL(baseUrl),
|
|
2538
|
+
title: {
|
|
2539
|
+
default: 'My Store',
|
|
2540
|
+
template: \`%s | My Store\`,
|
|
2541
|
+
},
|
|
2542
|
+
description: 'My Store',
|
|
2543
|
+
alternates: {
|
|
2544
|
+
canonical: '/',
|
|
2545
|
+
},
|
|
2546
|
+
openGraph: {
|
|
2547
|
+
siteName: 'My Store',
|
|
2548
|
+
type: 'website',
|
|
2549
|
+
},
|
|
2550
|
+
robots: {
|
|
2551
|
+
index: true,
|
|
2552
|
+
follow: true,
|
|
2553
|
+
},
|
|
2554
|
+
};
|
|
2555
|
+
|
|
2556
|
+
const organizationJsonLd = {
|
|
2557
|
+
'@context': 'https://schema.org',
|
|
2558
|
+
'@type': 'Organization',
|
|
2559
|
+
name: 'My Store',
|
|
2560
|
+
url: baseUrl,
|
|
2561
|
+
};
|
|
2562
|
+
|
|
2563
|
+
export function generateStaticParams() {
|
|
2564
|
+
return supportedLocales.map((locale) => ({ locale }));
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
export default function RootLayout({
|
|
2568
|
+
children,
|
|
2569
|
+
params,
|
|
2570
|
+
}: {
|
|
2571
|
+
children: React.ReactNode;
|
|
2572
|
+
params: { locale: string };
|
|
2573
|
+
}) {
|
|
2574
|
+
const dir = getDirection(params.locale);
|
|
2575
|
+
|
|
2576
|
+
return (
|
|
2577
|
+
<html lang={params.locale} dir={dir}>
|
|
2578
|
+
<head>
|
|
2579
|
+
<script
|
|
2580
|
+
type="application/ld+json"
|
|
2581
|
+
dangerouslySetInnerHTML={{ __html: JSON.stringify(organizationJsonLd) }}
|
|
2582
|
+
/>
|
|
2583
|
+
</head>
|
|
2584
|
+
<body className={font.className}>
|
|
2585
|
+
<StoreProvider locale={params.locale}>
|
|
2586
|
+
<div className="min-h-screen flex flex-col">
|
|
2587
|
+
<Header />
|
|
2588
|
+
<main className="flex-1">{children}</main>
|
|
2589
|
+
<Footer />
|
|
2590
|
+
</div>
|
|
2591
|
+
</StoreProvider>
|
|
2592
|
+
</body>
|
|
2593
|
+
</html>
|
|
2594
|
+
);
|
|
2595
|
+
}
|
|
2596
|
+
<% } else { %>
|
|
2597
|
+
import type { Metadata } from 'next';
|
|
2455
2598
|
<%- fontImport %>
|
|
2456
2599
|
import { StoreProvider } from '@/providers/store-provider';
|
|
2457
2600
|
import { Header } from '@/components/layout/header';
|
|
@@ -2515,6 +2658,7 @@ export default function RootLayout({
|
|
|
2515
2658
|
</html>
|
|
2516
2659
|
);
|
|
2517
2660
|
}
|
|
2661
|
+
<% } %>
|
|
2518
2662
|
`,
|
|
2519
2663
|
"src/app/products/page.tsx": `'use client';
|
|
2520
2664
|
|
|
@@ -3415,13 +3559,13 @@ function CheckoutContent() {
|
|
|
3415
3559
|
}, [checkout?.id, storeInfo?.upsell?.checkoutOrderBumpEnabled]);
|
|
3416
3560
|
|
|
3417
3561
|
// Handle bump toggle
|
|
3418
|
-
async function handleBumpToggle(bumpId: string, add: boolean) {
|
|
3562
|
+
async function handleBumpToggle(bumpId: string, add: boolean, variantId?: string) {
|
|
3419
3563
|
if (!cart?.id || bumpLoading) return;
|
|
3420
3564
|
try {
|
|
3421
3565
|
setBumpLoading(bumpId);
|
|
3422
3566
|
const client = getClient();
|
|
3423
3567
|
if (add) {
|
|
3424
|
-
await client.addOrderBump(cart.id, bumpId);
|
|
3568
|
+
await client.addOrderBump(cart.id, bumpId, variantId);
|
|
3425
3569
|
setAddedBumpIds((prev) => new Set([...prev, bumpId]));
|
|
3426
3570
|
} else {
|
|
3427
3571
|
await client.removeOrderBump(cart.id, bumpId);
|
|
@@ -4119,6 +4263,23 @@ function OrderConfirmationContent() {
|
|
|
4119
4263
|
client.handlePaymentSuccess(checkoutId!);
|
|
4120
4264
|
await refreshCart();
|
|
4121
4265
|
|
|
4266
|
+
// For redirect-based payment providers (e.g. CardCom), the customer
|
|
4267
|
+
// returns with provider params in the URL (lowprofilecode, etc.).
|
|
4268
|
+
// Send these to the backend for server-side verification via the
|
|
4269
|
+
// provider's API (e.g. GetLpResult) \u2014 never trust URL params alone.
|
|
4270
|
+
const lowProfileCode =
|
|
4271
|
+
searchParams.get('lowprofilecode') || searchParams.get('LowProfileCode');
|
|
4272
|
+
if (lowProfileCode) {
|
|
4273
|
+
try {
|
|
4274
|
+
await client.confirmSdkPayment(checkoutId!, {
|
|
4275
|
+
paymentIntentId: lowProfileCode,
|
|
4276
|
+
});
|
|
4277
|
+
} catch (err) {
|
|
4278
|
+
console.warn('Redirect payment confirmation failed:', err);
|
|
4279
|
+
// Don't block \u2014 webhook may still process the payment
|
|
4280
|
+
}
|
|
4281
|
+
}
|
|
4282
|
+
|
|
4122
4283
|
const orderResult = await client.waitForOrder(checkoutId!, {
|
|
4123
4284
|
maxWaitMs: 30000,
|
|
4124
4285
|
});
|
|
@@ -5635,7 +5796,62 @@ export async function POST(request: NextRequest) {
|
|
|
5635
5796
|
return response;
|
|
5636
5797
|
}
|
|
5637
5798
|
`,
|
|
5638
|
-
"src/middleware.ts":
|
|
5799
|
+
"src/middleware.ts.ejs": `<% if (i18nEnabled) { %>
|
|
5800
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
5801
|
+
|
|
5802
|
+
const TOKEN_COOKIE = 'brainerce_customer_token';
|
|
5803
|
+
const PROTECTED_PATHS = ['/account'];
|
|
5804
|
+
const supportedLocales = <%- supportedLocales %>;
|
|
5805
|
+
const defaultLocale = '<%= defaultLocale %>';
|
|
5806
|
+
|
|
5807
|
+
function getLocaleFromPath(pathname: string): string | null {
|
|
5808
|
+
const segment = pathname.split('/')[1];
|
|
5809
|
+
return supportedLocales.includes(segment) ? segment : null;
|
|
5810
|
+
}
|
|
5811
|
+
|
|
5812
|
+
export function middleware(request: NextRequest) {
|
|
5813
|
+
const { pathname } = request.nextUrl;
|
|
5814
|
+
|
|
5815
|
+
// Skip static files and API routes
|
|
5816
|
+
if (
|
|
5817
|
+
pathname.startsWith('/api/') ||
|
|
5818
|
+
pathname.startsWith('/_next/') ||
|
|
5819
|
+
pathname.includes('.')
|
|
5820
|
+
) {
|
|
5821
|
+
return NextResponse.next();
|
|
5822
|
+
}
|
|
5823
|
+
|
|
5824
|
+
const pathnameLocale = getLocaleFromPath(pathname);
|
|
5825
|
+
|
|
5826
|
+
// Redirect to default locale if no locale prefix
|
|
5827
|
+
if (!pathnameLocale) {
|
|
5828
|
+
const url = request.nextUrl.clone();
|
|
5829
|
+
url.pathname = \`/\${defaultLocale}\${pathname}\`;
|
|
5830
|
+
return NextResponse.redirect(url);
|
|
5831
|
+
}
|
|
5832
|
+
|
|
5833
|
+
// Auth protection (with locale prefix)
|
|
5834
|
+
const pathWithoutLocale = pathname.replace(\`/\${pathnameLocale}\`, '') || '/';
|
|
5835
|
+
const isProtected = PROTECTED_PATHS.some((p) => pathWithoutLocale.startsWith(p));
|
|
5836
|
+
if (isProtected) {
|
|
5837
|
+
const token = request.cookies.get(TOKEN_COOKIE);
|
|
5838
|
+
if (!token?.value) {
|
|
5839
|
+
const loginUrl = new URL(\`/\${pathnameLocale}/login\`, request.url);
|
|
5840
|
+
return NextResponse.redirect(loginUrl);
|
|
5841
|
+
}
|
|
5842
|
+
}
|
|
5843
|
+
|
|
5844
|
+
// Set locale header for server components
|
|
5845
|
+
const response = NextResponse.next();
|
|
5846
|
+
response.headers.set('x-locale', pathnameLocale);
|
|
5847
|
+
return response;
|
|
5848
|
+
}
|
|
5849
|
+
|
|
5850
|
+
export const config = {
|
|
5851
|
+
matcher: ['/((?!_next|api|.*\\\\..*).*)'],
|
|
5852
|
+
};
|
|
5853
|
+
<% } else { %>
|
|
5854
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
5639
5855
|
|
|
5640
5856
|
const TOKEN_COOKIE = 'brainerce_customer_token';
|
|
5641
5857
|
|
|
@@ -5660,6 +5876,7 @@ export function middleware(request: NextRequest) {
|
|
|
5660
5876
|
export const config = {
|
|
5661
5877
|
matcher: ['/account/:path*'],
|
|
5662
5878
|
};
|
|
5879
|
+
<% } %>
|
|
5663
5880
|
`,
|
|
5664
5881
|
"src/components/products/product-card.tsx": `'use client';
|
|
5665
5882
|
|
|
@@ -7521,7 +7738,9 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
|
|
|
7521
7738
|
initialized.current = true;
|
|
7522
7739
|
|
|
7523
7740
|
const client = getClient();
|
|
7524
|
-
const
|
|
7741
|
+
const iframeSuccessUrl = \`\${window.location.origin}/payment-complete?checkout_id=\${checkoutId}\`;
|
|
7742
|
+
const iframeFailedUrl = \`\${window.location.origin}/payment-complete?checkout_id=\${checkoutId}&failed=true\`;
|
|
7743
|
+
const redirectSuccessUrl = \`\${window.location.origin}/order-confirmation?checkout_id=\${checkoutId}\`;
|
|
7525
7744
|
const cancelUrl = \`\${window.location.origin}/checkout?checkout_id=\${checkoutId}&canceled=true\`;
|
|
7526
7745
|
|
|
7527
7746
|
let sdkInitDone = false;
|
|
@@ -7682,8 +7901,19 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
|
|
|
7682
7901
|
});
|
|
7683
7902
|
|
|
7684
7903
|
// C) Create payment intent (starts wallet timer)
|
|
7685
|
-
|
|
7686
|
-
|
|
7904
|
+
// Wait for provider info so we can choose the right success URL:
|
|
7905
|
+
// iframe providers redirect inside the iframe to /payment-complete (postMessage),
|
|
7906
|
+
// redirect providers go straight to /order-confirmation.
|
|
7907
|
+
const intentPromise = providerPromise
|
|
7908
|
+
.then((providerSdk) => {
|
|
7909
|
+
const isIframe = providerSdk?.renderType === 'iframe';
|
|
7910
|
+
const successUrl = isIframe ? iframeSuccessUrl : redirectSuccessUrl;
|
|
7911
|
+
const failedUrl = isIframe ? iframeFailedUrl : cancelUrl;
|
|
7912
|
+
return client.createPaymentIntent(checkoutId, {
|
|
7913
|
+
successUrl,
|
|
7914
|
+
cancelUrl: failedUrl,
|
|
7915
|
+
});
|
|
7916
|
+
})
|
|
7687
7917
|
.then((intent) => {
|
|
7688
7918
|
setPaymentIntent(intent);
|
|
7689
7919
|
return intent;
|
|
@@ -7708,6 +7938,36 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
|
|
|
7708
7938
|
window.location.href = intent.clientSecret;
|
|
7709
7939
|
return;
|
|
7710
7940
|
}
|
|
7941
|
+
|
|
7942
|
+
// Iframe mode: listen for postMessage from the /payment-complete callback
|
|
7943
|
+
// page that loads inside the iframe after the provider redirects on completion.
|
|
7944
|
+
if (sdk.renderType === 'iframe') {
|
|
7945
|
+
const handleMessage = (event: MessageEvent) => {
|
|
7946
|
+
if (event.origin !== window.location.origin) return;
|
|
7947
|
+
if (event.data?.type !== 'brainerce:payment-complete') return;
|
|
7948
|
+
|
|
7949
|
+
const params = event.data.data as Record<string, string> | undefined;
|
|
7950
|
+
if (params?.failed === 'true') {
|
|
7951
|
+
setError(t('paymentError'));
|
|
7952
|
+
return;
|
|
7953
|
+
}
|
|
7954
|
+
|
|
7955
|
+
// Map provider-specific params to normalized format for
|
|
7956
|
+
// server-side verification (e.g. CardCom lowprofilecode \u2192 paymentIntentId)
|
|
7957
|
+
const lowProfileCode = params?.lowprofilecode || params?.LowProfileCode;
|
|
7958
|
+
const normalized: Record<string, unknown> = { ...params };
|
|
7959
|
+
if (lowProfileCode) {
|
|
7960
|
+
normalized.paymentIntentId = lowProfileCode;
|
|
7961
|
+
}
|
|
7962
|
+
|
|
7963
|
+
// Trigger server-side verification + order creation
|
|
7964
|
+
handleSuccess(normalized);
|
|
7965
|
+
};
|
|
7966
|
+
window.addEventListener('message', handleMessage);
|
|
7967
|
+
cleanups.push(() => window.removeEventListener('message', handleMessage));
|
|
7968
|
+
return;
|
|
7969
|
+
}
|
|
7970
|
+
|
|
7711
7971
|
if (sdk.renderType !== 'sdk-widget' || !sdk.globalName) return;
|
|
7712
7972
|
|
|
7713
7973
|
// Store for retryRender from onError callback
|
|
@@ -7858,15 +8118,45 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
|
|
|
7858
8118
|
|
|
7859
8119
|
if (sdk.renderType === 'iframe') {
|
|
7860
8120
|
return (
|
|
7861
|
-
|
|
7862
|
-
|
|
7863
|
-
|
|
7864
|
-
className="w-full
|
|
7865
|
-
|
|
7866
|
-
|
|
7867
|
-
|
|
7868
|
-
|
|
7869
|
-
|
|
8121
|
+
<>
|
|
8122
|
+
{/* Modal overlay */}
|
|
8123
|
+
<div className="fixed inset-0 z-50 flex items-start justify-center overflow-y-auto bg-black/50 py-6 backdrop-blur-sm">
|
|
8124
|
+
<div className="relative mx-4 w-full max-w-md rounded-2xl bg-white shadow-2xl">
|
|
8125
|
+
{/* Close button */}
|
|
8126
|
+
<button
|
|
8127
|
+
onClick={() => {
|
|
8128
|
+
window.location.href = \`/checkout?checkout_id=\${checkoutId}&canceled=true\`;
|
|
8129
|
+
}}
|
|
8130
|
+
className="absolute end-3 top-3 z-10 flex h-8 w-8 items-center justify-center rounded-full bg-white/80 text-gray-500 shadow-sm transition-colors hover:bg-gray-100 hover:text-gray-700"
|
|
8131
|
+
aria-label="Close"
|
|
8132
|
+
>
|
|
8133
|
+
<svg
|
|
8134
|
+
width="14"
|
|
8135
|
+
height="14"
|
|
8136
|
+
viewBox="0 0 14 14"
|
|
8137
|
+
fill="none"
|
|
8138
|
+
stroke="currentColor"
|
|
8139
|
+
strokeWidth="2"
|
|
8140
|
+
strokeLinecap="round"
|
|
8141
|
+
>
|
|
8142
|
+
<path d="M1 1l12 12M13 1L1 13" />
|
|
8143
|
+
</svg>
|
|
8144
|
+
</button>
|
|
8145
|
+
<iframe
|
|
8146
|
+
src={paymentIntent.clientSecret}
|
|
8147
|
+
className="w-full rounded-2xl border-0"
|
|
8148
|
+
style={{ height: '80vh' }}
|
|
8149
|
+
title={t('payment')}
|
|
8150
|
+
allow="payment"
|
|
8151
|
+
/>
|
|
8152
|
+
</div>
|
|
8153
|
+
</div>
|
|
8154
|
+
{/* Placeholder so the checkout layout doesn't collapse */}
|
|
8155
|
+
<div className={cn('flex flex-col items-center justify-center py-12', className)}>
|
|
8156
|
+
<LoadingSpinner size="lg" />
|
|
8157
|
+
<p className="text-muted-foreground mt-4 text-sm">{t('preparingPayment')}</p>
|
|
8158
|
+
</div>
|
|
8159
|
+
</>
|
|
7870
8160
|
);
|
|
7871
8161
|
}
|
|
7872
8162
|
|
|
@@ -9993,10 +10283,10 @@ export function CartUpgradeBanner({
|
|
|
9993
10283
|
`,
|
|
9994
10284
|
"src/components/cart/cart-bundle-offer.tsx": `'use client';
|
|
9995
10285
|
|
|
9996
|
-
import { useState } from 'react';
|
|
10286
|
+
import { useState, useMemo } from 'react';
|
|
9997
10287
|
import Image from 'next/image';
|
|
9998
10288
|
import type { CartBundleOffer as CartBundleOfferType } from 'brainerce';
|
|
9999
|
-
import { formatPrice } from 'brainerce';
|
|
10289
|
+
import { formatPrice, getVariantOptions } from 'brainerce';
|
|
10000
10290
|
import { useStoreInfo } from '@/providers/store-provider';
|
|
10001
10291
|
import { useTranslations } from '@/lib/translations';
|
|
10002
10292
|
import { cn } from '@/lib/utils';
|
|
@@ -10013,28 +10303,114 @@ export function CartBundleOfferCard({ offer, cartId, onAdd, className }: CartBun
|
|
|
10013
10303
|
const t = useTranslations('cart');
|
|
10014
10304
|
const currency = storeInfo?.currency || 'USD';
|
|
10015
10305
|
const [adding, setAdding] = useState(false);
|
|
10306
|
+
const [selectedVariantId, setSelectedVariantId] = useState<string | null>(null);
|
|
10016
10307
|
|
|
10017
10308
|
const product = offer.bundleProduct;
|
|
10309
|
+
const variants = product.variants;
|
|
10310
|
+
const requiresSelection = offer.requiresVariantSelection && variants && variants.length > 0;
|
|
10311
|
+
|
|
10312
|
+
// Build attribute groups from variants
|
|
10313
|
+
const attributeGroups = useMemo(() => {
|
|
10314
|
+
if (!requiresSelection || !variants) return [];
|
|
10315
|
+
const groups = new Map<string, Set<string>>();
|
|
10316
|
+
for (const v of variants) {
|
|
10317
|
+
const opts = getVariantOptions(v as any);
|
|
10318
|
+
for (const opt of opts) {
|
|
10319
|
+
if (!groups.has(opt.name)) groups.set(opt.name, new Set());
|
|
10320
|
+
groups.get(opt.name)!.add(opt.value);
|
|
10321
|
+
}
|
|
10322
|
+
}
|
|
10323
|
+
return Array.from(groups.entries()).map(([name, values]) => ({
|
|
10324
|
+
name,
|
|
10325
|
+
values: Array.from(values),
|
|
10326
|
+
}));
|
|
10327
|
+
}, [requiresSelection, variants]);
|
|
10328
|
+
|
|
10329
|
+
const [selectedAttrs, setSelectedAttrs] = useState<Record<string, string>>({});
|
|
10330
|
+
|
|
10331
|
+
const selectedVariant = useMemo(() => {
|
|
10332
|
+
if (!requiresSelection || !variants) return null;
|
|
10333
|
+
return (
|
|
10334
|
+
variants.find((v) => {
|
|
10335
|
+
const opts = getVariantOptions(v as any);
|
|
10336
|
+
return attributeGroups.every((group) => {
|
|
10337
|
+
const opt = opts.find((o) => o.name === group.name);
|
|
10338
|
+
return opt && selectedAttrs[group.name] === opt.value;
|
|
10339
|
+
});
|
|
10340
|
+
}) ?? null
|
|
10341
|
+
);
|
|
10342
|
+
}, [requiresSelection, variants, selectedAttrs, attributeGroups]);
|
|
10343
|
+
|
|
10344
|
+
const effectiveVariantId = selectedVariant?.id ?? selectedVariantId;
|
|
10345
|
+
|
|
10346
|
+
function handleAttrSelect(attrName: string, value: string) {
|
|
10347
|
+
const next = { ...selectedAttrs, [attrName]: value };
|
|
10348
|
+
setSelectedAttrs(next);
|
|
10349
|
+
if (variants) {
|
|
10350
|
+
const match = variants.find((v) => {
|
|
10351
|
+
const opts = getVariantOptions(v as any);
|
|
10352
|
+
return attributeGroups.every((group) => {
|
|
10353
|
+
const opt = opts.find((o) => o.name === group.name);
|
|
10354
|
+
return opt && next[group.name] === opt.value;
|
|
10355
|
+
});
|
|
10356
|
+
});
|
|
10357
|
+
setSelectedVariantId(match?.id ?? null);
|
|
10358
|
+
}
|
|
10359
|
+
}
|
|
10360
|
+
|
|
10361
|
+
// Compute display prices
|
|
10362
|
+
const { displayOriginal, displayDiscounted, discountLabel } = useMemo(() => {
|
|
10363
|
+
let effectivePrice: number;
|
|
10364
|
+
if (selectedVariant) {
|
|
10365
|
+
const vSale = selectedVariant.salePrice ? parseFloat(selectedVariant.salePrice) : null;
|
|
10366
|
+
const vPrice = selectedVariant.price ? parseFloat(selectedVariant.price) : null;
|
|
10367
|
+
effectivePrice = vSale ?? vPrice ?? parseFloat(offer.originalPrice);
|
|
10368
|
+
} else {
|
|
10369
|
+
effectivePrice = parseFloat(offer.originalPrice);
|
|
10370
|
+
}
|
|
10371
|
+
|
|
10372
|
+
let discounted: number;
|
|
10373
|
+
if (offer.discountType === 'PERCENTAGE') {
|
|
10374
|
+
discounted = effectivePrice * (1 - parseFloat(offer.discountValue) / 100);
|
|
10375
|
+
} else {
|
|
10376
|
+
discounted = Math.max(0, effectivePrice - parseFloat(offer.discountValue));
|
|
10377
|
+
}
|
|
10378
|
+
|
|
10379
|
+
const label =
|
|
10380
|
+
offer.discountType === 'PERCENTAGE'
|
|
10381
|
+
? \`\${offer.discountValue}%\`
|
|
10382
|
+
: (formatPrice(parseFloat(offer.discountValue), { currency }) as string);
|
|
10383
|
+
|
|
10384
|
+
return { displayOriginal: effectivePrice, displayDiscounted: discounted, discountLabel: label };
|
|
10385
|
+
}, [selectedVariant, offer.originalPrice, offer.discountType, offer.discountValue, currency]);
|
|
10386
|
+
|
|
10387
|
+
const isOos =
|
|
10388
|
+
selectedVariant?.inventory?.trackingMode !== 'NOT_TRACKED' &&
|
|
10389
|
+
selectedVariant?.inventory?.available != null &&
|
|
10390
|
+
selectedVariant.inventory.available <= 0;
|
|
10391
|
+
|
|
10392
|
+
const lockedLabel =
|
|
10393
|
+
offer.lockedVariant?.name ??
|
|
10394
|
+
(offer.lockedVariant?.attributes
|
|
10395
|
+
? Object.values(offer.lockedVariant.attributes).join(' / ')
|
|
10396
|
+
: null);
|
|
10397
|
+
|
|
10018
10398
|
const firstImage = product.images?.[0];
|
|
10019
10399
|
const imageUrl = firstImage
|
|
10020
10400
|
? typeof firstImage === 'string'
|
|
10021
10401
|
? firstImage
|
|
10022
10402
|
: firstImage.url
|
|
10023
10403
|
: null;
|
|
10024
|
-
|
|
10025
|
-
const
|
|
10026
|
-
const discountLabel =
|
|
10027
|
-
offer.discountType === 'PERCENTAGE'
|
|
10028
|
-
? \`\${offer.discountValue}%\`
|
|
10029
|
-
: (formatPrice(parseFloat(offer.discountValue), { currency }) as string);
|
|
10404
|
+
|
|
10405
|
+
const canAdd = !requiresSelection || !!effectiveVariantId;
|
|
10030
10406
|
|
|
10031
10407
|
async function handleAdd() {
|
|
10032
|
-
if (adding) return;
|
|
10408
|
+
if (adding || !canAdd || isOos) return;
|
|
10033
10409
|
try {
|
|
10034
10410
|
setAdding(true);
|
|
10035
10411
|
const { getClient } = await import('@/lib/brainerce');
|
|
10036
10412
|
const client = getClient();
|
|
10037
|
-
await client.addBundleToCart(cartId, offer.id);
|
|
10413
|
+
await client.addBundleToCart(cartId, offer.id, effectiveVariantId ?? undefined);
|
|
10038
10414
|
onAdd();
|
|
10039
10415
|
} catch (err) {
|
|
10040
10416
|
console.error('Failed to add bundle item:', err);
|
|
@@ -10044,70 +10420,121 @@ export function CartBundleOfferCard({ offer, cartId, onAdd, className }: CartBun
|
|
|
10044
10420
|
}
|
|
10045
10421
|
|
|
10046
10422
|
return (
|
|
10047
|
-
<div
|
|
10048
|
-
className=
|
|
10049
|
-
|
|
10050
|
-
className
|
|
10051
|
-
|
|
10052
|
-
|
|
10053
|
-
|
|
10054
|
-
|
|
10055
|
-
|
|
10056
|
-
|
|
10057
|
-
|
|
10058
|
-
|
|
10059
|
-
|
|
10060
|
-
|
|
10061
|
-
|
|
10062
|
-
|
|
10063
|
-
|
|
10064
|
-
|
|
10065
|
-
|
|
10066
|
-
</svg>
|
|
10067
|
-
</div>
|
|
10068
|
-
)}
|
|
10069
|
-
</div>
|
|
10423
|
+
<div className={cn('bg-background border-border rounded-lg border p-4', className)}>
|
|
10424
|
+
<div className="flex items-center gap-4">
|
|
10425
|
+
{/* Product image */}
|
|
10426
|
+
<div className="bg-muted relative h-16 w-16 flex-shrink-0 overflow-hidden rounded">
|
|
10427
|
+
{imageUrl ? (
|
|
10428
|
+
<Image src={imageUrl} alt={product.name} fill sizes="64px" className="object-cover" />
|
|
10429
|
+
) : (
|
|
10430
|
+
<div className="text-muted-foreground flex h-full w-full items-center justify-center">
|
|
10431
|
+
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
10432
|
+
<path
|
|
10433
|
+
strokeLinecap="round"
|
|
10434
|
+
strokeLinejoin="round"
|
|
10435
|
+
strokeWidth={1.5}
|
|
10436
|
+
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
|
|
10437
|
+
/>
|
|
10438
|
+
</svg>
|
|
10439
|
+
</div>
|
|
10440
|
+
)}
|
|
10441
|
+
</div>
|
|
10070
10442
|
|
|
10071
|
-
|
|
10072
|
-
|
|
10073
|
-
|
|
10074
|
-
|
|
10075
|
-
|
|
10076
|
-
|
|
10077
|
-
|
|
10078
|
-
<
|
|
10079
|
-
|
|
10080
|
-
|
|
10081
|
-
|
|
10082
|
-
|
|
10083
|
-
|
|
10084
|
-
|
|
10085
|
-
-
|
|
10086
|
-
|
|
10443
|
+
{/* Details */}
|
|
10444
|
+
<div className="min-w-0 flex-1">
|
|
10445
|
+
<p className="text-foreground text-sm font-medium">{offer.name}</p>
|
|
10446
|
+
{offer.description && (
|
|
10447
|
+
<p className="text-muted-foreground mt-0.5 text-xs">{offer.description}</p>
|
|
10448
|
+
)}
|
|
10449
|
+
{lockedLabel && <p className="text-muted-foreground mt-0.5 text-xs">{lockedLabel}</p>}
|
|
10450
|
+
<div className="mt-1 flex items-center gap-2">
|
|
10451
|
+
<span className="text-muted-foreground text-sm line-through">
|
|
10452
|
+
{formatPrice(displayOriginal, { currency }) as string}
|
|
10453
|
+
</span>
|
|
10454
|
+
<span className="text-foreground text-sm font-semibold">
|
|
10455
|
+
{formatPrice(displayDiscounted, { currency }) as string}
|
|
10456
|
+
</span>
|
|
10457
|
+
<span className="bg-destructive/10 text-destructive rounded px-1.5 py-0.5 text-xs font-medium">
|
|
10458
|
+
-{discountLabel}
|
|
10459
|
+
</span>
|
|
10460
|
+
</div>
|
|
10087
10461
|
</div>
|
|
10462
|
+
|
|
10463
|
+
{/* Add button */}
|
|
10464
|
+
<button
|
|
10465
|
+
type="button"
|
|
10466
|
+
onClick={handleAdd}
|
|
10467
|
+
disabled={adding || !canAdd || isOos}
|
|
10468
|
+
className={cn(
|
|
10469
|
+
'bg-primary text-primary-foreground flex-shrink-0 rounded px-4 py-2 text-xs font-medium transition-opacity hover:opacity-90',
|
|
10470
|
+
'disabled:cursor-not-allowed disabled:opacity-50'
|
|
10471
|
+
)}
|
|
10472
|
+
>
|
|
10473
|
+
{adding
|
|
10474
|
+
? t('addingBundle')
|
|
10475
|
+
: requiresSelection && !canAdd
|
|
10476
|
+
? t('selectOptions') || 'Select options'
|
|
10477
|
+
: t('addBundleItem')}
|
|
10478
|
+
</button>
|
|
10088
10479
|
</div>
|
|
10089
10480
|
|
|
10090
|
-
{/*
|
|
10091
|
-
|
|
10092
|
-
|
|
10093
|
-
|
|
10094
|
-
|
|
10095
|
-
|
|
10096
|
-
|
|
10097
|
-
|
|
10098
|
-
|
|
10099
|
-
|
|
10100
|
-
|
|
10101
|
-
|
|
10481
|
+
{/* Compact variant selector */}
|
|
10482
|
+
{requiresSelection && (
|
|
10483
|
+
<div className="mt-3 space-y-1.5 ps-20">
|
|
10484
|
+
{attributeGroups.map((group) => (
|
|
10485
|
+
<div key={group.name} className="flex flex-wrap items-center gap-1.5">
|
|
10486
|
+
<span className="text-muted-foreground text-xs">{group.name}:</span>
|
|
10487
|
+
{group.values.map((value) => {
|
|
10488
|
+
const isSelected = selectedAttrs[group.name] === value;
|
|
10489
|
+
const variantForValue = variants?.find((v) => {
|
|
10490
|
+
const opts = getVariantOptions(v as any);
|
|
10491
|
+
const matchesValue = opts.some((o) => o.name === group.name && o.value === value);
|
|
10492
|
+
if (!matchesValue) return false;
|
|
10493
|
+
return Object.entries(selectedAttrs).every(([k, sv]) => {
|
|
10494
|
+
if (k === group.name) return true;
|
|
10495
|
+
return opts.some((o) => o.name === k && o.value === sv);
|
|
10496
|
+
});
|
|
10497
|
+
});
|
|
10498
|
+
const isVariantOos =
|
|
10499
|
+
variantForValue?.inventory?.trackingMode !== 'NOT_TRACKED' &&
|
|
10500
|
+
variantForValue?.inventory?.available != null &&
|
|
10501
|
+
variantForValue.inventory.available <= 0;
|
|
10502
|
+
|
|
10503
|
+
return (
|
|
10504
|
+
<button
|
|
10505
|
+
key={value}
|
|
10506
|
+
type="button"
|
|
10507
|
+
onClick={() => handleAttrSelect(group.name, value)}
|
|
10508
|
+
disabled={isVariantOos}
|
|
10509
|
+
className={cn(
|
|
10510
|
+
'rounded-full border px-2.5 py-0.5 text-xs transition-colors',
|
|
10511
|
+
isSelected
|
|
10512
|
+
? 'border-primary bg-primary text-primary-foreground'
|
|
10513
|
+
: 'border-border text-foreground hover:border-primary/50',
|
|
10514
|
+
isVariantOos && 'cursor-not-allowed line-through opacity-40'
|
|
10515
|
+
)}
|
|
10516
|
+
>
|
|
10517
|
+
{value}
|
|
10518
|
+
</button>
|
|
10519
|
+
);
|
|
10520
|
+
})}
|
|
10521
|
+
</div>
|
|
10522
|
+
))}
|
|
10523
|
+
{isOos && effectiveVariantId && (
|
|
10524
|
+
<p className="text-destructive text-xs">{t('outOfStock') || 'Out of stock'}</p>
|
|
10525
|
+
)}
|
|
10526
|
+
</div>
|
|
10527
|
+
)}
|
|
10102
10528
|
</div>
|
|
10103
10529
|
);
|
|
10104
10530
|
}
|
|
10105
10531
|
`,
|
|
10106
10532
|
"src/components/checkout/order-bump-card.tsx": `'use client';
|
|
10107
10533
|
|
|
10534
|
+
import { useState, useMemo } from 'react';
|
|
10108
10535
|
import Image from 'next/image';
|
|
10109
|
-
import type { OrderBump } from 'brainerce';
|
|
10110
|
-
import { formatPrice } from 'brainerce';
|
|
10536
|
+
import type { OrderBump, RecommendationVariant } from 'brainerce';
|
|
10537
|
+
import { formatPrice, getVariantOptions } from 'brainerce';
|
|
10111
10538
|
import { useStoreInfo } from '@/providers/store-provider';
|
|
10112
10539
|
import { useTranslations } from '@/lib/translations';
|
|
10113
10540
|
import { cn } from '@/lib/utils';
|
|
@@ -10115,7 +10542,7 @@ import { cn } from '@/lib/utils';
|
|
|
10115
10542
|
interface OrderBumpCardProps {
|
|
10116
10543
|
bump: OrderBump;
|
|
10117
10544
|
isAdded: boolean;
|
|
10118
|
-
onToggle: (bumpId: string, add: boolean) => void;
|
|
10545
|
+
onToggle: (bumpId: string, add: boolean, variantId?: string) => void;
|
|
10119
10546
|
loading: boolean;
|
|
10120
10547
|
className?: string;
|
|
10121
10548
|
}
|
|
@@ -10124,66 +10551,225 @@ export function OrderBumpCard({ bump, isAdded, onToggle, loading, className }: O
|
|
|
10124
10551
|
const { storeInfo } = useStoreInfo();
|
|
10125
10552
|
const t = useTranslations('checkout');
|
|
10126
10553
|
const currency = storeInfo?.currency || 'USD';
|
|
10554
|
+
const [selectedVariantId, setSelectedVariantId] = useState<string | null>(null);
|
|
10127
10555
|
|
|
10128
10556
|
const product = bump.bumpProduct;
|
|
10557
|
+
const variants = product.variants;
|
|
10558
|
+
const requiresSelection = bump.requiresVariantSelection && variants && variants.length > 0;
|
|
10559
|
+
|
|
10560
|
+
// Build attribute groups from variants for pill selector
|
|
10561
|
+
const attributeGroups = useMemo(() => {
|
|
10562
|
+
if (!requiresSelection || !variants) return [];
|
|
10563
|
+
const groups = new Map<string, Set<string>>();
|
|
10564
|
+
for (const v of variants) {
|
|
10565
|
+
const opts = getVariantOptions(v as any);
|
|
10566
|
+
for (const opt of opts) {
|
|
10567
|
+
if (!groups.has(opt.name)) groups.set(opt.name, new Set());
|
|
10568
|
+
groups.get(opt.name)!.add(opt.value);
|
|
10569
|
+
}
|
|
10570
|
+
}
|
|
10571
|
+
return Array.from(groups.entries()).map(([name, values]) => ({
|
|
10572
|
+
name,
|
|
10573
|
+
values: Array.from(values),
|
|
10574
|
+
}));
|
|
10575
|
+
}, [requiresSelection, variants]);
|
|
10576
|
+
|
|
10577
|
+
// Track selected attributes
|
|
10578
|
+
const [selectedAttrs, setSelectedAttrs] = useState<Record<string, string>>({});
|
|
10579
|
+
|
|
10580
|
+
// Find matching variant based on selected attributes
|
|
10581
|
+
const selectedVariant = useMemo(() => {
|
|
10582
|
+
if (!requiresSelection || !variants) return null;
|
|
10583
|
+
return (
|
|
10584
|
+
variants.find((v) => {
|
|
10585
|
+
const opts = getVariantOptions(v as any);
|
|
10586
|
+
return attributeGroups.every((group) => {
|
|
10587
|
+
const opt = opts.find((o) => o.name === group.name);
|
|
10588
|
+
return opt && selectedAttrs[group.name] === opt.value;
|
|
10589
|
+
});
|
|
10590
|
+
}) ?? null
|
|
10591
|
+
);
|
|
10592
|
+
}, [requiresSelection, variants, selectedAttrs, attributeGroups]);
|
|
10593
|
+
|
|
10594
|
+
// Update selectedVariantId when variant match changes
|
|
10595
|
+
const effectiveVariantId = selectedVariant?.id ?? selectedVariantId;
|
|
10596
|
+
|
|
10597
|
+
function handleAttrSelect(attrName: string, value: string) {
|
|
10598
|
+
const next = { ...selectedAttrs, [attrName]: value };
|
|
10599
|
+
setSelectedAttrs(next);
|
|
10600
|
+
// Find matching variant with new selection
|
|
10601
|
+
if (variants) {
|
|
10602
|
+
const match = variants.find((v) => {
|
|
10603
|
+
const opts = getVariantOptions(v as any);
|
|
10604
|
+
return attributeGroups.every((group) => {
|
|
10605
|
+
const opt = opts.find((o) => o.name === group.name);
|
|
10606
|
+
return opt && next[group.name] === opt.value;
|
|
10607
|
+
});
|
|
10608
|
+
});
|
|
10609
|
+
setSelectedVariantId(match?.id ?? null);
|
|
10610
|
+
}
|
|
10611
|
+
}
|
|
10612
|
+
|
|
10613
|
+
// Compute display price
|
|
10614
|
+
const { displayOriginal, displayDiscounted } = useMemo(() => {
|
|
10615
|
+
let effectivePrice: number;
|
|
10616
|
+
if (selectedVariant) {
|
|
10617
|
+
const vSale = selectedVariant.salePrice ? parseFloat(selectedVariant.salePrice) : null;
|
|
10618
|
+
const vPrice = selectedVariant.price ? parseFloat(selectedVariant.price) : null;
|
|
10619
|
+
effectivePrice = vSale ?? vPrice ?? parseFloat(bump.originalPrice);
|
|
10620
|
+
} else {
|
|
10621
|
+
effectivePrice = parseFloat(bump.originalPrice);
|
|
10622
|
+
}
|
|
10623
|
+
|
|
10624
|
+
let discounted: number | null = null;
|
|
10625
|
+
if (bump.discountType && bump.discountValue) {
|
|
10626
|
+
const dv = parseFloat(bump.discountValue);
|
|
10627
|
+
if (bump.discountType === 'PERCENTAGE') {
|
|
10628
|
+
discounted = effectivePrice * (1 - dv / 100);
|
|
10629
|
+
} else {
|
|
10630
|
+
discounted = Math.max(0, effectivePrice - dv);
|
|
10631
|
+
}
|
|
10632
|
+
}
|
|
10633
|
+
|
|
10634
|
+
return { displayOriginal: effectivePrice, displayDiscounted: discounted };
|
|
10635
|
+
}, [selectedVariant, bump.originalPrice, bump.discountType, bump.discountValue]);
|
|
10636
|
+
|
|
10637
|
+
// Check if selected variant is out of stock
|
|
10638
|
+
const isOos =
|
|
10639
|
+
selectedVariant?.inventory?.trackingMode !== 'NOT_TRACKED' &&
|
|
10640
|
+
selectedVariant?.inventory?.available != null &&
|
|
10641
|
+
selectedVariant.inventory.available <= 0;
|
|
10642
|
+
|
|
10643
|
+
// Locked variant label
|
|
10644
|
+
const lockedLabel =
|
|
10645
|
+
bump.lockedVariant?.name ??
|
|
10646
|
+
(bump.lockedVariant?.attributes
|
|
10647
|
+
? Object.values(bump.lockedVariant.attributes).join(' / ')
|
|
10648
|
+
: null);
|
|
10649
|
+
|
|
10129
10650
|
const firstImage = product.images?.[0];
|
|
10130
10651
|
const imageUrl = firstImage
|
|
10131
10652
|
? typeof firstImage === 'string'
|
|
10132
10653
|
? firstImage
|
|
10133
10654
|
: firstImage.url
|
|
10134
10655
|
: null;
|
|
10135
|
-
|
|
10136
|
-
const
|
|
10137
|
-
const discountedPrice = hasDiscount ? parseFloat(bump.discountedPrice!) : null;
|
|
10656
|
+
|
|
10657
|
+
const canToggle = !requiresSelection || !!effectiveVariantId;
|
|
10138
10658
|
|
|
10139
10659
|
return (
|
|
10140
|
-
<
|
|
10660
|
+
<div
|
|
10141
10661
|
className={cn(
|
|
10142
|
-
'border-border hover:border-primary/50
|
|
10662
|
+
'border-border hover:border-primary/50 rounded-lg border p-3 transition-colors',
|
|
10143
10663
|
isAdded && 'border-primary bg-primary/5',
|
|
10144
10664
|
loading && 'pointer-events-none opacity-60',
|
|
10145
10665
|
className
|
|
10146
10666
|
)}
|
|
10147
10667
|
>
|
|
10148
|
-
<
|
|
10149
|
-
|
|
10150
|
-
|
|
10151
|
-
|
|
10152
|
-
|
|
10153
|
-
|
|
10154
|
-
|
|
10155
|
-
|
|
10156
|
-
|
|
10157
|
-
|
|
10158
|
-
|
|
10159
|
-
|
|
10160
|
-
</div>
|
|
10161
|
-
)}
|
|
10668
|
+
<label className="flex cursor-pointer items-start gap-3">
|
|
10669
|
+
<input
|
|
10670
|
+
type="checkbox"
|
|
10671
|
+
checked={isAdded}
|
|
10672
|
+
onChange={() => {
|
|
10673
|
+
if (canToggle) {
|
|
10674
|
+
onToggle(bump.id, !isAdded, effectiveVariantId ?? undefined);
|
|
10675
|
+
}
|
|
10676
|
+
}}
|
|
10677
|
+
disabled={loading || !canToggle || isOos}
|
|
10678
|
+
className="mt-1 h-4 w-4 shrink-0 rounded"
|
|
10679
|
+
/>
|
|
10162
10680
|
|
|
10163
|
-
|
|
10164
|
-
|
|
10165
|
-
|
|
10166
|
-
|
|
10167
|
-
|
|
10681
|
+
{/* Image */}
|
|
10682
|
+
{imageUrl && (
|
|
10683
|
+
<div className="bg-muted relative h-10 w-10 shrink-0 overflow-hidden rounded">
|
|
10684
|
+
<Image src={imageUrl} alt={product.name} fill sizes="40px" className="object-cover" />
|
|
10685
|
+
</div>
|
|
10168
10686
|
)}
|
|
10169
|
-
|
|
10170
|
-
|
|
10171
|
-
|
|
10172
|
-
|
|
10173
|
-
|
|
10174
|
-
|
|
10687
|
+
|
|
10688
|
+
{/* Content */}
|
|
10689
|
+
<div className="min-w-0 flex-1">
|
|
10690
|
+
<p className="text-foreground text-sm font-medium">{bump.title}</p>
|
|
10691
|
+
{bump.description && (
|
|
10692
|
+
<p className="text-muted-foreground mt-0.5 text-xs">{bump.description}</p>
|
|
10693
|
+
)}
|
|
10694
|
+
|
|
10695
|
+
{/* Locked variant label */}
|
|
10696
|
+
{lockedLabel && <p className="text-muted-foreground mt-0.5 text-xs">{lockedLabel}</p>}
|
|
10697
|
+
|
|
10698
|
+
{/* Price */}
|
|
10699
|
+
<div className="mt-1 flex items-center gap-2">
|
|
10700
|
+
{displayDiscounted != null ? (
|
|
10701
|
+
<>
|
|
10702
|
+
<span className="text-muted-foreground text-xs line-through">
|
|
10703
|
+
{formatPrice(displayOriginal, { currency }) as string}
|
|
10704
|
+
</span>
|
|
10705
|
+
<span className="text-foreground text-sm font-semibold">
|
|
10706
|
+
{formatPrice(displayDiscounted, { currency }) as string}
|
|
10707
|
+
</span>
|
|
10708
|
+
</>
|
|
10709
|
+
) : (
|
|
10175
10710
|
<span className="text-foreground text-sm font-semibold">
|
|
10176
|
-
{formatPrice(
|
|
10711
|
+
{formatPrice(displayOriginal, { currency }) as string}
|
|
10177
10712
|
</span>
|
|
10178
|
-
|
|
10179
|
-
|
|
10180
|
-
|
|
10181
|
-
|
|
10182
|
-
|
|
10713
|
+
)}
|
|
10714
|
+
{requiresSelection && !effectiveVariantId && (
|
|
10715
|
+
<span className="text-muted-foreground text-xs">
|
|
10716
|
+
{t('selectOptions') || 'Select options'}
|
|
10717
|
+
</span>
|
|
10718
|
+
)}
|
|
10719
|
+
</div>
|
|
10720
|
+
</div>
|
|
10721
|
+
</label>
|
|
10722
|
+
|
|
10723
|
+
{/* Compact variant selector */}
|
|
10724
|
+
{requiresSelection && !isAdded && (
|
|
10725
|
+
<div className="ms-7 mt-2 space-y-1.5">
|
|
10726
|
+
{attributeGroups.map((group) => (
|
|
10727
|
+
<div key={group.name} className="flex flex-wrap items-center gap-1.5">
|
|
10728
|
+
<span className="text-muted-foreground text-xs">{group.name}:</span>
|
|
10729
|
+
{group.values.map((value) => {
|
|
10730
|
+
const isSelected = selectedAttrs[group.name] === value;
|
|
10731
|
+
// Check if this value leads to any available variant
|
|
10732
|
+
const variantForValue = variants?.find((v) => {
|
|
10733
|
+
const opts = getVariantOptions(v as any);
|
|
10734
|
+
const matchesValue = opts.some((o) => o.name === group.name && o.value === value);
|
|
10735
|
+
if (!matchesValue) return false;
|
|
10736
|
+
// Check other selected attrs
|
|
10737
|
+
return Object.entries(selectedAttrs).every(([k, sv]) => {
|
|
10738
|
+
if (k === group.name) return true;
|
|
10739
|
+
return opts.some((o) => o.name === k && o.value === sv);
|
|
10740
|
+
});
|
|
10741
|
+
});
|
|
10742
|
+
const isVariantOos =
|
|
10743
|
+
variantForValue?.inventory?.trackingMode !== 'NOT_TRACKED' &&
|
|
10744
|
+
variantForValue?.inventory?.available != null &&
|
|
10745
|
+
variantForValue.inventory.available <= 0;
|
|
10746
|
+
|
|
10747
|
+
return (
|
|
10748
|
+
<button
|
|
10749
|
+
key={value}
|
|
10750
|
+
type="button"
|
|
10751
|
+
onClick={() => handleAttrSelect(group.name, value)}
|
|
10752
|
+
disabled={isVariantOos}
|
|
10753
|
+
className={cn(
|
|
10754
|
+
'rounded-full border px-2.5 py-0.5 text-xs transition-colors',
|
|
10755
|
+
isSelected
|
|
10756
|
+
? 'border-primary bg-primary text-primary-foreground'
|
|
10757
|
+
: 'border-border text-foreground hover:border-primary/50',
|
|
10758
|
+
isVariantOos && 'cursor-not-allowed line-through opacity-40'
|
|
10759
|
+
)}
|
|
10760
|
+
>
|
|
10761
|
+
{value}
|
|
10762
|
+
</button>
|
|
10763
|
+
);
|
|
10764
|
+
})}
|
|
10765
|
+
</div>
|
|
10766
|
+
))}
|
|
10767
|
+
{isOos && effectiveVariantId && (
|
|
10768
|
+
<p className="text-destructive text-xs">{t('outOfStock') || 'Out of stock'}</p>
|
|
10183
10769
|
)}
|
|
10184
10770
|
</div>
|
|
10185
|
-
|
|
10186
|
-
</
|
|
10771
|
+
)}
|
|
10772
|
+
</div>
|
|
10187
10773
|
);
|
|
10188
10774
|
}
|
|
10189
10775
|
`,
|
|
@@ -10627,46 +11213,81 @@ async function handleGetRequiredPages(args) {
|
|
|
10627
11213
|
var import_zod5 = require("zod");
|
|
10628
11214
|
|
|
10629
11215
|
// src/utils/fetch-store-info.ts
|
|
10630
|
-
var
|
|
10631
|
-
|
|
10632
|
-
|
|
11216
|
+
var KNOWN_API_URLS = {
|
|
11217
|
+
production: "https://api.brainerce.com",
|
|
11218
|
+
staging: "https://api-staging.brainerce.com"
|
|
11219
|
+
};
|
|
11220
|
+
var ConnectionNotFoundError = class extends Error {
|
|
11221
|
+
constructor(connectionId, baseUrl) {
|
|
11222
|
+
super(`Connection "${connectionId}" not found at ${baseUrl}`);
|
|
11223
|
+
this.code = "NOT_FOUND";
|
|
11224
|
+
}
|
|
11225
|
+
};
|
|
11226
|
+
async function fetchStoreInfo(connectionId, baseUrl = KNOWN_API_URLS.production) {
|
|
10633
11227
|
const url = `${baseUrl}/api/vc/${connectionId}/info`;
|
|
10634
|
-
|
|
10635
|
-
|
|
10636
|
-
|
|
10637
|
-
|
|
10638
|
-
|
|
10639
|
-
|
|
10640
|
-
|
|
10641
|
-
|
|
10642
|
-
|
|
10643
|
-
|
|
10644
|
-
|
|
10645
|
-
|
|
10646
|
-
|
|
10647
|
-
|
|
10648
|
-
|
|
10649
|
-
|
|
10650
|
-
|
|
10651
|
-
|
|
10652
|
-
|
|
10653
|
-
|
|
10654
|
-
|
|
10655
|
-
|
|
10656
|
-
|
|
10657
|
-
|
|
10658
|
-
|
|
10659
|
-
|
|
10660
|
-
|
|
10661
|
-
|
|
10662
|
-
|
|
10663
|
-
|
|
10664
|
-
|
|
10665
|
-
|
|
10666
|
-
|
|
10667
|
-
|
|
10668
|
-
|
|
10669
|
-
|
|
11228
|
+
const controller = new AbortController();
|
|
11229
|
+
const timeout = setTimeout(() => controller.abort(), 1e4);
|
|
11230
|
+
let res;
|
|
11231
|
+
try {
|
|
11232
|
+
res = await fetch(url, { signal: controller.signal });
|
|
11233
|
+
} catch (err) {
|
|
11234
|
+
if (err.name === "AbortError") {
|
|
11235
|
+
throw new Error(`Request to ${baseUrl} timed out`);
|
|
11236
|
+
}
|
|
11237
|
+
throw new Error(`Failed to connect to ${baseUrl}: ${err.message}`);
|
|
11238
|
+
} finally {
|
|
11239
|
+
clearTimeout(timeout);
|
|
11240
|
+
}
|
|
11241
|
+
if (res.status === 404) {
|
|
11242
|
+
throw new ConnectionNotFoundError(connectionId, baseUrl);
|
|
11243
|
+
}
|
|
11244
|
+
if (!res.ok) {
|
|
11245
|
+
throw new Error(`${baseUrl} returned status ${res.status}`);
|
|
11246
|
+
}
|
|
11247
|
+
let json;
|
|
11248
|
+
try {
|
|
11249
|
+
json = await res.json();
|
|
11250
|
+
} catch {
|
|
11251
|
+
throw new Error(`Invalid response from ${baseUrl}`);
|
|
11252
|
+
}
|
|
11253
|
+
return {
|
|
11254
|
+
name: json.name || json.storeName || "My Store",
|
|
11255
|
+
currency: json.currency || "USD",
|
|
11256
|
+
language: json.language || "en",
|
|
11257
|
+
...json.i18n ? { i18n: json.i18n } : {}
|
|
11258
|
+
};
|
|
11259
|
+
}
|
|
11260
|
+
async function resolveStoreInfo(connectionId, candidateUrls) {
|
|
11261
|
+
const urls = candidateUrls.filter((u, i) => u && candidateUrls.indexOf(u) === i);
|
|
11262
|
+
if (urls.length === 0) {
|
|
11263
|
+
throw new Error("No API URLs to try");
|
|
11264
|
+
}
|
|
11265
|
+
let lastError;
|
|
11266
|
+
let allNotFound = true;
|
|
11267
|
+
for (let i = 0; i < urls.length; i++) {
|
|
11268
|
+
const baseUrl = urls[i];
|
|
11269
|
+
try {
|
|
11270
|
+
const info = await fetchStoreInfo(connectionId, baseUrl);
|
|
11271
|
+
return { info, apiBaseUrl: baseUrl, fellBack: i > 0 };
|
|
11272
|
+
} catch (err) {
|
|
11273
|
+
lastError = err;
|
|
11274
|
+
const isNotFound = err.code === "NOT_FOUND";
|
|
11275
|
+
if (!isNotFound) {
|
|
11276
|
+
allNotFound = false;
|
|
11277
|
+
}
|
|
11278
|
+
}
|
|
11279
|
+
}
|
|
11280
|
+
if (allNotFound) {
|
|
11281
|
+
throw new Error(
|
|
11282
|
+
`Connection "${connectionId}" not found in any known environment (${urls.join(", ")}).`
|
|
11283
|
+
);
|
|
11284
|
+
}
|
|
11285
|
+
throw lastError || new Error("Failed to resolve store info");
|
|
11286
|
+
}
|
|
11287
|
+
function getCandidateApiUrls() {
|
|
11288
|
+
const explicit = (process.env.BRAINERCE_API_URL || "").replace(/\/$/, "");
|
|
11289
|
+
if (explicit) return [explicit];
|
|
11290
|
+
return [KNOWN_API_URLS.production, KNOWN_API_URLS.staging];
|
|
10670
11291
|
}
|
|
10671
11292
|
|
|
10672
11293
|
// src/tools/get-store-info.ts
|
|
@@ -10677,17 +11298,18 @@ var GET_STORE_INFO_SCHEMA = {
|
|
|
10677
11298
|
};
|
|
10678
11299
|
async function handleGetStoreInfo(args) {
|
|
10679
11300
|
try {
|
|
10680
|
-
const
|
|
11301
|
+
const resolved = await resolveStoreInfo(args.connectionId, getCandidateApiUrls());
|
|
10681
11302
|
return {
|
|
10682
11303
|
content: [
|
|
10683
11304
|
{
|
|
10684
11305
|
type: "text",
|
|
10685
11306
|
text: JSON.stringify(
|
|
10686
11307
|
{
|
|
10687
|
-
name: info.name,
|
|
10688
|
-
currency: info.currency,
|
|
10689
|
-
language: info.language,
|
|
10690
|
-
connectionId: args.connectionId
|
|
11308
|
+
name: resolved.info.name,
|
|
11309
|
+
currency: resolved.info.currency,
|
|
11310
|
+
language: resolved.info.language,
|
|
11311
|
+
connectionId: args.connectionId,
|
|
11312
|
+
apiBaseUrl: resolved.apiBaseUrl
|
|
10691
11313
|
},
|
|
10692
11314
|
null,
|
|
10693
11315
|
2
|
|
@@ -10708,41 +11330,65 @@ async function handleGetStoreInfo(args) {
|
|
|
10708
11330
|
var import_zod6 = require("zod");
|
|
10709
11331
|
|
|
10710
11332
|
// src/utils/fetch-store-capabilities.ts
|
|
10711
|
-
var
|
|
10712
|
-
|
|
11333
|
+
var ConnectionNotFoundError2 = class extends Error {
|
|
11334
|
+
constructor(connectionId, baseUrl) {
|
|
11335
|
+
super(`Connection "${connectionId}" not found at ${baseUrl}`);
|
|
11336
|
+
this.code = "NOT_FOUND";
|
|
11337
|
+
}
|
|
11338
|
+
};
|
|
10713
11339
|
async function fetchStoreCapabilities(connectionId, baseUrl = "https://api.brainerce.com") {
|
|
10714
11340
|
const url = `${baseUrl}/api/vc/${connectionId}/capabilities`;
|
|
10715
|
-
|
|
10716
|
-
|
|
10717
|
-
|
|
10718
|
-
|
|
10719
|
-
|
|
10720
|
-
|
|
10721
|
-
|
|
10722
|
-
|
|
10723
|
-
|
|
10724
|
-
|
|
10725
|
-
|
|
10726
|
-
|
|
10727
|
-
|
|
10728
|
-
|
|
10729
|
-
|
|
10730
|
-
|
|
10731
|
-
|
|
10732
|
-
|
|
10733
|
-
|
|
10734
|
-
|
|
10735
|
-
|
|
10736
|
-
|
|
10737
|
-
});
|
|
10738
|
-
|
|
10739
|
-
|
|
10740
|
-
|
|
10741
|
-
|
|
10742
|
-
|
|
10743
|
-
|
|
10744
|
-
|
|
10745
|
-
|
|
11341
|
+
const controller = new AbortController();
|
|
11342
|
+
const timeout = setTimeout(() => controller.abort(), 1e4);
|
|
11343
|
+
let res;
|
|
11344
|
+
try {
|
|
11345
|
+
res = await fetch(url, { signal: controller.signal });
|
|
11346
|
+
} catch (err) {
|
|
11347
|
+
if (err.name === "AbortError") {
|
|
11348
|
+
throw new Error(`Request to ${baseUrl} timed out`);
|
|
11349
|
+
}
|
|
11350
|
+
throw new Error(`Failed to connect to ${baseUrl}: ${err.message}`);
|
|
11351
|
+
} finally {
|
|
11352
|
+
clearTimeout(timeout);
|
|
11353
|
+
}
|
|
11354
|
+
if (res.status === 404) {
|
|
11355
|
+
throw new ConnectionNotFoundError2(connectionId, baseUrl);
|
|
11356
|
+
}
|
|
11357
|
+
if (!res.ok) {
|
|
11358
|
+
throw new Error(`${baseUrl} returned status ${res.status}`);
|
|
11359
|
+
}
|
|
11360
|
+
try {
|
|
11361
|
+
return await res.json();
|
|
11362
|
+
} catch {
|
|
11363
|
+
throw new Error(`Invalid response from ${baseUrl}`);
|
|
11364
|
+
}
|
|
11365
|
+
}
|
|
11366
|
+
async function resolveStoreCapabilities(connectionId, candidateUrls) {
|
|
11367
|
+
const urls = candidateUrls.filter((u, i) => u && candidateUrls.indexOf(u) === i);
|
|
11368
|
+
if (urls.length === 0) {
|
|
11369
|
+
throw new Error("No API URLs to try");
|
|
11370
|
+
}
|
|
11371
|
+
let lastError;
|
|
11372
|
+
let allNotFound = true;
|
|
11373
|
+
for (let i = 0; i < urls.length; i++) {
|
|
11374
|
+
const baseUrl = urls[i];
|
|
11375
|
+
try {
|
|
11376
|
+
const capabilities = await fetchStoreCapabilities(connectionId, baseUrl);
|
|
11377
|
+
return { capabilities, apiBaseUrl: baseUrl, fellBack: i > 0 };
|
|
11378
|
+
} catch (err) {
|
|
11379
|
+
lastError = err;
|
|
11380
|
+
const isNotFound = err.code === "NOT_FOUND";
|
|
11381
|
+
if (!isNotFound) {
|
|
11382
|
+
allNotFound = false;
|
|
11383
|
+
}
|
|
11384
|
+
}
|
|
11385
|
+
}
|
|
11386
|
+
if (allNotFound) {
|
|
11387
|
+
throw new Error(
|
|
11388
|
+
`Connection "${connectionId}" not found in any known environment (${urls.join(", ")}).`
|
|
11389
|
+
);
|
|
11390
|
+
}
|
|
11391
|
+
throw lastError || new Error("Failed to resolve store capabilities");
|
|
10746
11392
|
}
|
|
10747
11393
|
|
|
10748
11394
|
// src/tools/get-store-capabilities.ts
|
|
@@ -10755,6 +11401,11 @@ function formatCapabilities(caps) {
|
|
|
10755
11401
|
const lines = [];
|
|
10756
11402
|
lines.push(`## Store: ${caps.store.name} (${caps.store.currency})`);
|
|
10757
11403
|
lines.push(`Language: ${caps.store.language}`);
|
|
11404
|
+
if (caps.store.i18n?.enabled) {
|
|
11405
|
+
lines.push(
|
|
11406
|
+
`Multi-language: ENABLED \u2014 locales: ${caps.store.i18n.supportedLocales.join(", ")} (default: ${caps.store.i18n.defaultLocale})`
|
|
11407
|
+
);
|
|
11408
|
+
}
|
|
10758
11409
|
lines.push("");
|
|
10759
11410
|
lines.push("## Configured Features");
|
|
10760
11411
|
if (caps.features.paymentProviders.length > 0) {
|
|
@@ -10841,6 +11492,11 @@ function formatCapabilities(caps) {
|
|
|
10841
11492
|
'Inventory reservation is active \u2014 show a countdown timer. Use get-code-example("reservation-countdown").'
|
|
10842
11493
|
);
|
|
10843
11494
|
}
|
|
11495
|
+
if (caps.store.i18n?.enabled && caps.store.i18n.supportedLocales.length > 1) {
|
|
11496
|
+
suggestions.push(
|
|
11497
|
+
`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.`
|
|
11498
|
+
);
|
|
11499
|
+
}
|
|
10844
11500
|
if (suggestions.length === 0) {
|
|
10845
11501
|
suggestions.push(
|
|
10846
11502
|
"Start by building the required pages. Use get-required-pages() for the checklist."
|
|
@@ -10851,9 +11507,9 @@ function formatCapabilities(caps) {
|
|
|
10851
11507
|
}
|
|
10852
11508
|
async function handleGetStoreCapabilities(args) {
|
|
10853
11509
|
try {
|
|
10854
|
-
const
|
|
11510
|
+
const resolved = await resolveStoreCapabilities(args.connectionId, getCandidateApiUrls());
|
|
10855
11511
|
return {
|
|
10856
|
-
content: [{ type: "text", text: formatCapabilities(
|
|
11512
|
+
content: [{ type: "text", text: formatCapabilities(resolved.capabilities) }]
|
|
10857
11513
|
};
|
|
10858
11514
|
} catch (error) {
|
|
10859
11515
|
const message = error instanceof Error ? error.message : "Failed to fetch store capabilities";
|
|
@@ -10948,6 +11604,20 @@ function formatCapabilitiesSummary(caps) {
|
|
|
10948
11604
|
const lines = [];
|
|
10949
11605
|
lines.push(`## Store: ${caps.store.name}`);
|
|
10950
11606
|
lines.push(`Currency: ${caps.store.currency} | Language: ${caps.store.language}`);
|
|
11607
|
+
if (caps.store.i18n?.enabled && caps.store.i18n.supportedLocales.length > 1) {
|
|
11608
|
+
lines.push(
|
|
11609
|
+
`Multi-language: ${caps.store.i18n.supportedLocales.join(", ")} (default: ${caps.store.i18n.defaultLocale})`
|
|
11610
|
+
);
|
|
11611
|
+
lines.push("");
|
|
11612
|
+
lines.push("### i18n Implementation");
|
|
11613
|
+
lines.push(
|
|
11614
|
+
"- Call `client.setLocale(locale)` at app init based on URL prefix or user preference"
|
|
11615
|
+
);
|
|
11616
|
+
lines.push("- All getProducts/getCategories/getBrands calls auto-include the locale");
|
|
11617
|
+
lines.push("- Use locale-prefixed routes: `/[locale]/products`, `/[locale]/products/[slug]`");
|
|
11618
|
+
lines.push("- Add a language switcher component in the header");
|
|
11619
|
+
lines.push('- RTL locales (he, ar): set `<html dir="rtl">` \u2014 flexbox reversal is automatic');
|
|
11620
|
+
}
|
|
10951
11621
|
lines.push("");
|
|
10952
11622
|
lines.push("### Configured Features");
|
|
10953
11623
|
if (caps.features.paymentProviders.length > 0) {
|
|
@@ -11001,8 +11671,7 @@ function buildStoreBundle(options) {
|
|
|
11001
11671
|
connectionId,
|
|
11002
11672
|
storeName,
|
|
11003
11673
|
currency,
|
|
11004
|
-
language: capabilities?.store.language || "en"
|
|
11005
|
-
apiUrl: "https://api.brainerce.com"
|
|
11674
|
+
language: capabilities?.store.language || "en"
|
|
11006
11675
|
};
|
|
11007
11676
|
const sections = [];
|
|
11008
11677
|
sections.push(`# Build: ${storeName} \u2014 ${storeType}${styleDesc}
|
|
@@ -11105,7 +11774,8 @@ var BUILD_STORE_SCHEMA = {
|
|
|
11105
11774
|
async function handleBuildStore(args) {
|
|
11106
11775
|
let capabilities = null;
|
|
11107
11776
|
try {
|
|
11108
|
-
|
|
11777
|
+
const resolved = await resolveStoreCapabilities(args.connectionId, getCandidateApiUrls());
|
|
11778
|
+
capabilities = resolved.capabilities;
|
|
11109
11779
|
} catch {
|
|
11110
11780
|
}
|
|
11111
11781
|
const bundle = buildStoreBundle({
|
|
@@ -11444,6 +12114,57 @@ ${results.join("\n\n---\n\n")}`
|
|
|
11444
12114
|
};
|
|
11445
12115
|
}
|
|
11446
12116
|
|
|
12117
|
+
// src/tools/get-integration-guide.ts
|
|
12118
|
+
var import_zod10 = require("zod");
|
|
12119
|
+
var GET_INTEGRATION_GUIDE_NAME = "get-integration-guide";
|
|
12120
|
+
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.';
|
|
12121
|
+
var GET_INTEGRATION_GUIDE_SCHEMA = {
|
|
12122
|
+
part: import_zod10.z.enum(["core", "optional", "rules"]).describe(
|
|
12123
|
+
'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.'
|
|
12124
|
+
)
|
|
12125
|
+
};
|
|
12126
|
+
var GUIDE_URLS = {
|
|
12127
|
+
core: "https://brainerce.com/docs/integration/raw?part=core",
|
|
12128
|
+
optional: "https://brainerce.com/docs/integration/raw?part=optional",
|
|
12129
|
+
rules: "https://brainerce.com/docs/integration/raw?part=rules"
|
|
12130
|
+
};
|
|
12131
|
+
async function handleGetIntegrationGuide(args) {
|
|
12132
|
+
const url = GUIDE_URLS[args.part];
|
|
12133
|
+
if (!url) {
|
|
12134
|
+
return {
|
|
12135
|
+
content: [
|
|
12136
|
+
{
|
|
12137
|
+
type: "text",
|
|
12138
|
+
text: `Unknown part: "${args.part}". Available parts: core, optional, rules.`
|
|
12139
|
+
}
|
|
12140
|
+
]
|
|
12141
|
+
};
|
|
12142
|
+
}
|
|
12143
|
+
try {
|
|
12144
|
+
const response = await fetch(url, {
|
|
12145
|
+
headers: { Accept: "text/plain" },
|
|
12146
|
+
signal: AbortSignal.timeout(15e3)
|
|
12147
|
+
});
|
|
12148
|
+
if (!response.ok) {
|
|
12149
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
12150
|
+
}
|
|
12151
|
+
const content = await response.text();
|
|
12152
|
+
return {
|
|
12153
|
+
content: [{ type: "text", text: content }]
|
|
12154
|
+
};
|
|
12155
|
+
} catch (error) {
|
|
12156
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
12157
|
+
return {
|
|
12158
|
+
content: [
|
|
12159
|
+
{
|
|
12160
|
+
type: "text",
|
|
12161
|
+
text: `Failed to fetch integration guide (${args.part}): ${message}. You can read it directly at: ${url}`
|
|
12162
|
+
}
|
|
12163
|
+
]
|
|
12164
|
+
};
|
|
12165
|
+
}
|
|
12166
|
+
}
|
|
12167
|
+
|
|
11447
12168
|
// src/resources/sdk-types.ts
|
|
11448
12169
|
var SDK_TYPES_URI = "brainerce://sdk/types";
|
|
11449
12170
|
var SDK_TYPES_NAME = "Brainerce SDK Types";
|
|
@@ -11598,15 +12319,15 @@ async function handleProjectTemplate(uri) {
|
|
|
11598
12319
|
}
|
|
11599
12320
|
|
|
11600
12321
|
// src/prompts/create-store.ts
|
|
11601
|
-
var
|
|
12322
|
+
var import_zod11 = require("zod");
|
|
11602
12323
|
var CREATE_STORE_NAME = "create-store";
|
|
11603
12324
|
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.";
|
|
11604
12325
|
var CREATE_STORE_SCHEMA = {
|
|
11605
|
-
connectionId:
|
|
11606
|
-
storeName:
|
|
11607
|
-
storeType:
|
|
11608
|
-
currency:
|
|
11609
|
-
storeStyle:
|
|
12326
|
+
connectionId: import_zod11.z.string().describe("Vibe-coded connection ID (starts with vc_)"),
|
|
12327
|
+
storeName: import_zod11.z.string().describe('Name of the store (e.g., "Urban Threads")'),
|
|
12328
|
+
storeType: import_zod11.z.string().describe('Type of store (e.g., "Clothing store", "Electronics shop", "Bakery")'),
|
|
12329
|
+
currency: import_zod11.z.string().optional().default("USD").describe("Store currency code (e.g., USD, ILS, EUR)"),
|
|
12330
|
+
storeStyle: import_zod11.z.string().optional().describe('Design style (e.g., "minimalist", "luxury", "playful", "dark mode")')
|
|
11610
12331
|
};
|
|
11611
12332
|
function handleCreateStore(args) {
|
|
11612
12333
|
const styleDesc = args.storeStyle ? ` with a ${args.storeStyle} design style` : "";
|
|
@@ -11645,6 +12366,13 @@ DO NOT STOP until all 13 pages + header are implemented:
|
|
|
11645
12366
|
13. \`/account\` \u2014 Account dashboard
|
|
11646
12367
|
14. Header component with cart count + search
|
|
11647
12368
|
|
|
12369
|
+
## Multi-Language
|
|
12370
|
+
If the store has multi-language enabled (check build-store output), implement:
|
|
12371
|
+
- Locale-prefixed routes: /[locale]/products instead of /products
|
|
12372
|
+
- Call client.setLocale(locale) based on URL prefix
|
|
12373
|
+
- Language switcher in header
|
|
12374
|
+
- dir="rtl" on <html> for RTL locales (he, ar)
|
|
12375
|
+
|
|
11648
12376
|
## Step 3: Validate
|
|
11649
12377
|
After building all pages, call \`validate-store\` with the list of files you created to verify nothing is missing.
|
|
11650
12378
|
|
|
@@ -11663,11 +12391,11 @@ After building all pages, call \`validate-store\` with the list of files you cre
|
|
|
11663
12391
|
}
|
|
11664
12392
|
|
|
11665
12393
|
// src/prompts/add-feature.ts
|
|
11666
|
-
var
|
|
12394
|
+
var import_zod12 = require("zod");
|
|
11667
12395
|
var ADD_FEATURE_NAME = "add-feature";
|
|
11668
12396
|
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.";
|
|
11669
12397
|
var ADD_FEATURE_SCHEMA = {
|
|
11670
|
-
feature:
|
|
12398
|
+
feature: import_zod12.z.enum([
|
|
11671
12399
|
"products",
|
|
11672
12400
|
"cart",
|
|
11673
12401
|
"checkout",
|
|
@@ -11680,8 +12408,8 @@ var ADD_FEATURE_SCHEMA = {
|
|
|
11680
12408
|
"search",
|
|
11681
12409
|
"tax"
|
|
11682
12410
|
]).describe("The feature to add"),
|
|
11683
|
-
connectionId:
|
|
11684
|
-
currency:
|
|
12411
|
+
connectionId: import_zod12.z.string().optional().describe("Vibe-coded connection ID (starts with vc_)"),
|
|
12412
|
+
currency: import_zod12.z.string().optional().default("USD").describe("Store currency code")
|
|
11685
12413
|
};
|
|
11686
12414
|
var FEATURE_TO_TOPICS = {
|
|
11687
12415
|
products: { topics: ["products"], domains: ["products"] },
|
|
@@ -11796,6 +12524,12 @@ function createServer() {
|
|
|
11796
12524
|
GET_PAGE_CODE_SCHEMA,
|
|
11797
12525
|
handleGetPageCode
|
|
11798
12526
|
);
|
|
12527
|
+
server.tool(
|
|
12528
|
+
GET_INTEGRATION_GUIDE_NAME,
|
|
12529
|
+
GET_INTEGRATION_GUIDE_DESCRIPTION,
|
|
12530
|
+
GET_INTEGRATION_GUIDE_SCHEMA,
|
|
12531
|
+
handleGetIntegrationGuide
|
|
12532
|
+
);
|
|
11799
12533
|
server.resource(
|
|
11800
12534
|
SDK_TYPES_NAME,
|
|
11801
12535
|
SDK_TYPES_URI,
|