@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.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __create = Object.create;
|
|
3
2
|
var __defProp = Object.defineProperty;
|
|
4
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
7
5
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
6
|
var __export = (target, all) => {
|
|
9
7
|
for (var name in all)
|
|
@@ -17,14 +15,6 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
17
15
|
}
|
|
18
16
|
return to;
|
|
19
17
|
};
|
|
20
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
-
mod
|
|
27
|
-
));
|
|
28
18
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
19
|
|
|
30
20
|
// src/index.ts
|
|
@@ -197,7 +187,7 @@ import {
|
|
|
197
187
|
getVariantPrice, getVariantOptions, getStockStatus,
|
|
198
188
|
getCartItemName, getCartItemImage,
|
|
199
189
|
getDescriptionContent, isHtmlDescription,
|
|
200
|
-
getProductMetafieldValue,
|
|
190
|
+
getProductMetafieldValue, getProductCustomizationFields,
|
|
201
191
|
} from 'brainerce';
|
|
202
192
|
\`\`\``;
|
|
203
193
|
}
|
|
@@ -239,6 +229,7 @@ async function startCheckout() {
|
|
|
239
229
|
4. Start checkout session (\`startGuestCheckout()\` or \`createCheckout()\`)
|
|
240
230
|
5. Set shipping address (includes required email) \u2192 \`setShippingAddress()\`
|
|
241
231
|
6. Select shipping method \u2192 \`selectShippingMethod()\`
|
|
232
|
+
6b. (Optional) Set checkout custom fields \u2192 \`setCheckoutCustomFields()\` \u2014 surcharges auto-calculated
|
|
242
233
|
7. Create payment intent \u2192 \`createPaymentIntent()\` \u2192 returns \`{ clientSecret, provider }\`
|
|
243
234
|
8. Branch on \`provider\`: \`'stripe'\` \u2192 Stripe Elements, \`'grow'\` \u2192 iframe, \`'paypal'\` \u2192 PayPal Buttons
|
|
244
235
|
9. **Order is created AUTOMATICALLY after payment succeeds (via webhook) \u2014 for ALL providers!**
|
|
@@ -329,6 +320,20 @@ function CheckoutPage() {
|
|
|
329
320
|
if (!paymentData) return <div>Unable to initialize payment</div>;
|
|
330
321
|
|
|
331
322
|
// Render payment UI based on provider
|
|
323
|
+
if (paymentData.provider === 'sandbox') {
|
|
324
|
+
return (
|
|
325
|
+
<div className="text-center p-6 bg-amber-50 border border-amber-200 rounded-lg">
|
|
326
|
+
<h3 className="font-semibold mb-2">Test Mode</h3>
|
|
327
|
+
<p className="text-sm text-gray-600 mb-4">No real payment will be charged.</p>
|
|
328
|
+
<button onClick={async () => {
|
|
329
|
+
await client.completeGuestCheckout(checkoutId);
|
|
330
|
+
window.location.href = \\\`/order-confirmation?checkout_id=\\\${checkoutId}\\\`;
|
|
331
|
+
}} className="bg-amber-500 text-white px-6 py-2 rounded">
|
|
332
|
+
Complete Test Order
|
|
333
|
+
</button>
|
|
334
|
+
</div>
|
|
335
|
+
);
|
|
336
|
+
}
|
|
332
337
|
if (paymentData.provider === 'grow') {
|
|
333
338
|
return <GrowPaymentForm paymentUrl={paymentData.clientSecret} checkoutId={paymentData.checkoutId} />;
|
|
334
339
|
}
|
|
@@ -447,11 +452,12 @@ const growProvider = providers.find(p => p.provider === 'grow');
|
|
|
447
452
|
const paypalProvider = providers.find(p => p.provider === 'paypal');
|
|
448
453
|
\`\`\`
|
|
449
454
|
|
|
450
|
-
Each provider has: \`id\`, \`provider\` ('stripe'|'grow'|'paypal'), \`name\`, \`publicKey\`, \`stripeAccountId\` (Stripe only), \`supportedMethods\`, \`testMode\`, \`isDefault\`.
|
|
455
|
+
Each provider has: \`id\`, \`provider\` ('stripe'|'grow'|'paypal'|'sandbox'), \`name\`, \`publicKey\`, \`stripeAccountId\` (Stripe only), \`supportedMethods\`, \`testMode\`, \`isDefault\`.
|
|
451
456
|
|
|
452
457
|
- **Stripe:** \`npm install @stripe/stripe-js @stripe/react-stripe-js\` \u2014 \`loadStripe(publicKey, { stripeAccount })\`
|
|
453
458
|
- **Grow:** No SDK \u2014 uses iframe with payment URL. Supports credit cards, Bit, Apple Pay, Google Pay.
|
|
454
|
-
- **PayPal:** \`npm install @paypal/react-paypal-js\` \u2014 \`PayPalScriptProvider\` + \`PayPalButtons
|
|
459
|
+
- **PayPal:** \`npm install @paypal/react-paypal-js\` \u2014 \`PayPalScriptProvider\` + \`PayPalButtons\`
|
|
460
|
+
- **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\`.`;
|
|
455
461
|
}
|
|
456
462
|
function getProductsSection(_currency) {
|
|
457
463
|
return `## Products & Variants
|
|
@@ -606,6 +612,35 @@ const material = getProductMetafieldValue(product, 'material');
|
|
|
606
612
|
|
|
607
613
|
**Metafield fields:** \`definitionName\` (display label), \`definitionKey\` (lookup key), \`value\`, \`type\` (IMAGE, GALLERY, URL, COLOR, BOOLEAN, DATE, DATETIME, TEXT, TEXTAREA, NUMBER, DIMENSION, WEIGHT, JSON)
|
|
608
614
|
|
|
615
|
+
### Product Customization Fields (Customer Input)
|
|
616
|
+
|
|
617
|
+
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.
|
|
618
|
+
|
|
619
|
+
\`\`\`typescript
|
|
620
|
+
import { getProductCustomizationFields } from 'brainerce';
|
|
621
|
+
import type { ProductCustomizationField } from 'brainerce';
|
|
622
|
+
|
|
623
|
+
const fields = getProductCustomizationFields(product);
|
|
624
|
+
|
|
625
|
+
// Render input for each field based on field.type:
|
|
626
|
+
// TEXT/TEXTAREA \u2192 text input, NUMBER \u2192 number input, BOOLEAN \u2192 checkbox,
|
|
627
|
+
// COLOR \u2192 color picker, DATE \u2192 date picker, IMAGE \u2192 file upload, etc.
|
|
628
|
+
// Check field.required, field.minLength, field.maxLength, field.enumValues for validation.
|
|
629
|
+
|
|
630
|
+
// Pass customer values in metadata when adding to cart:
|
|
631
|
+
await client.addToCart(cartId, {
|
|
632
|
+
productId: product.id,
|
|
633
|
+
quantity: 1,
|
|
634
|
+
metadata: { cake_text: 'Happy Birthday!' },
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
// For IMAGE fields, upload first:
|
|
638
|
+
const { url } = await client.uploadCustomizationFile(file);
|
|
639
|
+
// Then use the URL in metadata: metadata: { logo: url }
|
|
640
|
+
\`\`\`
|
|
641
|
+
|
|
642
|
+
**Customization field properties:** \`key\`, \`name\` (display label), \`type\`, \`required\`, \`minLength\`, \`maxLength\`, \`minValue\`, \`maxValue\`, \`enumValues\`, \`defaultValue\`, \`position\`
|
|
643
|
+
|
|
609
644
|
### Downloadable / Digital Products
|
|
610
645
|
|
|
611
646
|
Products with \`isDownloadable: true\` are digital products. Show a digital badge instead of stock badge, list included files, and note that download links appear after purchase.
|
|
@@ -792,9 +827,14 @@ const { bundles } = await client.getCartBundles(cartId);
|
|
|
792
827
|
// bundles[].bundleProduct \u2014 the product to offer
|
|
793
828
|
// bundles[].originalPrice / bundles[].discountedPrice \u2014 prices
|
|
794
829
|
// bundles[].discountType ('PERCENTAGE' | 'FIXED_AMOUNT') + discountValue
|
|
830
|
+
// bundles[].requiresVariantSelection \u2014 true if customer must pick a variant
|
|
831
|
+
// bundles[].lockedVariant \u2014 set if admin pre-selected a specific variant
|
|
832
|
+
// bundles[].bundleProduct.variants \u2014 available variants (only when requiresVariantSelection)
|
|
795
833
|
|
|
796
834
|
// Add a bundle to cart (applies the discount automatically):
|
|
797
835
|
await client.addBundleToCart(cartId, bundleOfferId);
|
|
836
|
+
// For products with variants \u2014 pass the selected variantId:
|
|
837
|
+
await client.addBundleToCart(cartId, bundleOfferId, selectedVariantId);
|
|
798
838
|
// Remove a bundle from cart:
|
|
799
839
|
await client.removeBundleFromCart(cartId, bundleOfferId);
|
|
800
840
|
// Detect already-added bundles: check cart.items for metadata?.isBundleItem === true
|
|
@@ -819,9 +859,14 @@ const { bumps } = await client.getCheckoutBumps(checkoutId);
|
|
|
819
859
|
// bumps[].bumpProduct \u2014 product to offer
|
|
820
860
|
// bumps[].originalPrice / discountedPrice \u2014 with optional discount
|
|
821
861
|
// bumps[].title / description \u2014 display text
|
|
862
|
+
// bumps[].requiresVariantSelection \u2014 true if customer must pick a variant
|
|
863
|
+
// bumps[].lockedVariant \u2014 set if admin pre-selected a specific variant
|
|
864
|
+
// bumps[].bumpProduct.variants \u2014 available variants (only when requiresVariantSelection)
|
|
822
865
|
|
|
823
866
|
// Add a bump to cart:
|
|
824
867
|
await client.addOrderBump(cartId, bumpId);
|
|
868
|
+
// For products with variants \u2014 pass the selected variantId:
|
|
869
|
+
await client.addOrderBump(cartId, bumpId, selectedVariantId);
|
|
825
870
|
// Remove a bump:
|
|
826
871
|
await client.removeOrderBump(cartId, bumpId);
|
|
827
872
|
// Detect already-added bumps: check cart.items for metadata?.isOrderBump === true
|
|
@@ -846,10 +891,11 @@ Always check the flag before rendering: \`if (storeInfo?.upsell?.featureName !==
|
|
|
846
891
|
| CartRecommendationSection | products/ | Cross-sell grid at bottom of cart page |
|
|
847
892
|
| FreeShippingBar | cart/ | Progress bar toward free shipping threshold |
|
|
848
893
|
| CartUpgradeBanner | cart/ | Inline "Upgrade to X for +$Y" per cart item |
|
|
849
|
-
| CartBundleOfferCard | cart/ | Discounted bundle offer card in cart |
|
|
850
|
-
| OrderBumpCard | checkout/ | Checkbox add-on card in checkout sidebar |
|
|
894
|
+
| CartBundleOfferCard | cart/ | Discounted bundle offer card in cart (with inline variant selector for variable products) |
|
|
895
|
+
| OrderBumpCard | checkout/ | Checkbox add-on card in checkout sidebar (with inline variant selector for variable products) |
|
|
851
896
|
|
|
852
|
-
ProductRecommendation: \`id\`, \`name\`, \`slug\`, \`basePrice\`, \`salePrice\`, \`images\`, \`type\`, \`inventory\`, \`relationType
|
|
897
|
+
ProductRecommendation: \`id\`, \`name\`, \`slug\`, \`basePrice\`, \`salePrice\`, \`images\`, \`type\`, \`inventory\`, \`relationType\`, \`variants?\` (when variant selection needed).
|
|
898
|
+
OrderBump/CartBundleOffer: includes \`requiresVariantSelection\` (boolean) and \`lockedVariant?\` (\`{ id, name, attributes }\`).`;
|
|
853
899
|
}
|
|
854
900
|
function getInventorySection() {
|
|
855
901
|
return `## Inventory, Stock Display & Reservation Countdown
|
|
@@ -1989,6 +2035,15 @@ export function getClient(): BrainerceClient {
|
|
|
1989
2035
|
}
|
|
1990
2036
|
return clientInstance;
|
|
1991
2037
|
}
|
|
2038
|
+
<% if (i18nEnabled) { %>
|
|
2039
|
+
|
|
2040
|
+
/** Initialize client with a specific locale for translated content */
|
|
2041
|
+
export function initClientWithLocale(locale: string): BrainerceClient {
|
|
2042
|
+
const client = getClient();
|
|
2043
|
+
client.setLocale(locale);
|
|
2044
|
+
return client;
|
|
2045
|
+
}
|
|
2046
|
+
<% } %>
|
|
1992
2047
|
|
|
1993
2048
|
// Cart ID helpers (not a security token \u2014 safe in localStorage)
|
|
1994
2049
|
const CART_ID_KEY = 'brainerce_cart_id';
|
|
@@ -2187,6 +2242,10 @@ import type { StoreInfo, Cart, CustomerProfile } from 'brainerce';
|
|
|
2187
2242
|
import { getCartTotals } from 'brainerce';
|
|
2188
2243
|
import { getClient, initClient, setStoredCartId } from '@/lib/brainerce';
|
|
2189
2244
|
import { checkAuthStatus, proxyLogout } from '@/lib/auth';
|
|
2245
|
+
<% if (i18nEnabled) { %>
|
|
2246
|
+
import { MessagesContext } from '@/lib/translations';
|
|
2247
|
+
import { getMessages, defaultLocale } from '@/i18n';
|
|
2248
|
+
<% } %>
|
|
2190
2249
|
|
|
2191
2250
|
// ---- Store Info Context ----
|
|
2192
2251
|
interface StoreInfoContextValue {
|
|
@@ -2246,7 +2305,11 @@ export function useCart() {
|
|
|
2246
2305
|
}
|
|
2247
2306
|
|
|
2248
2307
|
// ---- Provider Component ----
|
|
2308
|
+
<% if (i18nEnabled) { %>
|
|
2309
|
+
export function StoreProvider({ children, locale }: { children: React.ReactNode; locale?: string }) {
|
|
2310
|
+
<% } else { %>
|
|
2249
2311
|
export function StoreProvider({ children }: { children: React.ReactNode }) {
|
|
2312
|
+
<% } %>
|
|
2250
2313
|
const [storeInfo, setStoreInfo] = useState<StoreInfo | null>(null);
|
|
2251
2314
|
const [storeLoading, setStoreLoading] = useState(true);
|
|
2252
2315
|
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
|
@@ -2254,6 +2317,9 @@ export function StoreProvider({ children }: { children: React.ReactNode }) {
|
|
|
2254
2317
|
const [authLoading, setAuthLoading] = useState(true);
|
|
2255
2318
|
const [cart, setCart] = useState<Cart | null>(null);
|
|
2256
2319
|
const [cartLoading, setCartLoading] = useState(true);
|
|
2320
|
+
<% if (i18nEnabled) { %>
|
|
2321
|
+
const [messages, setMessages] = useState<Record<string, Record<string, string>>>({});
|
|
2322
|
+
<% } %>
|
|
2257
2323
|
|
|
2258
2324
|
// Check auth status via httpOnly cookie (server-side validation)
|
|
2259
2325
|
const refreshAuth = useCallback(async () => {
|
|
@@ -2272,6 +2338,16 @@ export function StoreProvider({ children }: { children: React.ReactNode }) {
|
|
|
2272
2338
|
// Initialize client, check auth, and fetch store info
|
|
2273
2339
|
useEffect(() => {
|
|
2274
2340
|
const client = initClient();
|
|
2341
|
+
<% if (i18nEnabled) { %>
|
|
2342
|
+
|
|
2343
|
+
// Set locale on SDK client for translated product content
|
|
2344
|
+
if (locale) {
|
|
2345
|
+
client.setLocale(locale);
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
// Load UI message strings for current locale
|
|
2349
|
+
getMessages(locale || defaultLocale).then(setMessages);
|
|
2350
|
+
<% } %>
|
|
2275
2351
|
|
|
2276
2352
|
// Optimistic check: if brainerce_logged_in cookie exists, assume logged in
|
|
2277
2353
|
// while we validate the actual token server-side
|
|
@@ -2288,7 +2364,11 @@ export function StoreProvider({ children }: { children: React.ReactNode }) {
|
|
|
2288
2364
|
.then(setStoreInfo)
|
|
2289
2365
|
.catch(console.error)
|
|
2290
2366
|
.finally(() => setStoreLoading(false));
|
|
2367
|
+
<% if (i18nEnabled) { %>
|
|
2368
|
+
}, [refreshAuth, locale]);
|
|
2369
|
+
<% } else { %>
|
|
2291
2370
|
}, [refreshAuth]);
|
|
2371
|
+
<% } %>
|
|
2292
2372
|
|
|
2293
2373
|
// Cart management
|
|
2294
2374
|
const refreshCart = useCallback(async () => {
|
|
@@ -2341,6 +2421,21 @@ export function StoreProvider({ children }: { children: React.ReactNode }) {
|
|
|
2341
2421
|
|
|
2342
2422
|
const totals = cart ? getCartTotals(cart) : { subtotal: 0, discount: 0, shipping: 0, total: 0 };
|
|
2343
2423
|
|
|
2424
|
+
<% if (i18nEnabled) { %>
|
|
2425
|
+
return (
|
|
2426
|
+
<MessagesContext.Provider value={messages}>
|
|
2427
|
+
<StoreInfoContext.Provider value={{ storeInfo, loading: storeLoading }}>
|
|
2428
|
+
<AuthContext.Provider value={{ isLoggedIn, authLoading, customer, login, logout }}>
|
|
2429
|
+
<CartContext.Provider
|
|
2430
|
+
value={{ cart, cartLoading, refreshCart, itemCount, totals }}
|
|
2431
|
+
>
|
|
2432
|
+
{children}
|
|
2433
|
+
</CartContext.Provider>
|
|
2434
|
+
</AuthContext.Provider>
|
|
2435
|
+
</StoreInfoContext.Provider>
|
|
2436
|
+
</MessagesContext.Provider>
|
|
2437
|
+
);
|
|
2438
|
+
<% } else { %>
|
|
2344
2439
|
return (
|
|
2345
2440
|
<StoreInfoContext.Provider value={{ storeInfo, loading: storeLoading }}>
|
|
2346
2441
|
<AuthContext.Provider value={{ isLoggedIn, authLoading, customer, login, logout }}>
|
|
@@ -2352,6 +2447,7 @@ export function StoreProvider({ children }: { children: React.ReactNode }) {
|
|
|
2352
2447
|
</AuthContext.Provider>
|
|
2353
2448
|
</StoreInfoContext.Provider>
|
|
2354
2449
|
);
|
|
2450
|
+
<% } %>
|
|
2355
2451
|
}
|
|
2356
2452
|
`,
|
|
2357
2453
|
"src/app/page.tsx": `'use client';
|
|
@@ -2453,7 +2549,81 @@ export default function HomePage() {
|
|
|
2453
2549
|
);
|
|
2454
2550
|
}
|
|
2455
2551
|
`,
|
|
2456
|
-
"src/app/layout.tsx.ejs":
|
|
2552
|
+
"src/app/layout.tsx.ejs": `<% if (i18nEnabled) { %>
|
|
2553
|
+
import type { Metadata } from 'next';
|
|
2554
|
+
<%- fontImport %>
|
|
2555
|
+
import { StoreProvider } from '@/providers/store-provider';
|
|
2556
|
+
import { Header } from '@/components/layout/header';
|
|
2557
|
+
import { Footer } from '@/components/layout/footer';
|
|
2558
|
+
import { getDirection, supportedLocales } from '@/i18n';
|
|
2559
|
+
import '../globals.css';
|
|
2560
|
+
|
|
2561
|
+
<%- fontVariable %>
|
|
2562
|
+
|
|
2563
|
+
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com';
|
|
2564
|
+
|
|
2565
|
+
export const metadata: Metadata = {
|
|
2566
|
+
metadataBase: new URL(baseUrl),
|
|
2567
|
+
title: {
|
|
2568
|
+
default: 'My Store',
|
|
2569
|
+
template: \`%s | My Store\`,
|
|
2570
|
+
},
|
|
2571
|
+
description: 'My Store',
|
|
2572
|
+
alternates: {
|
|
2573
|
+
canonical: '/',
|
|
2574
|
+
},
|
|
2575
|
+
openGraph: {
|
|
2576
|
+
siteName: 'My Store',
|
|
2577
|
+
type: 'website',
|
|
2578
|
+
},
|
|
2579
|
+
robots: {
|
|
2580
|
+
index: true,
|
|
2581
|
+
follow: true,
|
|
2582
|
+
},
|
|
2583
|
+
};
|
|
2584
|
+
|
|
2585
|
+
const organizationJsonLd = {
|
|
2586
|
+
'@context': 'https://schema.org',
|
|
2587
|
+
'@type': 'Organization',
|
|
2588
|
+
name: 'My Store',
|
|
2589
|
+
url: baseUrl,
|
|
2590
|
+
};
|
|
2591
|
+
|
|
2592
|
+
export function generateStaticParams() {
|
|
2593
|
+
return supportedLocales.map((locale) => ({ locale }));
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
export default function RootLayout({
|
|
2597
|
+
children,
|
|
2598
|
+
params,
|
|
2599
|
+
}: {
|
|
2600
|
+
children: React.ReactNode;
|
|
2601
|
+
params: { locale: string };
|
|
2602
|
+
}) {
|
|
2603
|
+
const dir = getDirection(params.locale);
|
|
2604
|
+
|
|
2605
|
+
return (
|
|
2606
|
+
<html lang={params.locale} dir={dir}>
|
|
2607
|
+
<head>
|
|
2608
|
+
<script
|
|
2609
|
+
type="application/ld+json"
|
|
2610
|
+
dangerouslySetInnerHTML={{ __html: JSON.stringify(organizationJsonLd) }}
|
|
2611
|
+
/>
|
|
2612
|
+
</head>
|
|
2613
|
+
<body className={font.className}>
|
|
2614
|
+
<StoreProvider locale={params.locale}>
|
|
2615
|
+
<div className="min-h-screen flex flex-col">
|
|
2616
|
+
<Header />
|
|
2617
|
+
<main className="flex-1">{children}</main>
|
|
2618
|
+
<Footer />
|
|
2619
|
+
</div>
|
|
2620
|
+
</StoreProvider>
|
|
2621
|
+
</body>
|
|
2622
|
+
</html>
|
|
2623
|
+
);
|
|
2624
|
+
}
|
|
2625
|
+
<% } else { %>
|
|
2626
|
+
import type { Metadata } from 'next';
|
|
2457
2627
|
<%- fontImport %>
|
|
2458
2628
|
import { StoreProvider } from '@/providers/store-provider';
|
|
2459
2629
|
import { Header } from '@/components/layout/header';
|
|
@@ -2517,6 +2687,7 @@ export default function RootLayout({
|
|
|
2517
2687
|
</html>
|
|
2518
2688
|
);
|
|
2519
2689
|
}
|
|
2690
|
+
<% } %>
|
|
2520
2691
|
`,
|
|
2521
2692
|
"src/app/products/page.tsx": `'use client';
|
|
2522
2693
|
|
|
@@ -3417,13 +3588,13 @@ function CheckoutContent() {
|
|
|
3417
3588
|
}, [checkout?.id, storeInfo?.upsell?.checkoutOrderBumpEnabled]);
|
|
3418
3589
|
|
|
3419
3590
|
// Handle bump toggle
|
|
3420
|
-
async function handleBumpToggle(bumpId: string, add: boolean) {
|
|
3591
|
+
async function handleBumpToggle(bumpId: string, add: boolean, variantId?: string) {
|
|
3421
3592
|
if (!cart?.id || bumpLoading) return;
|
|
3422
3593
|
try {
|
|
3423
3594
|
setBumpLoading(bumpId);
|
|
3424
3595
|
const client = getClient();
|
|
3425
3596
|
if (add) {
|
|
3426
|
-
await client.addOrderBump(cart.id, bumpId);
|
|
3597
|
+
await client.addOrderBump(cart.id, bumpId, variantId);
|
|
3427
3598
|
setAddedBumpIds((prev) => new Set([...prev, bumpId]));
|
|
3428
3599
|
} else {
|
|
3429
3600
|
await client.removeOrderBump(cart.id, bumpId);
|
|
@@ -4121,6 +4292,23 @@ function OrderConfirmationContent() {
|
|
|
4121
4292
|
client.handlePaymentSuccess(checkoutId!);
|
|
4122
4293
|
await refreshCart();
|
|
4123
4294
|
|
|
4295
|
+
// For redirect-based payment providers (e.g. CardCom), the customer
|
|
4296
|
+
// returns with provider params in the URL (lowprofilecode, etc.).
|
|
4297
|
+
// Send these to the backend for server-side verification via the
|
|
4298
|
+
// provider's API (e.g. GetLpResult) \u2014 never trust URL params alone.
|
|
4299
|
+
const lowProfileCode =
|
|
4300
|
+
searchParams.get('lowprofilecode') || searchParams.get('LowProfileCode');
|
|
4301
|
+
if (lowProfileCode) {
|
|
4302
|
+
try {
|
|
4303
|
+
await client.confirmSdkPayment(checkoutId!, {
|
|
4304
|
+
paymentIntentId: lowProfileCode,
|
|
4305
|
+
});
|
|
4306
|
+
} catch (err) {
|
|
4307
|
+
console.warn('Redirect payment confirmation failed:', err);
|
|
4308
|
+
// Don't block \u2014 webhook may still process the payment
|
|
4309
|
+
}
|
|
4310
|
+
}
|
|
4311
|
+
|
|
4124
4312
|
const orderResult = await client.waitForOrder(checkoutId!, {
|
|
4125
4313
|
maxWaitMs: 30000,
|
|
4126
4314
|
});
|
|
@@ -5637,7 +5825,62 @@ export async function POST(request: NextRequest) {
|
|
|
5637
5825
|
return response;
|
|
5638
5826
|
}
|
|
5639
5827
|
`,
|
|
5640
|
-
"src/middleware.ts":
|
|
5828
|
+
"src/middleware.ts.ejs": `<% if (i18nEnabled) { %>
|
|
5829
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
5830
|
+
|
|
5831
|
+
const TOKEN_COOKIE = 'brainerce_customer_token';
|
|
5832
|
+
const PROTECTED_PATHS = ['/account'];
|
|
5833
|
+
const supportedLocales = <%- supportedLocales %>;
|
|
5834
|
+
const defaultLocale = '<%= defaultLocale %>';
|
|
5835
|
+
|
|
5836
|
+
function getLocaleFromPath(pathname: string): string | null {
|
|
5837
|
+
const segment = pathname.split('/')[1];
|
|
5838
|
+
return supportedLocales.includes(segment) ? segment : null;
|
|
5839
|
+
}
|
|
5840
|
+
|
|
5841
|
+
export function middleware(request: NextRequest) {
|
|
5842
|
+
const { pathname } = request.nextUrl;
|
|
5843
|
+
|
|
5844
|
+
// Skip static files and API routes
|
|
5845
|
+
if (
|
|
5846
|
+
pathname.startsWith('/api/') ||
|
|
5847
|
+
pathname.startsWith('/_next/') ||
|
|
5848
|
+
pathname.includes('.')
|
|
5849
|
+
) {
|
|
5850
|
+
return NextResponse.next();
|
|
5851
|
+
}
|
|
5852
|
+
|
|
5853
|
+
const pathnameLocale = getLocaleFromPath(pathname);
|
|
5854
|
+
|
|
5855
|
+
// Redirect to default locale if no locale prefix
|
|
5856
|
+
if (!pathnameLocale) {
|
|
5857
|
+
const url = request.nextUrl.clone();
|
|
5858
|
+
url.pathname = \`/\${defaultLocale}\${pathname}\`;
|
|
5859
|
+
return NextResponse.redirect(url);
|
|
5860
|
+
}
|
|
5861
|
+
|
|
5862
|
+
// Auth protection (with locale prefix)
|
|
5863
|
+
const pathWithoutLocale = pathname.replace(\`/\${pathnameLocale}\`, '') || '/';
|
|
5864
|
+
const isProtected = PROTECTED_PATHS.some((p) => pathWithoutLocale.startsWith(p));
|
|
5865
|
+
if (isProtected) {
|
|
5866
|
+
const token = request.cookies.get(TOKEN_COOKIE);
|
|
5867
|
+
if (!token?.value) {
|
|
5868
|
+
const loginUrl = new URL(\`/\${pathnameLocale}/login\`, request.url);
|
|
5869
|
+
return NextResponse.redirect(loginUrl);
|
|
5870
|
+
}
|
|
5871
|
+
}
|
|
5872
|
+
|
|
5873
|
+
// Set locale header for server components
|
|
5874
|
+
const response = NextResponse.next();
|
|
5875
|
+
response.headers.set('x-locale', pathnameLocale);
|
|
5876
|
+
return response;
|
|
5877
|
+
}
|
|
5878
|
+
|
|
5879
|
+
export const config = {
|
|
5880
|
+
matcher: ['/((?!_next|api|.*\\\\..*).*)'],
|
|
5881
|
+
};
|
|
5882
|
+
<% } else { %>
|
|
5883
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
5641
5884
|
|
|
5642
5885
|
const TOKEN_COOKIE = 'brainerce_customer_token';
|
|
5643
5886
|
|
|
@@ -5662,6 +5905,7 @@ export function middleware(request: NextRequest) {
|
|
|
5662
5905
|
export const config = {
|
|
5663
5906
|
matcher: ['/account/:path*'],
|
|
5664
5907
|
};
|
|
5908
|
+
<% } %>
|
|
5665
5909
|
`,
|
|
5666
5910
|
"src/components/products/product-card.tsx": `'use client';
|
|
5667
5911
|
|
|
@@ -7523,7 +7767,9 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
|
|
|
7523
7767
|
initialized.current = true;
|
|
7524
7768
|
|
|
7525
7769
|
const client = getClient();
|
|
7526
|
-
const
|
|
7770
|
+
const iframeSuccessUrl = \`\${window.location.origin}/payment-complete?checkout_id=\${checkoutId}\`;
|
|
7771
|
+
const iframeFailedUrl = \`\${window.location.origin}/payment-complete?checkout_id=\${checkoutId}&failed=true\`;
|
|
7772
|
+
const redirectSuccessUrl = \`\${window.location.origin}/order-confirmation?checkout_id=\${checkoutId}\`;
|
|
7527
7773
|
const cancelUrl = \`\${window.location.origin}/checkout?checkout_id=\${checkoutId}&canceled=true\`;
|
|
7528
7774
|
|
|
7529
7775
|
let sdkInitDone = false;
|
|
@@ -7684,8 +7930,19 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
|
|
|
7684
7930
|
});
|
|
7685
7931
|
|
|
7686
7932
|
// C) Create payment intent (starts wallet timer)
|
|
7687
|
-
|
|
7688
|
-
|
|
7933
|
+
// Wait for provider info so we can choose the right success URL:
|
|
7934
|
+
// iframe providers redirect inside the iframe to /payment-complete (postMessage),
|
|
7935
|
+
// redirect providers go straight to /order-confirmation.
|
|
7936
|
+
const intentPromise = providerPromise
|
|
7937
|
+
.then((providerSdk) => {
|
|
7938
|
+
const isIframe = providerSdk?.renderType === 'iframe';
|
|
7939
|
+
const successUrl = isIframe ? iframeSuccessUrl : redirectSuccessUrl;
|
|
7940
|
+
const failedUrl = isIframe ? iframeFailedUrl : cancelUrl;
|
|
7941
|
+
return client.createPaymentIntent(checkoutId, {
|
|
7942
|
+
successUrl,
|
|
7943
|
+
cancelUrl: failedUrl,
|
|
7944
|
+
});
|
|
7945
|
+
})
|
|
7689
7946
|
.then((intent) => {
|
|
7690
7947
|
setPaymentIntent(intent);
|
|
7691
7948
|
return intent;
|
|
@@ -7710,6 +7967,36 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
|
|
|
7710
7967
|
window.location.href = intent.clientSecret;
|
|
7711
7968
|
return;
|
|
7712
7969
|
}
|
|
7970
|
+
|
|
7971
|
+
// Iframe mode: listen for postMessage from the /payment-complete callback
|
|
7972
|
+
// page that loads inside the iframe after the provider redirects on completion.
|
|
7973
|
+
if (sdk.renderType === 'iframe') {
|
|
7974
|
+
const handleMessage = (event: MessageEvent) => {
|
|
7975
|
+
if (event.origin !== window.location.origin) return;
|
|
7976
|
+
if (event.data?.type !== 'brainerce:payment-complete') return;
|
|
7977
|
+
|
|
7978
|
+
const params = event.data.data as Record<string, string> | undefined;
|
|
7979
|
+
if (params?.failed === 'true') {
|
|
7980
|
+
setError(t('paymentError'));
|
|
7981
|
+
return;
|
|
7982
|
+
}
|
|
7983
|
+
|
|
7984
|
+
// Map provider-specific params to normalized format for
|
|
7985
|
+
// server-side verification (e.g. CardCom lowprofilecode \u2192 paymentIntentId)
|
|
7986
|
+
const lowProfileCode = params?.lowprofilecode || params?.LowProfileCode;
|
|
7987
|
+
const normalized: Record<string, unknown> = { ...params };
|
|
7988
|
+
if (lowProfileCode) {
|
|
7989
|
+
normalized.paymentIntentId = lowProfileCode;
|
|
7990
|
+
}
|
|
7991
|
+
|
|
7992
|
+
// Trigger server-side verification + order creation
|
|
7993
|
+
handleSuccess(normalized);
|
|
7994
|
+
};
|
|
7995
|
+
window.addEventListener('message', handleMessage);
|
|
7996
|
+
cleanups.push(() => window.removeEventListener('message', handleMessage));
|
|
7997
|
+
return;
|
|
7998
|
+
}
|
|
7999
|
+
|
|
7713
8000
|
if (sdk.renderType !== 'sdk-widget' || !sdk.globalName) return;
|
|
7714
8001
|
|
|
7715
8002
|
// Store for retryRender from onError callback
|
|
@@ -7860,15 +8147,45 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
|
|
|
7860
8147
|
|
|
7861
8148
|
if (sdk.renderType === 'iframe') {
|
|
7862
8149
|
return (
|
|
7863
|
-
|
|
7864
|
-
|
|
7865
|
-
|
|
7866
|
-
className="w-full
|
|
7867
|
-
|
|
7868
|
-
|
|
7869
|
-
|
|
7870
|
-
|
|
7871
|
-
|
|
8150
|
+
<>
|
|
8151
|
+
{/* Modal overlay */}
|
|
8152
|
+
<div className="fixed inset-0 z-50 flex items-start justify-center overflow-y-auto bg-black/50 py-6 backdrop-blur-sm">
|
|
8153
|
+
<div className="relative mx-4 w-full max-w-md rounded-2xl bg-white shadow-2xl">
|
|
8154
|
+
{/* Close button */}
|
|
8155
|
+
<button
|
|
8156
|
+
onClick={() => {
|
|
8157
|
+
window.location.href = \`/checkout?checkout_id=\${checkoutId}&canceled=true\`;
|
|
8158
|
+
}}
|
|
8159
|
+
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"
|
|
8160
|
+
aria-label="Close"
|
|
8161
|
+
>
|
|
8162
|
+
<svg
|
|
8163
|
+
width="14"
|
|
8164
|
+
height="14"
|
|
8165
|
+
viewBox="0 0 14 14"
|
|
8166
|
+
fill="none"
|
|
8167
|
+
stroke="currentColor"
|
|
8168
|
+
strokeWidth="2"
|
|
8169
|
+
strokeLinecap="round"
|
|
8170
|
+
>
|
|
8171
|
+
<path d="M1 1l12 12M13 1L1 13" />
|
|
8172
|
+
</svg>
|
|
8173
|
+
</button>
|
|
8174
|
+
<iframe
|
|
8175
|
+
src={paymentIntent.clientSecret}
|
|
8176
|
+
className="w-full rounded-2xl border-0"
|
|
8177
|
+
style={{ height: '80vh' }}
|
|
8178
|
+
title={t('payment')}
|
|
8179
|
+
allow="payment"
|
|
8180
|
+
/>
|
|
8181
|
+
</div>
|
|
8182
|
+
</div>
|
|
8183
|
+
{/* Placeholder so the checkout layout doesn't collapse */}
|
|
8184
|
+
<div className={cn('flex flex-col items-center justify-center py-12', className)}>
|
|
8185
|
+
<LoadingSpinner size="lg" />
|
|
8186
|
+
<p className="text-muted-foreground mt-4 text-sm">{t('preparingPayment')}</p>
|
|
8187
|
+
</div>
|
|
8188
|
+
</>
|
|
7872
8189
|
);
|
|
7873
8190
|
}
|
|
7874
8191
|
|
|
@@ -9995,10 +10312,10 @@ export function CartUpgradeBanner({
|
|
|
9995
10312
|
`,
|
|
9996
10313
|
"src/components/cart/cart-bundle-offer.tsx": `'use client';
|
|
9997
10314
|
|
|
9998
|
-
import { useState } from 'react';
|
|
10315
|
+
import { useState, useMemo } from 'react';
|
|
9999
10316
|
import Image from 'next/image';
|
|
10000
10317
|
import type { CartBundleOffer as CartBundleOfferType } from 'brainerce';
|
|
10001
|
-
import { formatPrice } from 'brainerce';
|
|
10318
|
+
import { formatPrice, getVariantOptions } from 'brainerce';
|
|
10002
10319
|
import { useStoreInfo } from '@/providers/store-provider';
|
|
10003
10320
|
import { useTranslations } from '@/lib/translations';
|
|
10004
10321
|
import { cn } from '@/lib/utils';
|
|
@@ -10015,28 +10332,114 @@ export function CartBundleOfferCard({ offer, cartId, onAdd, className }: CartBun
|
|
|
10015
10332
|
const t = useTranslations('cart');
|
|
10016
10333
|
const currency = storeInfo?.currency || 'USD';
|
|
10017
10334
|
const [adding, setAdding] = useState(false);
|
|
10335
|
+
const [selectedVariantId, setSelectedVariantId] = useState<string | null>(null);
|
|
10018
10336
|
|
|
10019
10337
|
const product = offer.bundleProduct;
|
|
10338
|
+
const variants = product.variants;
|
|
10339
|
+
const requiresSelection = offer.requiresVariantSelection && variants && variants.length > 0;
|
|
10340
|
+
|
|
10341
|
+
// Build attribute groups from variants
|
|
10342
|
+
const attributeGroups = useMemo(() => {
|
|
10343
|
+
if (!requiresSelection || !variants) return [];
|
|
10344
|
+
const groups = new Map<string, Set<string>>();
|
|
10345
|
+
for (const v of variants) {
|
|
10346
|
+
const opts = getVariantOptions(v as any);
|
|
10347
|
+
for (const opt of opts) {
|
|
10348
|
+
if (!groups.has(opt.name)) groups.set(opt.name, new Set());
|
|
10349
|
+
groups.get(opt.name)!.add(opt.value);
|
|
10350
|
+
}
|
|
10351
|
+
}
|
|
10352
|
+
return Array.from(groups.entries()).map(([name, values]) => ({
|
|
10353
|
+
name,
|
|
10354
|
+
values: Array.from(values),
|
|
10355
|
+
}));
|
|
10356
|
+
}, [requiresSelection, variants]);
|
|
10357
|
+
|
|
10358
|
+
const [selectedAttrs, setSelectedAttrs] = useState<Record<string, string>>({});
|
|
10359
|
+
|
|
10360
|
+
const selectedVariant = useMemo(() => {
|
|
10361
|
+
if (!requiresSelection || !variants) return null;
|
|
10362
|
+
return (
|
|
10363
|
+
variants.find((v) => {
|
|
10364
|
+
const opts = getVariantOptions(v as any);
|
|
10365
|
+
return attributeGroups.every((group) => {
|
|
10366
|
+
const opt = opts.find((o) => o.name === group.name);
|
|
10367
|
+
return opt && selectedAttrs[group.name] === opt.value;
|
|
10368
|
+
});
|
|
10369
|
+
}) ?? null
|
|
10370
|
+
);
|
|
10371
|
+
}, [requiresSelection, variants, selectedAttrs, attributeGroups]);
|
|
10372
|
+
|
|
10373
|
+
const effectiveVariantId = selectedVariant?.id ?? selectedVariantId;
|
|
10374
|
+
|
|
10375
|
+
function handleAttrSelect(attrName: string, value: string) {
|
|
10376
|
+
const next = { ...selectedAttrs, [attrName]: value };
|
|
10377
|
+
setSelectedAttrs(next);
|
|
10378
|
+
if (variants) {
|
|
10379
|
+
const match = variants.find((v) => {
|
|
10380
|
+
const opts = getVariantOptions(v as any);
|
|
10381
|
+
return attributeGroups.every((group) => {
|
|
10382
|
+
const opt = opts.find((o) => o.name === group.name);
|
|
10383
|
+
return opt && next[group.name] === opt.value;
|
|
10384
|
+
});
|
|
10385
|
+
});
|
|
10386
|
+
setSelectedVariantId(match?.id ?? null);
|
|
10387
|
+
}
|
|
10388
|
+
}
|
|
10389
|
+
|
|
10390
|
+
// Compute display prices
|
|
10391
|
+
const { displayOriginal, displayDiscounted, discountLabel } = useMemo(() => {
|
|
10392
|
+
let effectivePrice: number;
|
|
10393
|
+
if (selectedVariant) {
|
|
10394
|
+
const vSale = selectedVariant.salePrice ? parseFloat(selectedVariant.salePrice) : null;
|
|
10395
|
+
const vPrice = selectedVariant.price ? parseFloat(selectedVariant.price) : null;
|
|
10396
|
+
effectivePrice = vSale ?? vPrice ?? parseFloat(offer.originalPrice);
|
|
10397
|
+
} else {
|
|
10398
|
+
effectivePrice = parseFloat(offer.originalPrice);
|
|
10399
|
+
}
|
|
10400
|
+
|
|
10401
|
+
let discounted: number;
|
|
10402
|
+
if (offer.discountType === 'PERCENTAGE') {
|
|
10403
|
+
discounted = effectivePrice * (1 - parseFloat(offer.discountValue) / 100);
|
|
10404
|
+
} else {
|
|
10405
|
+
discounted = Math.max(0, effectivePrice - parseFloat(offer.discountValue));
|
|
10406
|
+
}
|
|
10407
|
+
|
|
10408
|
+
const label =
|
|
10409
|
+
offer.discountType === 'PERCENTAGE'
|
|
10410
|
+
? \`\${offer.discountValue}%\`
|
|
10411
|
+
: (formatPrice(parseFloat(offer.discountValue), { currency }) as string);
|
|
10412
|
+
|
|
10413
|
+
return { displayOriginal: effectivePrice, displayDiscounted: discounted, discountLabel: label };
|
|
10414
|
+
}, [selectedVariant, offer.originalPrice, offer.discountType, offer.discountValue, currency]);
|
|
10415
|
+
|
|
10416
|
+
const isOos =
|
|
10417
|
+
selectedVariant?.inventory?.trackingMode !== 'NOT_TRACKED' &&
|
|
10418
|
+
selectedVariant?.inventory?.available != null &&
|
|
10419
|
+
selectedVariant.inventory.available <= 0;
|
|
10420
|
+
|
|
10421
|
+
const lockedLabel =
|
|
10422
|
+
offer.lockedVariant?.name ??
|
|
10423
|
+
(offer.lockedVariant?.attributes
|
|
10424
|
+
? Object.values(offer.lockedVariant.attributes).join(' / ')
|
|
10425
|
+
: null);
|
|
10426
|
+
|
|
10020
10427
|
const firstImage = product.images?.[0];
|
|
10021
10428
|
const imageUrl = firstImage
|
|
10022
10429
|
? typeof firstImage === 'string'
|
|
10023
10430
|
? firstImage
|
|
10024
10431
|
: firstImage.url
|
|
10025
10432
|
: null;
|
|
10026
|
-
|
|
10027
|
-
const
|
|
10028
|
-
const discountLabel =
|
|
10029
|
-
offer.discountType === 'PERCENTAGE'
|
|
10030
|
-
? \`\${offer.discountValue}%\`
|
|
10031
|
-
: (formatPrice(parseFloat(offer.discountValue), { currency }) as string);
|
|
10433
|
+
|
|
10434
|
+
const canAdd = !requiresSelection || !!effectiveVariantId;
|
|
10032
10435
|
|
|
10033
10436
|
async function handleAdd() {
|
|
10034
|
-
if (adding) return;
|
|
10437
|
+
if (adding || !canAdd || isOos) return;
|
|
10035
10438
|
try {
|
|
10036
10439
|
setAdding(true);
|
|
10037
10440
|
const { getClient } = await import('@/lib/brainerce');
|
|
10038
10441
|
const client = getClient();
|
|
10039
|
-
await client.addBundleToCart(cartId, offer.id);
|
|
10442
|
+
await client.addBundleToCart(cartId, offer.id, effectiveVariantId ?? undefined);
|
|
10040
10443
|
onAdd();
|
|
10041
10444
|
} catch (err) {
|
|
10042
10445
|
console.error('Failed to add bundle item:', err);
|
|
@@ -10046,70 +10449,121 @@ export function CartBundleOfferCard({ offer, cartId, onAdd, className }: CartBun
|
|
|
10046
10449
|
}
|
|
10047
10450
|
|
|
10048
10451
|
return (
|
|
10049
|
-
<div
|
|
10050
|
-
className=
|
|
10051
|
-
|
|
10052
|
-
className
|
|
10053
|
-
|
|
10054
|
-
|
|
10055
|
-
|
|
10056
|
-
|
|
10057
|
-
|
|
10058
|
-
|
|
10059
|
-
|
|
10060
|
-
|
|
10061
|
-
|
|
10062
|
-
|
|
10063
|
-
|
|
10064
|
-
|
|
10065
|
-
|
|
10066
|
-
|
|
10067
|
-
|
|
10068
|
-
</svg>
|
|
10069
|
-
</div>
|
|
10070
|
-
)}
|
|
10071
|
-
</div>
|
|
10452
|
+
<div className={cn('bg-background border-border rounded-lg border p-4', className)}>
|
|
10453
|
+
<div className="flex items-center gap-4">
|
|
10454
|
+
{/* Product image */}
|
|
10455
|
+
<div className="bg-muted relative h-16 w-16 flex-shrink-0 overflow-hidden rounded">
|
|
10456
|
+
{imageUrl ? (
|
|
10457
|
+
<Image src={imageUrl} alt={product.name} fill sizes="64px" className="object-cover" />
|
|
10458
|
+
) : (
|
|
10459
|
+
<div className="text-muted-foreground flex h-full w-full items-center justify-center">
|
|
10460
|
+
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
10461
|
+
<path
|
|
10462
|
+
strokeLinecap="round"
|
|
10463
|
+
strokeLinejoin="round"
|
|
10464
|
+
strokeWidth={1.5}
|
|
10465
|
+
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"
|
|
10466
|
+
/>
|
|
10467
|
+
</svg>
|
|
10468
|
+
</div>
|
|
10469
|
+
)}
|
|
10470
|
+
</div>
|
|
10072
10471
|
|
|
10073
|
-
|
|
10074
|
-
|
|
10075
|
-
|
|
10076
|
-
|
|
10077
|
-
|
|
10078
|
-
|
|
10079
|
-
|
|
10080
|
-
<
|
|
10081
|
-
|
|
10082
|
-
|
|
10083
|
-
|
|
10084
|
-
|
|
10085
|
-
|
|
10086
|
-
|
|
10087
|
-
-
|
|
10088
|
-
|
|
10472
|
+
{/* Details */}
|
|
10473
|
+
<div className="min-w-0 flex-1">
|
|
10474
|
+
<p className="text-foreground text-sm font-medium">{offer.name}</p>
|
|
10475
|
+
{offer.description && (
|
|
10476
|
+
<p className="text-muted-foreground mt-0.5 text-xs">{offer.description}</p>
|
|
10477
|
+
)}
|
|
10478
|
+
{lockedLabel && <p className="text-muted-foreground mt-0.5 text-xs">{lockedLabel}</p>}
|
|
10479
|
+
<div className="mt-1 flex items-center gap-2">
|
|
10480
|
+
<span className="text-muted-foreground text-sm line-through">
|
|
10481
|
+
{formatPrice(displayOriginal, { currency }) as string}
|
|
10482
|
+
</span>
|
|
10483
|
+
<span className="text-foreground text-sm font-semibold">
|
|
10484
|
+
{formatPrice(displayDiscounted, { currency }) as string}
|
|
10485
|
+
</span>
|
|
10486
|
+
<span className="bg-destructive/10 text-destructive rounded px-1.5 py-0.5 text-xs font-medium">
|
|
10487
|
+
-{discountLabel}
|
|
10488
|
+
</span>
|
|
10489
|
+
</div>
|
|
10089
10490
|
</div>
|
|
10491
|
+
|
|
10492
|
+
{/* Add button */}
|
|
10493
|
+
<button
|
|
10494
|
+
type="button"
|
|
10495
|
+
onClick={handleAdd}
|
|
10496
|
+
disabled={adding || !canAdd || isOos}
|
|
10497
|
+
className={cn(
|
|
10498
|
+
'bg-primary text-primary-foreground flex-shrink-0 rounded px-4 py-2 text-xs font-medium transition-opacity hover:opacity-90',
|
|
10499
|
+
'disabled:cursor-not-allowed disabled:opacity-50'
|
|
10500
|
+
)}
|
|
10501
|
+
>
|
|
10502
|
+
{adding
|
|
10503
|
+
? t('addingBundle')
|
|
10504
|
+
: requiresSelection && !canAdd
|
|
10505
|
+
? t('selectOptions') || 'Select options'
|
|
10506
|
+
: t('addBundleItem')}
|
|
10507
|
+
</button>
|
|
10090
10508
|
</div>
|
|
10091
10509
|
|
|
10092
|
-
{/*
|
|
10093
|
-
|
|
10094
|
-
|
|
10095
|
-
|
|
10096
|
-
|
|
10097
|
-
|
|
10098
|
-
|
|
10099
|
-
|
|
10100
|
-
|
|
10101
|
-
|
|
10102
|
-
|
|
10103
|
-
|
|
10510
|
+
{/* Compact variant selector */}
|
|
10511
|
+
{requiresSelection && (
|
|
10512
|
+
<div className="mt-3 space-y-1.5 ps-20">
|
|
10513
|
+
{attributeGroups.map((group) => (
|
|
10514
|
+
<div key={group.name} className="flex flex-wrap items-center gap-1.5">
|
|
10515
|
+
<span className="text-muted-foreground text-xs">{group.name}:</span>
|
|
10516
|
+
{group.values.map((value) => {
|
|
10517
|
+
const isSelected = selectedAttrs[group.name] === value;
|
|
10518
|
+
const variantForValue = variants?.find((v) => {
|
|
10519
|
+
const opts = getVariantOptions(v as any);
|
|
10520
|
+
const matchesValue = opts.some((o) => o.name === group.name && o.value === value);
|
|
10521
|
+
if (!matchesValue) return false;
|
|
10522
|
+
return Object.entries(selectedAttrs).every(([k, sv]) => {
|
|
10523
|
+
if (k === group.name) return true;
|
|
10524
|
+
return opts.some((o) => o.name === k && o.value === sv);
|
|
10525
|
+
});
|
|
10526
|
+
});
|
|
10527
|
+
const isVariantOos =
|
|
10528
|
+
variantForValue?.inventory?.trackingMode !== 'NOT_TRACKED' &&
|
|
10529
|
+
variantForValue?.inventory?.available != null &&
|
|
10530
|
+
variantForValue.inventory.available <= 0;
|
|
10531
|
+
|
|
10532
|
+
return (
|
|
10533
|
+
<button
|
|
10534
|
+
key={value}
|
|
10535
|
+
type="button"
|
|
10536
|
+
onClick={() => handleAttrSelect(group.name, value)}
|
|
10537
|
+
disabled={isVariantOos}
|
|
10538
|
+
className={cn(
|
|
10539
|
+
'rounded-full border px-2.5 py-0.5 text-xs transition-colors',
|
|
10540
|
+
isSelected
|
|
10541
|
+
? 'border-primary bg-primary text-primary-foreground'
|
|
10542
|
+
: 'border-border text-foreground hover:border-primary/50',
|
|
10543
|
+
isVariantOos && 'cursor-not-allowed line-through opacity-40'
|
|
10544
|
+
)}
|
|
10545
|
+
>
|
|
10546
|
+
{value}
|
|
10547
|
+
</button>
|
|
10548
|
+
);
|
|
10549
|
+
})}
|
|
10550
|
+
</div>
|
|
10551
|
+
))}
|
|
10552
|
+
{isOos && effectiveVariantId && (
|
|
10553
|
+
<p className="text-destructive text-xs">{t('outOfStock') || 'Out of stock'}</p>
|
|
10554
|
+
)}
|
|
10555
|
+
</div>
|
|
10556
|
+
)}
|
|
10104
10557
|
</div>
|
|
10105
10558
|
);
|
|
10106
10559
|
}
|
|
10107
10560
|
`,
|
|
10108
10561
|
"src/components/checkout/order-bump-card.tsx": `'use client';
|
|
10109
10562
|
|
|
10563
|
+
import { useState, useMemo } from 'react';
|
|
10110
10564
|
import Image from 'next/image';
|
|
10111
|
-
import type { OrderBump } from 'brainerce';
|
|
10112
|
-
import { formatPrice } from 'brainerce';
|
|
10565
|
+
import type { OrderBump, RecommendationVariant } from 'brainerce';
|
|
10566
|
+
import { formatPrice, getVariantOptions } from 'brainerce';
|
|
10113
10567
|
import { useStoreInfo } from '@/providers/store-provider';
|
|
10114
10568
|
import { useTranslations } from '@/lib/translations';
|
|
10115
10569
|
import { cn } from '@/lib/utils';
|
|
@@ -10117,7 +10571,7 @@ import { cn } from '@/lib/utils';
|
|
|
10117
10571
|
interface OrderBumpCardProps {
|
|
10118
10572
|
bump: OrderBump;
|
|
10119
10573
|
isAdded: boolean;
|
|
10120
|
-
onToggle: (bumpId: string, add: boolean) => void;
|
|
10574
|
+
onToggle: (bumpId: string, add: boolean, variantId?: string) => void;
|
|
10121
10575
|
loading: boolean;
|
|
10122
10576
|
className?: string;
|
|
10123
10577
|
}
|
|
@@ -10126,66 +10580,225 @@ export function OrderBumpCard({ bump, isAdded, onToggle, loading, className }: O
|
|
|
10126
10580
|
const { storeInfo } = useStoreInfo();
|
|
10127
10581
|
const t = useTranslations('checkout');
|
|
10128
10582
|
const currency = storeInfo?.currency || 'USD';
|
|
10583
|
+
const [selectedVariantId, setSelectedVariantId] = useState<string | null>(null);
|
|
10129
10584
|
|
|
10130
10585
|
const product = bump.bumpProduct;
|
|
10586
|
+
const variants = product.variants;
|
|
10587
|
+
const requiresSelection = bump.requiresVariantSelection && variants && variants.length > 0;
|
|
10588
|
+
|
|
10589
|
+
// Build attribute groups from variants for pill selector
|
|
10590
|
+
const attributeGroups = useMemo(() => {
|
|
10591
|
+
if (!requiresSelection || !variants) return [];
|
|
10592
|
+
const groups = new Map<string, Set<string>>();
|
|
10593
|
+
for (const v of variants) {
|
|
10594
|
+
const opts = getVariantOptions(v as any);
|
|
10595
|
+
for (const opt of opts) {
|
|
10596
|
+
if (!groups.has(opt.name)) groups.set(opt.name, new Set());
|
|
10597
|
+
groups.get(opt.name)!.add(opt.value);
|
|
10598
|
+
}
|
|
10599
|
+
}
|
|
10600
|
+
return Array.from(groups.entries()).map(([name, values]) => ({
|
|
10601
|
+
name,
|
|
10602
|
+
values: Array.from(values),
|
|
10603
|
+
}));
|
|
10604
|
+
}, [requiresSelection, variants]);
|
|
10605
|
+
|
|
10606
|
+
// Track selected attributes
|
|
10607
|
+
const [selectedAttrs, setSelectedAttrs] = useState<Record<string, string>>({});
|
|
10608
|
+
|
|
10609
|
+
// Find matching variant based on selected attributes
|
|
10610
|
+
const selectedVariant = useMemo(() => {
|
|
10611
|
+
if (!requiresSelection || !variants) return null;
|
|
10612
|
+
return (
|
|
10613
|
+
variants.find((v) => {
|
|
10614
|
+
const opts = getVariantOptions(v as any);
|
|
10615
|
+
return attributeGroups.every((group) => {
|
|
10616
|
+
const opt = opts.find((o) => o.name === group.name);
|
|
10617
|
+
return opt && selectedAttrs[group.name] === opt.value;
|
|
10618
|
+
});
|
|
10619
|
+
}) ?? null
|
|
10620
|
+
);
|
|
10621
|
+
}, [requiresSelection, variants, selectedAttrs, attributeGroups]);
|
|
10622
|
+
|
|
10623
|
+
// Update selectedVariantId when variant match changes
|
|
10624
|
+
const effectiveVariantId = selectedVariant?.id ?? selectedVariantId;
|
|
10625
|
+
|
|
10626
|
+
function handleAttrSelect(attrName: string, value: string) {
|
|
10627
|
+
const next = { ...selectedAttrs, [attrName]: value };
|
|
10628
|
+
setSelectedAttrs(next);
|
|
10629
|
+
// Find matching variant with new selection
|
|
10630
|
+
if (variants) {
|
|
10631
|
+
const match = variants.find((v) => {
|
|
10632
|
+
const opts = getVariantOptions(v as any);
|
|
10633
|
+
return attributeGroups.every((group) => {
|
|
10634
|
+
const opt = opts.find((o) => o.name === group.name);
|
|
10635
|
+
return opt && next[group.name] === opt.value;
|
|
10636
|
+
});
|
|
10637
|
+
});
|
|
10638
|
+
setSelectedVariantId(match?.id ?? null);
|
|
10639
|
+
}
|
|
10640
|
+
}
|
|
10641
|
+
|
|
10642
|
+
// Compute display price
|
|
10643
|
+
const { displayOriginal, displayDiscounted } = useMemo(() => {
|
|
10644
|
+
let effectivePrice: number;
|
|
10645
|
+
if (selectedVariant) {
|
|
10646
|
+
const vSale = selectedVariant.salePrice ? parseFloat(selectedVariant.salePrice) : null;
|
|
10647
|
+
const vPrice = selectedVariant.price ? parseFloat(selectedVariant.price) : null;
|
|
10648
|
+
effectivePrice = vSale ?? vPrice ?? parseFloat(bump.originalPrice);
|
|
10649
|
+
} else {
|
|
10650
|
+
effectivePrice = parseFloat(bump.originalPrice);
|
|
10651
|
+
}
|
|
10652
|
+
|
|
10653
|
+
let discounted: number | null = null;
|
|
10654
|
+
if (bump.discountType && bump.discountValue) {
|
|
10655
|
+
const dv = parseFloat(bump.discountValue);
|
|
10656
|
+
if (bump.discountType === 'PERCENTAGE') {
|
|
10657
|
+
discounted = effectivePrice * (1 - dv / 100);
|
|
10658
|
+
} else {
|
|
10659
|
+
discounted = Math.max(0, effectivePrice - dv);
|
|
10660
|
+
}
|
|
10661
|
+
}
|
|
10662
|
+
|
|
10663
|
+
return { displayOriginal: effectivePrice, displayDiscounted: discounted };
|
|
10664
|
+
}, [selectedVariant, bump.originalPrice, bump.discountType, bump.discountValue]);
|
|
10665
|
+
|
|
10666
|
+
// Check if selected variant is out of stock
|
|
10667
|
+
const isOos =
|
|
10668
|
+
selectedVariant?.inventory?.trackingMode !== 'NOT_TRACKED' &&
|
|
10669
|
+
selectedVariant?.inventory?.available != null &&
|
|
10670
|
+
selectedVariant.inventory.available <= 0;
|
|
10671
|
+
|
|
10672
|
+
// Locked variant label
|
|
10673
|
+
const lockedLabel =
|
|
10674
|
+
bump.lockedVariant?.name ??
|
|
10675
|
+
(bump.lockedVariant?.attributes
|
|
10676
|
+
? Object.values(bump.lockedVariant.attributes).join(' / ')
|
|
10677
|
+
: null);
|
|
10678
|
+
|
|
10131
10679
|
const firstImage = product.images?.[0];
|
|
10132
10680
|
const imageUrl = firstImage
|
|
10133
10681
|
? typeof firstImage === 'string'
|
|
10134
10682
|
? firstImage
|
|
10135
10683
|
: firstImage.url
|
|
10136
10684
|
: null;
|
|
10137
|
-
|
|
10138
|
-
const
|
|
10139
|
-
const discountedPrice = hasDiscount ? parseFloat(bump.discountedPrice!) : null;
|
|
10685
|
+
|
|
10686
|
+
const canToggle = !requiresSelection || !!effectiveVariantId;
|
|
10140
10687
|
|
|
10141
10688
|
return (
|
|
10142
|
-
<
|
|
10689
|
+
<div
|
|
10143
10690
|
className={cn(
|
|
10144
|
-
'border-border hover:border-primary/50
|
|
10691
|
+
'border-border hover:border-primary/50 rounded-lg border p-3 transition-colors',
|
|
10145
10692
|
isAdded && 'border-primary bg-primary/5',
|
|
10146
10693
|
loading && 'pointer-events-none opacity-60',
|
|
10147
10694
|
className
|
|
10148
10695
|
)}
|
|
10149
10696
|
>
|
|
10150
|
-
<
|
|
10151
|
-
|
|
10152
|
-
|
|
10153
|
-
|
|
10154
|
-
|
|
10155
|
-
|
|
10156
|
-
|
|
10157
|
-
|
|
10158
|
-
|
|
10159
|
-
|
|
10160
|
-
|
|
10161
|
-
|
|
10162
|
-
</div>
|
|
10163
|
-
)}
|
|
10697
|
+
<label className="flex cursor-pointer items-start gap-3">
|
|
10698
|
+
<input
|
|
10699
|
+
type="checkbox"
|
|
10700
|
+
checked={isAdded}
|
|
10701
|
+
onChange={() => {
|
|
10702
|
+
if (canToggle) {
|
|
10703
|
+
onToggle(bump.id, !isAdded, effectiveVariantId ?? undefined);
|
|
10704
|
+
}
|
|
10705
|
+
}}
|
|
10706
|
+
disabled={loading || !canToggle || isOos}
|
|
10707
|
+
className="mt-1 h-4 w-4 shrink-0 rounded"
|
|
10708
|
+
/>
|
|
10164
10709
|
|
|
10165
|
-
|
|
10166
|
-
|
|
10167
|
-
|
|
10168
|
-
|
|
10169
|
-
|
|
10710
|
+
{/* Image */}
|
|
10711
|
+
{imageUrl && (
|
|
10712
|
+
<div className="bg-muted relative h-10 w-10 shrink-0 overflow-hidden rounded">
|
|
10713
|
+
<Image src={imageUrl} alt={product.name} fill sizes="40px" className="object-cover" />
|
|
10714
|
+
</div>
|
|
10170
10715
|
)}
|
|
10171
|
-
|
|
10172
|
-
|
|
10173
|
-
|
|
10174
|
-
|
|
10175
|
-
|
|
10176
|
-
|
|
10716
|
+
|
|
10717
|
+
{/* Content */}
|
|
10718
|
+
<div className="min-w-0 flex-1">
|
|
10719
|
+
<p className="text-foreground text-sm font-medium">{bump.title}</p>
|
|
10720
|
+
{bump.description && (
|
|
10721
|
+
<p className="text-muted-foreground mt-0.5 text-xs">{bump.description}</p>
|
|
10722
|
+
)}
|
|
10723
|
+
|
|
10724
|
+
{/* Locked variant label */}
|
|
10725
|
+
{lockedLabel && <p className="text-muted-foreground mt-0.5 text-xs">{lockedLabel}</p>}
|
|
10726
|
+
|
|
10727
|
+
{/* Price */}
|
|
10728
|
+
<div className="mt-1 flex items-center gap-2">
|
|
10729
|
+
{displayDiscounted != null ? (
|
|
10730
|
+
<>
|
|
10731
|
+
<span className="text-muted-foreground text-xs line-through">
|
|
10732
|
+
{formatPrice(displayOriginal, { currency }) as string}
|
|
10733
|
+
</span>
|
|
10734
|
+
<span className="text-foreground text-sm font-semibold">
|
|
10735
|
+
{formatPrice(displayDiscounted, { currency }) as string}
|
|
10736
|
+
</span>
|
|
10737
|
+
</>
|
|
10738
|
+
) : (
|
|
10177
10739
|
<span className="text-foreground text-sm font-semibold">
|
|
10178
|
-
{formatPrice(
|
|
10740
|
+
{formatPrice(displayOriginal, { currency }) as string}
|
|
10179
10741
|
</span>
|
|
10180
|
-
|
|
10181
|
-
|
|
10182
|
-
|
|
10183
|
-
|
|
10184
|
-
|
|
10742
|
+
)}
|
|
10743
|
+
{requiresSelection && !effectiveVariantId && (
|
|
10744
|
+
<span className="text-muted-foreground text-xs">
|
|
10745
|
+
{t('selectOptions') || 'Select options'}
|
|
10746
|
+
</span>
|
|
10747
|
+
)}
|
|
10748
|
+
</div>
|
|
10749
|
+
</div>
|
|
10750
|
+
</label>
|
|
10751
|
+
|
|
10752
|
+
{/* Compact variant selector */}
|
|
10753
|
+
{requiresSelection && !isAdded && (
|
|
10754
|
+
<div className="ms-7 mt-2 space-y-1.5">
|
|
10755
|
+
{attributeGroups.map((group) => (
|
|
10756
|
+
<div key={group.name} className="flex flex-wrap items-center gap-1.5">
|
|
10757
|
+
<span className="text-muted-foreground text-xs">{group.name}:</span>
|
|
10758
|
+
{group.values.map((value) => {
|
|
10759
|
+
const isSelected = selectedAttrs[group.name] === value;
|
|
10760
|
+
// Check if this value leads to any available variant
|
|
10761
|
+
const variantForValue = variants?.find((v) => {
|
|
10762
|
+
const opts = getVariantOptions(v as any);
|
|
10763
|
+
const matchesValue = opts.some((o) => o.name === group.name && o.value === value);
|
|
10764
|
+
if (!matchesValue) return false;
|
|
10765
|
+
// Check other selected attrs
|
|
10766
|
+
return Object.entries(selectedAttrs).every(([k, sv]) => {
|
|
10767
|
+
if (k === group.name) return true;
|
|
10768
|
+
return opts.some((o) => o.name === k && o.value === sv);
|
|
10769
|
+
});
|
|
10770
|
+
});
|
|
10771
|
+
const isVariantOos =
|
|
10772
|
+
variantForValue?.inventory?.trackingMode !== 'NOT_TRACKED' &&
|
|
10773
|
+
variantForValue?.inventory?.available != null &&
|
|
10774
|
+
variantForValue.inventory.available <= 0;
|
|
10775
|
+
|
|
10776
|
+
return (
|
|
10777
|
+
<button
|
|
10778
|
+
key={value}
|
|
10779
|
+
type="button"
|
|
10780
|
+
onClick={() => handleAttrSelect(group.name, value)}
|
|
10781
|
+
disabled={isVariantOos}
|
|
10782
|
+
className={cn(
|
|
10783
|
+
'rounded-full border px-2.5 py-0.5 text-xs transition-colors',
|
|
10784
|
+
isSelected
|
|
10785
|
+
? 'border-primary bg-primary text-primary-foreground'
|
|
10786
|
+
: 'border-border text-foreground hover:border-primary/50',
|
|
10787
|
+
isVariantOos && 'cursor-not-allowed line-through opacity-40'
|
|
10788
|
+
)}
|
|
10789
|
+
>
|
|
10790
|
+
{value}
|
|
10791
|
+
</button>
|
|
10792
|
+
);
|
|
10793
|
+
})}
|
|
10794
|
+
</div>
|
|
10795
|
+
))}
|
|
10796
|
+
{isOos && effectiveVariantId && (
|
|
10797
|
+
<p className="text-destructive text-xs">{t('outOfStock') || 'Out of stock'}</p>
|
|
10185
10798
|
)}
|
|
10186
10799
|
</div>
|
|
10187
|
-
|
|
10188
|
-
</
|
|
10800
|
+
)}
|
|
10801
|
+
</div>
|
|
10189
10802
|
);
|
|
10190
10803
|
}
|
|
10191
10804
|
`,
|
|
@@ -10548,7 +11161,8 @@ var DETAILED = `# Required Pages \u2014 Detailed Guide
|
|
|
10548
11161
|
- Step 1: Customer info (email, name)
|
|
10549
11162
|
- Step 2: Shipping address form
|
|
10550
11163
|
- Step 3: Shipping method selection (from rates returned by \`setShippingAddress()\`)
|
|
10551
|
-
- Step 4: Payment (auto-detects Stripe/Grow/PayPal via \`getPaymentProviders()\`)
|
|
11164
|
+
- Step 4: Payment (auto-detects Stripe/Grow/PayPal/Sandbox via \`getPaymentProviders()\`)
|
|
11165
|
+
- 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.
|
|
10552
11166
|
- Order summary sidebar showing \`checkout.lineItems\` (NOT cart.items!)
|
|
10553
11167
|
- Tax display after address entry
|
|
10554
11168
|
- Reservation countdown
|
|
@@ -10628,46 +11242,39 @@ async function handleGetRequiredPages(args) {
|
|
|
10628
11242
|
var import_zod5 = require("zod");
|
|
10629
11243
|
|
|
10630
11244
|
// src/utils/fetch-store-info.ts
|
|
10631
|
-
var import_https = __toESM(require("https"));
|
|
10632
|
-
var import_http = __toESM(require("http"));
|
|
10633
11245
|
async function fetchStoreInfo(connectionId, baseUrl = "https://api.brainerce.com") {
|
|
10634
11246
|
const url = `${baseUrl}/api/vc/${connectionId}/info`;
|
|
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
|
-
req.on("timeout", () => {
|
|
10667
|
-
req.destroy();
|
|
10668
|
-
reject(new Error("Request timed out"));
|
|
10669
|
-
});
|
|
10670
|
-
});
|
|
11247
|
+
const controller = new AbortController();
|
|
11248
|
+
const timeout = setTimeout(() => controller.abort(), 1e4);
|
|
11249
|
+
let res;
|
|
11250
|
+
try {
|
|
11251
|
+
res = await fetch(url, { signal: controller.signal });
|
|
11252
|
+
} catch (err) {
|
|
11253
|
+
if (err.name === "AbortError") {
|
|
11254
|
+
throw new Error("Request timed out");
|
|
11255
|
+
}
|
|
11256
|
+
throw new Error(`Failed to connect to Brainerce API: ${err.message}`);
|
|
11257
|
+
} finally {
|
|
11258
|
+
clearTimeout(timeout);
|
|
11259
|
+
}
|
|
11260
|
+
if (res.status === 404) {
|
|
11261
|
+
throw new Error(`Connection ID "${connectionId}" not found. Check your dashboard.`);
|
|
11262
|
+
}
|
|
11263
|
+
if (!res.ok) {
|
|
11264
|
+
throw new Error(`API returned status ${res.status}`);
|
|
11265
|
+
}
|
|
11266
|
+
let json;
|
|
11267
|
+
try {
|
|
11268
|
+
json = await res.json();
|
|
11269
|
+
} catch {
|
|
11270
|
+
throw new Error("Invalid response from API");
|
|
11271
|
+
}
|
|
11272
|
+
return {
|
|
11273
|
+
name: json.name || json.storeName || "My Store",
|
|
11274
|
+
currency: json.currency || "USD",
|
|
11275
|
+
language: json.language || "en",
|
|
11276
|
+
...json.i18n ? { i18n: json.i18n } : {}
|
|
11277
|
+
};
|
|
10671
11278
|
}
|
|
10672
11279
|
|
|
10673
11280
|
// src/tools/get-store-info.ts
|
|
@@ -10678,7 +11285,11 @@ var GET_STORE_INFO_SCHEMA = {
|
|
|
10678
11285
|
};
|
|
10679
11286
|
async function handleGetStoreInfo(args) {
|
|
10680
11287
|
try {
|
|
10681
|
-
const
|
|
11288
|
+
const baseUrl = (process.env.BRAINERCE_API_URL || "https://api.brainerce.com").replace(
|
|
11289
|
+
/\/$/,
|
|
11290
|
+
""
|
|
11291
|
+
);
|
|
11292
|
+
const info = await fetchStoreInfo(args.connectionId, baseUrl);
|
|
10682
11293
|
return {
|
|
10683
11294
|
content: [
|
|
10684
11295
|
{
|
|
@@ -10709,41 +11320,32 @@ async function handleGetStoreInfo(args) {
|
|
|
10709
11320
|
var import_zod6 = require("zod");
|
|
10710
11321
|
|
|
10711
11322
|
// src/utils/fetch-store-capabilities.ts
|
|
10712
|
-
var import_https2 = __toESM(require("https"));
|
|
10713
|
-
var import_http2 = __toESM(require("http"));
|
|
10714
11323
|
async function fetchStoreCapabilities(connectionId, baseUrl = "https://api.brainerce.com") {
|
|
10715
11324
|
const url = `${baseUrl}/api/vc/${connectionId}/capabilities`;
|
|
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
|
-
reject(new Error(`Failed to connect to Brainerce API: ${err.message}`));
|
|
10741
|
-
});
|
|
10742
|
-
req.on("timeout", () => {
|
|
10743
|
-
req.destroy();
|
|
10744
|
-
reject(new Error("Request timed out"));
|
|
10745
|
-
});
|
|
10746
|
-
});
|
|
11325
|
+
const controller = new AbortController();
|
|
11326
|
+
const timeout = setTimeout(() => controller.abort(), 1e4);
|
|
11327
|
+
let res;
|
|
11328
|
+
try {
|
|
11329
|
+
res = await fetch(url, { signal: controller.signal });
|
|
11330
|
+
} catch (err) {
|
|
11331
|
+
if (err.name === "AbortError") {
|
|
11332
|
+
throw new Error("Request timed out");
|
|
11333
|
+
}
|
|
11334
|
+
throw new Error(`Failed to connect to Brainerce API: ${err.message}`);
|
|
11335
|
+
} finally {
|
|
11336
|
+
clearTimeout(timeout);
|
|
11337
|
+
}
|
|
11338
|
+
if (res.status === 404) {
|
|
11339
|
+
throw new Error(`Connection ID "${connectionId}" not found. Check your dashboard.`);
|
|
11340
|
+
}
|
|
11341
|
+
if (!res.ok) {
|
|
11342
|
+
throw new Error(`API returned status ${res.status}`);
|
|
11343
|
+
}
|
|
11344
|
+
try {
|
|
11345
|
+
return await res.json();
|
|
11346
|
+
} catch {
|
|
11347
|
+
throw new Error("Invalid response from API");
|
|
11348
|
+
}
|
|
10747
11349
|
}
|
|
10748
11350
|
|
|
10749
11351
|
// src/tools/get-store-capabilities.ts
|
|
@@ -10756,6 +11358,11 @@ function formatCapabilities(caps) {
|
|
|
10756
11358
|
const lines = [];
|
|
10757
11359
|
lines.push(`## Store: ${caps.store.name} (${caps.store.currency})`);
|
|
10758
11360
|
lines.push(`Language: ${caps.store.language}`);
|
|
11361
|
+
if (caps.store.i18n?.enabled) {
|
|
11362
|
+
lines.push(
|
|
11363
|
+
`Multi-language: ENABLED \u2014 locales: ${caps.store.i18n.supportedLocales.join(", ")} (default: ${caps.store.i18n.defaultLocale})`
|
|
11364
|
+
);
|
|
11365
|
+
}
|
|
10759
11366
|
lines.push("");
|
|
10760
11367
|
lines.push("## Configured Features");
|
|
10761
11368
|
if (caps.features.paymentProviders.length > 0) {
|
|
@@ -10790,6 +11397,9 @@ function formatCapabilities(caps) {
|
|
|
10790
11397
|
lines.push(
|
|
10791
11398
|
`- Email verification: ${caps.connection.requireEmailVerification ? "required" : "not required"}`
|
|
10792
11399
|
);
|
|
11400
|
+
lines.push(
|
|
11401
|
+
`- Sandbox payments: ${caps.connection.sandboxPaymentsEnabled ? "enabled (test orders without real payment)" : "disabled"}`
|
|
11402
|
+
);
|
|
10793
11403
|
lines.push(
|
|
10794
11404
|
`- Inventory reservation: ${caps.connection.reservationStrategy} (${caps.connection.reservationTimeout} min timeout)`
|
|
10795
11405
|
);
|
|
@@ -10829,11 +11439,21 @@ function formatCapabilities(caps) {
|
|
|
10829
11439
|
'Email verification is required \u2014 ALWAYS build the verify-email page. Use get-code-example("verify-email").'
|
|
10830
11440
|
);
|
|
10831
11441
|
}
|
|
11442
|
+
if (caps.connection.sandboxPaymentsEnabled) {
|
|
11443
|
+
suggestions.push(
|
|
11444
|
+
`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.`
|
|
11445
|
+
);
|
|
11446
|
+
}
|
|
10832
11447
|
if (caps.connection.reservationStrategy !== "ON_PAYMENT") {
|
|
10833
11448
|
suggestions.push(
|
|
10834
11449
|
'Inventory reservation is active \u2014 show a countdown timer. Use get-code-example("reservation-countdown").'
|
|
10835
11450
|
);
|
|
10836
11451
|
}
|
|
11452
|
+
if (caps.store.i18n?.enabled && caps.store.i18n.supportedLocales.length > 1) {
|
|
11453
|
+
suggestions.push(
|
|
11454
|
+
`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.`
|
|
11455
|
+
);
|
|
11456
|
+
}
|
|
10837
11457
|
if (suggestions.length === 0) {
|
|
10838
11458
|
suggestions.push(
|
|
10839
11459
|
"Start by building the required pages. Use get-required-pages() for the checklist."
|
|
@@ -10844,7 +11464,11 @@ function formatCapabilities(caps) {
|
|
|
10844
11464
|
}
|
|
10845
11465
|
async function handleGetStoreCapabilities(args) {
|
|
10846
11466
|
try {
|
|
10847
|
-
const
|
|
11467
|
+
const baseUrl = (process.env.BRAINERCE_API_URL || "https://api.brainerce.com").replace(
|
|
11468
|
+
/\/$/,
|
|
11469
|
+
""
|
|
11470
|
+
);
|
|
11471
|
+
const caps = await fetchStoreCapabilities(args.connectionId, baseUrl);
|
|
10848
11472
|
return {
|
|
10849
11473
|
content: [{ type: "text", text: formatCapabilities(caps) }]
|
|
10850
11474
|
};
|
|
@@ -10941,6 +11565,20 @@ function formatCapabilitiesSummary(caps) {
|
|
|
10941
11565
|
const lines = [];
|
|
10942
11566
|
lines.push(`## Store: ${caps.store.name}`);
|
|
10943
11567
|
lines.push(`Currency: ${caps.store.currency} | Language: ${caps.store.language}`);
|
|
11568
|
+
if (caps.store.i18n?.enabled && caps.store.i18n.supportedLocales.length > 1) {
|
|
11569
|
+
lines.push(
|
|
11570
|
+
`Multi-language: ${caps.store.i18n.supportedLocales.join(", ")} (default: ${caps.store.i18n.defaultLocale})`
|
|
11571
|
+
);
|
|
11572
|
+
lines.push("");
|
|
11573
|
+
lines.push("### i18n Implementation");
|
|
11574
|
+
lines.push(
|
|
11575
|
+
"- Call `client.setLocale(locale)` at app init based on URL prefix or user preference"
|
|
11576
|
+
);
|
|
11577
|
+
lines.push("- All getProducts/getCategories/getBrands calls auto-include the locale");
|
|
11578
|
+
lines.push("- Use locale-prefixed routes: `/[locale]/products`, `/[locale]/products/[slug]`");
|
|
11579
|
+
lines.push("- Add a language switcher component in the header");
|
|
11580
|
+
lines.push('- RTL locales (he, ar): set `<html dir="rtl">` \u2014 flexbox reversal is automatic');
|
|
11581
|
+
}
|
|
10944
11582
|
lines.push("");
|
|
10945
11583
|
lines.push("### Configured Features");
|
|
10946
11584
|
if (caps.features.paymentProviders.length > 0) {
|
|
@@ -10982,6 +11620,9 @@ function formatCapabilitiesSummary(caps) {
|
|
|
10982
11620
|
lines.push(
|
|
10983
11621
|
`- Guest checkout tracking: ${caps.connection.guestCheckoutTracking ? "enabled" : "disabled"}`
|
|
10984
11622
|
);
|
|
11623
|
+
lines.push(
|
|
11624
|
+
`- Sandbox payments: ${caps.connection.sandboxPaymentsEnabled ? "ENABLED \u2014 payment step shows test order button (no real charges)" : "disabled"}`
|
|
11625
|
+
);
|
|
10985
11626
|
return lines.join("\n");
|
|
10986
11627
|
}
|
|
10987
11628
|
function buildStoreBundle(options) {
|
|
@@ -10991,8 +11632,7 @@ function buildStoreBundle(options) {
|
|
|
10991
11632
|
connectionId,
|
|
10992
11633
|
storeName,
|
|
10993
11634
|
currency,
|
|
10994
|
-
language: capabilities?.store.language || "en"
|
|
10995
|
-
apiUrl: "https://api.brainerce.com"
|
|
11635
|
+
language: capabilities?.store.language || "en"
|
|
10996
11636
|
};
|
|
10997
11637
|
const sections = [];
|
|
10998
11638
|
sections.push(`# Build: ${storeName} \u2014 ${storeType}${styleDesc}
|
|
@@ -11093,9 +11733,13 @@ var BUILD_STORE_SCHEMA = {
|
|
|
11093
11733
|
storeStyle: import_zod7.z.string().optional().describe('Design style (e.g., "minimalist", "luxury", "playful", "dark mode")')
|
|
11094
11734
|
};
|
|
11095
11735
|
async function handleBuildStore(args) {
|
|
11736
|
+
const baseUrl = (process.env.BRAINERCE_API_URL || "https://api.brainerce.com").replace(
|
|
11737
|
+
/\/$/,
|
|
11738
|
+
""
|
|
11739
|
+
);
|
|
11096
11740
|
let capabilities = null;
|
|
11097
11741
|
try {
|
|
11098
|
-
capabilities = await fetchStoreCapabilities(args.connectionId);
|
|
11742
|
+
capabilities = await fetchStoreCapabilities(args.connectionId, baseUrl);
|
|
11099
11743
|
} catch {
|
|
11100
11744
|
}
|
|
11101
11745
|
const bundle = buildStoreBundle({
|
|
@@ -11434,6 +12078,57 @@ ${results.join("\n\n---\n\n")}`
|
|
|
11434
12078
|
};
|
|
11435
12079
|
}
|
|
11436
12080
|
|
|
12081
|
+
// src/tools/get-integration-guide.ts
|
|
12082
|
+
var import_zod10 = require("zod");
|
|
12083
|
+
var GET_INTEGRATION_GUIDE_NAME = "get-integration-guide";
|
|
12084
|
+
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.';
|
|
12085
|
+
var GET_INTEGRATION_GUIDE_SCHEMA = {
|
|
12086
|
+
part: import_zod10.z.enum(["core", "optional", "rules"]).describe(
|
|
12087
|
+
'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.'
|
|
12088
|
+
)
|
|
12089
|
+
};
|
|
12090
|
+
var GUIDE_URLS = {
|
|
12091
|
+
core: "https://brainerce.com/docs/integration/raw?part=core",
|
|
12092
|
+
optional: "https://brainerce.com/docs/integration/raw?part=optional",
|
|
12093
|
+
rules: "https://brainerce.com/docs/integration/raw?part=rules"
|
|
12094
|
+
};
|
|
12095
|
+
async function handleGetIntegrationGuide(args) {
|
|
12096
|
+
const url = GUIDE_URLS[args.part];
|
|
12097
|
+
if (!url) {
|
|
12098
|
+
return {
|
|
12099
|
+
content: [
|
|
12100
|
+
{
|
|
12101
|
+
type: "text",
|
|
12102
|
+
text: `Unknown part: "${args.part}". Available parts: core, optional, rules.`
|
|
12103
|
+
}
|
|
12104
|
+
]
|
|
12105
|
+
};
|
|
12106
|
+
}
|
|
12107
|
+
try {
|
|
12108
|
+
const response = await fetch(url, {
|
|
12109
|
+
headers: { Accept: "text/plain" },
|
|
12110
|
+
signal: AbortSignal.timeout(15e3)
|
|
12111
|
+
});
|
|
12112
|
+
if (!response.ok) {
|
|
12113
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
12114
|
+
}
|
|
12115
|
+
const content = await response.text();
|
|
12116
|
+
return {
|
|
12117
|
+
content: [{ type: "text", text: content }]
|
|
12118
|
+
};
|
|
12119
|
+
} catch (error) {
|
|
12120
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
12121
|
+
return {
|
|
12122
|
+
content: [
|
|
12123
|
+
{
|
|
12124
|
+
type: "text",
|
|
12125
|
+
text: `Failed to fetch integration guide (${args.part}): ${message}. You can read it directly at: ${url}`
|
|
12126
|
+
}
|
|
12127
|
+
]
|
|
12128
|
+
};
|
|
12129
|
+
}
|
|
12130
|
+
}
|
|
12131
|
+
|
|
11437
12132
|
// src/resources/sdk-types.ts
|
|
11438
12133
|
var SDK_TYPES_URI = "brainerce://sdk/types";
|
|
11439
12134
|
var SDK_TYPES_NAME = "Brainerce SDK Types";
|
|
@@ -11588,15 +12283,15 @@ async function handleProjectTemplate(uri) {
|
|
|
11588
12283
|
}
|
|
11589
12284
|
|
|
11590
12285
|
// src/prompts/create-store.ts
|
|
11591
|
-
var
|
|
12286
|
+
var import_zod11 = require("zod");
|
|
11592
12287
|
var CREATE_STORE_NAME = "create-store";
|
|
11593
12288
|
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.";
|
|
11594
12289
|
var CREATE_STORE_SCHEMA = {
|
|
11595
|
-
connectionId:
|
|
11596
|
-
storeName:
|
|
11597
|
-
storeType:
|
|
11598
|
-
currency:
|
|
11599
|
-
storeStyle:
|
|
12290
|
+
connectionId: import_zod11.z.string().describe("Vibe-coded connection ID (starts with vc_)"),
|
|
12291
|
+
storeName: import_zod11.z.string().describe('Name of the store (e.g., "Urban Threads")'),
|
|
12292
|
+
storeType: import_zod11.z.string().describe('Type of store (e.g., "Clothing store", "Electronics shop", "Bakery")'),
|
|
12293
|
+
currency: import_zod11.z.string().optional().default("USD").describe("Store currency code (e.g., USD, ILS, EUR)"),
|
|
12294
|
+
storeStyle: import_zod11.z.string().optional().describe('Design style (e.g., "minimalist", "luxury", "playful", "dark mode")')
|
|
11600
12295
|
};
|
|
11601
12296
|
function handleCreateStore(args) {
|
|
11602
12297
|
const styleDesc = args.storeStyle ? ` with a ${args.storeStyle} design style` : "";
|
|
@@ -11635,6 +12330,13 @@ DO NOT STOP until all 13 pages + header are implemented:
|
|
|
11635
12330
|
13. \`/account\` \u2014 Account dashboard
|
|
11636
12331
|
14. Header component with cart count + search
|
|
11637
12332
|
|
|
12333
|
+
## Multi-Language
|
|
12334
|
+
If the store has multi-language enabled (check build-store output), implement:
|
|
12335
|
+
- Locale-prefixed routes: /[locale]/products instead of /products
|
|
12336
|
+
- Call client.setLocale(locale) based on URL prefix
|
|
12337
|
+
- Language switcher in header
|
|
12338
|
+
- dir="rtl" on <html> for RTL locales (he, ar)
|
|
12339
|
+
|
|
11638
12340
|
## Step 3: Validate
|
|
11639
12341
|
After building all pages, call \`validate-store\` with the list of files you created to verify nothing is missing.
|
|
11640
12342
|
|
|
@@ -11653,11 +12355,11 @@ After building all pages, call \`validate-store\` with the list of files you cre
|
|
|
11653
12355
|
}
|
|
11654
12356
|
|
|
11655
12357
|
// src/prompts/add-feature.ts
|
|
11656
|
-
var
|
|
12358
|
+
var import_zod12 = require("zod");
|
|
11657
12359
|
var ADD_FEATURE_NAME = "add-feature";
|
|
11658
12360
|
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.";
|
|
11659
12361
|
var ADD_FEATURE_SCHEMA = {
|
|
11660
|
-
feature:
|
|
12362
|
+
feature: import_zod12.z.enum([
|
|
11661
12363
|
"products",
|
|
11662
12364
|
"cart",
|
|
11663
12365
|
"checkout",
|
|
@@ -11670,8 +12372,8 @@ var ADD_FEATURE_SCHEMA = {
|
|
|
11670
12372
|
"search",
|
|
11671
12373
|
"tax"
|
|
11672
12374
|
]).describe("The feature to add"),
|
|
11673
|
-
connectionId:
|
|
11674
|
-
currency:
|
|
12375
|
+
connectionId: import_zod12.z.string().optional().describe("Vibe-coded connection ID (starts with vc_)"),
|
|
12376
|
+
currency: import_zod12.z.string().optional().default("USD").describe("Store currency code")
|
|
11675
12377
|
};
|
|
11676
12378
|
var FEATURE_TO_TOPICS = {
|
|
11677
12379
|
products: { topics: ["products"], domains: ["products"] },
|
|
@@ -11786,6 +12488,12 @@ function createServer() {
|
|
|
11786
12488
|
GET_PAGE_CODE_SCHEMA,
|
|
11787
12489
|
handleGetPageCode
|
|
11788
12490
|
);
|
|
12491
|
+
server.tool(
|
|
12492
|
+
GET_INTEGRATION_GUIDE_NAME,
|
|
12493
|
+
GET_INTEGRATION_GUIDE_DESCRIPTION,
|
|
12494
|
+
GET_INTEGRATION_GUIDE_SCHEMA,
|
|
12495
|
+
handleGetIntegrationGuide
|
|
12496
|
+
);
|
|
11789
12497
|
server.resource(
|
|
11790
12498
|
SDK_TYPES_NAME,
|
|
11791
12499
|
SDK_TYPES_URI,
|