@brainerce/mcp-server 2.1.1 → 2.4.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 +87 -12
- package/dist/bin/stdio.js +87 -12
- package/dist/index.d.mts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +87 -12
- package/dist/index.mjs +87 -12
- package/package.json +1 -1
package/dist/bin/http.js
CHANGED
|
@@ -314,6 +314,20 @@ function CheckoutPage() {
|
|
|
314
314
|
if (!paymentData) return <div>Unable to initialize payment</div>;
|
|
315
315
|
|
|
316
316
|
// Render payment UI based on provider
|
|
317
|
+
if (paymentData.provider === 'sandbox') {
|
|
318
|
+
return (
|
|
319
|
+
<div className="text-center p-6 bg-amber-50 border border-amber-200 rounded-lg">
|
|
320
|
+
<h3 className="font-semibold mb-2">Test Mode</h3>
|
|
321
|
+
<p className="text-sm text-gray-600 mb-4">No real payment will be charged.</p>
|
|
322
|
+
<button onClick={async () => {
|
|
323
|
+
await client.completeGuestCheckout(checkoutId);
|
|
324
|
+
window.location.href = \\\`/order-confirmation?checkout_id=\\\${checkoutId}\\\`;
|
|
325
|
+
}} className="bg-amber-500 text-white px-6 py-2 rounded">
|
|
326
|
+
Complete Test Order
|
|
327
|
+
</button>
|
|
328
|
+
</div>
|
|
329
|
+
);
|
|
330
|
+
}
|
|
317
331
|
if (paymentData.provider === 'grow') {
|
|
318
332
|
return <GrowPaymentForm paymentUrl={paymentData.clientSecret} checkoutId={paymentData.checkoutId} />;
|
|
319
333
|
}
|
|
@@ -432,11 +446,12 @@ const growProvider = providers.find(p => p.provider === 'grow');
|
|
|
432
446
|
const paypalProvider = providers.find(p => p.provider === 'paypal');
|
|
433
447
|
\`\`\`
|
|
434
448
|
|
|
435
|
-
Each provider has: \`id\`, \`provider\` ('stripe'|'grow'|'paypal'), \`name\`, \`publicKey\`, \`stripeAccountId\` (Stripe only), \`supportedMethods\`, \`testMode\`, \`isDefault\`.
|
|
449
|
+
Each provider has: \`id\`, \`provider\` ('stripe'|'grow'|'paypal'|'sandbox'), \`name\`, \`publicKey\`, \`stripeAccountId\` (Stripe only), \`supportedMethods\`, \`testMode\`, \`isDefault\`.
|
|
436
450
|
|
|
437
451
|
- **Stripe:** \`npm install @stripe/stripe-js @stripe/react-stripe-js\` \u2014 \`loadStripe(publicKey, { stripeAccount })\`
|
|
438
452
|
- **Grow:** No SDK \u2014 uses iframe with payment URL. Supports credit cards, Bit, Apple Pay, Google Pay.
|
|
439
|
-
- **PayPal:** \`npm install @paypal/react-paypal-js\` \u2014 \`PayPalScriptProvider\` + \`PayPalButtons
|
|
453
|
+
- **PayPal:** \`npm install @paypal/react-paypal-js\` \u2014 \`PayPalScriptProvider\` + \`PayPalButtons\`
|
|
454
|
+
- **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\`.`;
|
|
440
455
|
}
|
|
441
456
|
function getProductsSection(_currency) {
|
|
442
457
|
return `## Products & Variants
|
|
@@ -3157,7 +3172,12 @@ export default function CartPage() {
|
|
|
3157
3172
|
<div className="mt-6 space-y-3">
|
|
3158
3173
|
<h3 className="text-foreground text-sm font-semibold">{t('bundleOffers')}</h3>
|
|
3159
3174
|
{bundles.bundles.map((offer) => (
|
|
3160
|
-
<CartBundleOfferCard
|
|
3175
|
+
<CartBundleOfferCard
|
|
3176
|
+
key={offer.id}
|
|
3177
|
+
offer={offer}
|
|
3178
|
+
cartId={cart.id}
|
|
3179
|
+
onAdd={refreshCart}
|
|
3180
|
+
/>
|
|
3161
3181
|
))}
|
|
3162
3182
|
</div>
|
|
3163
3183
|
)}
|
|
@@ -7654,8 +7674,9 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
|
|
|
7654
7674
|
})
|
|
7655
7675
|
.catch(() => null);
|
|
7656
7676
|
|
|
7657
|
-
// B) Load + init SDK as early as possible
|
|
7677
|
+
// B) Load + init SDK as early as possible (skip for sandbox)
|
|
7658
7678
|
providerPromise.then((providerSdk) => {
|
|
7679
|
+
if (providerSdk?.renderType === 'sandbox') return;
|
|
7659
7680
|
if (providerSdk?.renderType === 'sdk-widget' && providerSdk.scriptUrl) {
|
|
7660
7681
|
currentSdk = providerSdk;
|
|
7661
7682
|
loadScript(providerSdk);
|
|
@@ -7682,6 +7703,9 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
|
|
|
7682
7703
|
const sdk = resolveClientSdk(intent, providerSdk);
|
|
7683
7704
|
currentSdk = sdk;
|
|
7684
7705
|
|
|
7706
|
+
// Sandbox mode \u2014 no SDK to load, UI handles it
|
|
7707
|
+
if (sdk.renderType === 'sandbox') return;
|
|
7708
|
+
|
|
7685
7709
|
if (sdk.renderType === 'redirect') {
|
|
7686
7710
|
window.location.href = intent.clientSecret;
|
|
7687
7711
|
return;
|
|
@@ -7776,6 +7800,48 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
|
|
|
7776
7800
|
|
|
7777
7801
|
const sdk = resolveClientSdk(paymentIntent, preloadedSdk);
|
|
7778
7802
|
|
|
7803
|
+
if (sdk.renderType === 'sandbox') {
|
|
7804
|
+
const handleCompleteSandbox = async () => {
|
|
7805
|
+
setLoading(true);
|
|
7806
|
+
try {
|
|
7807
|
+
const client = getClient();
|
|
7808
|
+
await client.completeGuestCheckout(checkoutId);
|
|
7809
|
+
window.location.href = \`/order-confirmation?checkout_id=\${checkoutId}\`;
|
|
7810
|
+
} catch (err) {
|
|
7811
|
+
setError(err instanceof Error ? err.message : t('paymentError'));
|
|
7812
|
+
setLoading(false);
|
|
7813
|
+
}
|
|
7814
|
+
};
|
|
7815
|
+
|
|
7816
|
+
return (
|
|
7817
|
+
<div className={cn('py-8 text-center', className)}>
|
|
7818
|
+
<div className="mx-auto max-w-md rounded-lg border border-amber-200 bg-amber-50 p-6">
|
|
7819
|
+
<svg
|
|
7820
|
+
className="mx-auto mb-3 h-10 w-10 text-amber-500"
|
|
7821
|
+
fill="none"
|
|
7822
|
+
viewBox="0 0 24 24"
|
|
7823
|
+
stroke="currentColor"
|
|
7824
|
+
>
|
|
7825
|
+
<path
|
|
7826
|
+
strokeLinecap="round"
|
|
7827
|
+
strokeLinejoin="round"
|
|
7828
|
+
strokeWidth={1.5}
|
|
7829
|
+
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4.5c-.77-.833-2.694-.833-3.464 0L3.34 16.5c-.77.833.192 2.5 1.732 2.5z"
|
|
7830
|
+
/>
|
|
7831
|
+
</svg>
|
|
7832
|
+
<h3 className="text-foreground mb-1 text-lg font-semibold">{t('sandboxTitle')}</h3>
|
|
7833
|
+
<p className="text-muted-foreground mb-4 text-sm">{t('sandboxDescription')}</p>
|
|
7834
|
+
<button
|
|
7835
|
+
onClick={handleCompleteSandbox}
|
|
7836
|
+
className="inline-flex items-center rounded-md bg-amber-500 px-6 py-2.5 text-sm font-medium text-white transition-colors hover:bg-amber-600"
|
|
7837
|
+
>
|
|
7838
|
+
{t('completeTestOrder')}
|
|
7839
|
+
</button>
|
|
7840
|
+
</div>
|
|
7841
|
+
</div>
|
|
7842
|
+
);
|
|
7843
|
+
}
|
|
7844
|
+
|
|
7779
7845
|
if (sdk.renderType === 'sdk-widget') {
|
|
7780
7846
|
const containerId =
|
|
7781
7847
|
sdk.containerId || \`\${paymentIntent.provider || 'payment'}-payment-container\`;
|
|
@@ -9933,18 +9999,18 @@ import { useState } from 'react';
|
|
|
9933
9999
|
import Image from 'next/image';
|
|
9934
10000
|
import type { CartBundleOffer as CartBundleOfferType } from 'brainerce';
|
|
9935
10001
|
import { formatPrice } from 'brainerce';
|
|
9936
|
-
import { getClient } from '@/lib/brainerce';
|
|
9937
10002
|
import { useStoreInfo } from '@/providers/store-provider';
|
|
9938
10003
|
import { useTranslations } from '@/lib/translations';
|
|
9939
10004
|
import { cn } from '@/lib/utils';
|
|
9940
10005
|
|
|
9941
10006
|
interface CartBundleOfferCardProps {
|
|
9942
10007
|
offer: CartBundleOfferType;
|
|
10008
|
+
cartId: string;
|
|
9943
10009
|
onAdd: () => void;
|
|
9944
10010
|
className?: string;
|
|
9945
10011
|
}
|
|
9946
10012
|
|
|
9947
|
-
export function CartBundleOfferCard({ offer, onAdd, className }: CartBundleOfferCardProps) {
|
|
10013
|
+
export function CartBundleOfferCard({ offer, cartId, onAdd, className }: CartBundleOfferCardProps) {
|
|
9948
10014
|
const { storeInfo } = useStoreInfo();
|
|
9949
10015
|
const t = useTranslations('cart');
|
|
9950
10016
|
const currency = storeInfo?.currency || 'USD';
|
|
@@ -9968,12 +10034,9 @@ export function CartBundleOfferCard({ offer, onAdd, className }: CartBundleOffer
|
|
|
9968
10034
|
if (adding) return;
|
|
9969
10035
|
try {
|
|
9970
10036
|
setAdding(true);
|
|
10037
|
+
const { getClient } = await import('@/lib/brainerce');
|
|
9971
10038
|
const client = getClient();
|
|
9972
|
-
await client.
|
|
9973
|
-
productId: product.id,
|
|
9974
|
-
variantId: offer.bundleVariantId || undefined,
|
|
9975
|
-
quantity: 1,
|
|
9976
|
-
});
|
|
10039
|
+
await client.addBundleToCart(cartId, offer.id);
|
|
9977
10040
|
onAdd();
|
|
9978
10041
|
} catch (err) {
|
|
9979
10042
|
console.error('Failed to add bundle item:', err);
|
|
@@ -10485,7 +10548,8 @@ var DETAILED = `# Required Pages \u2014 Detailed Guide
|
|
|
10485
10548
|
- Step 1: Customer info (email, name)
|
|
10486
10549
|
- Step 2: Shipping address form
|
|
10487
10550
|
- Step 3: Shipping method selection (from rates returned by \`setShippingAddress()\`)
|
|
10488
|
-
- Step 4: Payment (auto-detects Stripe/Grow/PayPal via \`getPaymentProviders()\`)
|
|
10551
|
+
- Step 4: Payment (auto-detects Stripe/Grow/PayPal/Sandbox via \`getPaymentProviders()\`)
|
|
10552
|
+
- 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.
|
|
10489
10553
|
- Order summary sidebar showing \`checkout.lineItems\` (NOT cart.items!)
|
|
10490
10554
|
- Tax display after address entry
|
|
10491
10555
|
- Reservation countdown
|
|
@@ -10727,6 +10791,9 @@ function formatCapabilities(caps) {
|
|
|
10727
10791
|
lines.push(
|
|
10728
10792
|
`- Email verification: ${caps.connection.requireEmailVerification ? "required" : "not required"}`
|
|
10729
10793
|
);
|
|
10794
|
+
lines.push(
|
|
10795
|
+
`- Sandbox payments: ${caps.connection.sandboxPaymentsEnabled ? "enabled (test orders without real payment)" : "disabled"}`
|
|
10796
|
+
);
|
|
10730
10797
|
lines.push(
|
|
10731
10798
|
`- Inventory reservation: ${caps.connection.reservationStrategy} (${caps.connection.reservationTimeout} min timeout)`
|
|
10732
10799
|
);
|
|
@@ -10766,6 +10833,11 @@ function formatCapabilities(caps) {
|
|
|
10766
10833
|
'Email verification is required \u2014 ALWAYS build the verify-email page. Use get-code-example("verify-email").'
|
|
10767
10834
|
);
|
|
10768
10835
|
}
|
|
10836
|
+
if (caps.connection.sandboxPaymentsEnabled) {
|
|
10837
|
+
suggestions.push(
|
|
10838
|
+
`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.`
|
|
10839
|
+
);
|
|
10840
|
+
}
|
|
10769
10841
|
if (caps.connection.reservationStrategy !== "ON_PAYMENT") {
|
|
10770
10842
|
suggestions.push(
|
|
10771
10843
|
'Inventory reservation is active \u2014 show a countdown timer. Use get-code-example("reservation-countdown").'
|
|
@@ -10919,6 +10991,9 @@ function formatCapabilitiesSummary(caps) {
|
|
|
10919
10991
|
lines.push(
|
|
10920
10992
|
`- Guest checkout tracking: ${caps.connection.guestCheckoutTracking ? "enabled" : "disabled"}`
|
|
10921
10993
|
);
|
|
10994
|
+
lines.push(
|
|
10995
|
+
`- Sandbox payments: ${caps.connection.sandboxPaymentsEnabled ? "ENABLED \u2014 payment step shows test order button (no real charges)" : "disabled"}`
|
|
10996
|
+
);
|
|
10922
10997
|
return lines.join("\n");
|
|
10923
10998
|
}
|
|
10924
10999
|
function buildStoreBundle(options) {
|
package/dist/bin/stdio.js
CHANGED
|
@@ -312,6 +312,20 @@ function CheckoutPage() {
|
|
|
312
312
|
if (!paymentData) return <div>Unable to initialize payment</div>;
|
|
313
313
|
|
|
314
314
|
// Render payment UI based on provider
|
|
315
|
+
if (paymentData.provider === 'sandbox') {
|
|
316
|
+
return (
|
|
317
|
+
<div className="text-center p-6 bg-amber-50 border border-amber-200 rounded-lg">
|
|
318
|
+
<h3 className="font-semibold mb-2">Test Mode</h3>
|
|
319
|
+
<p className="text-sm text-gray-600 mb-4">No real payment will be charged.</p>
|
|
320
|
+
<button onClick={async () => {
|
|
321
|
+
await client.completeGuestCheckout(checkoutId);
|
|
322
|
+
window.location.href = \\\`/order-confirmation?checkout_id=\\\${checkoutId}\\\`;
|
|
323
|
+
}} className="bg-amber-500 text-white px-6 py-2 rounded">
|
|
324
|
+
Complete Test Order
|
|
325
|
+
</button>
|
|
326
|
+
</div>
|
|
327
|
+
);
|
|
328
|
+
}
|
|
315
329
|
if (paymentData.provider === 'grow') {
|
|
316
330
|
return <GrowPaymentForm paymentUrl={paymentData.clientSecret} checkoutId={paymentData.checkoutId} />;
|
|
317
331
|
}
|
|
@@ -430,11 +444,12 @@ const growProvider = providers.find(p => p.provider === 'grow');
|
|
|
430
444
|
const paypalProvider = providers.find(p => p.provider === 'paypal');
|
|
431
445
|
\`\`\`
|
|
432
446
|
|
|
433
|
-
Each provider has: \`id\`, \`provider\` ('stripe'|'grow'|'paypal'), \`name\`, \`publicKey\`, \`stripeAccountId\` (Stripe only), \`supportedMethods\`, \`testMode\`, \`isDefault\`.
|
|
447
|
+
Each provider has: \`id\`, \`provider\` ('stripe'|'grow'|'paypal'|'sandbox'), \`name\`, \`publicKey\`, \`stripeAccountId\` (Stripe only), \`supportedMethods\`, \`testMode\`, \`isDefault\`.
|
|
434
448
|
|
|
435
449
|
- **Stripe:** \`npm install @stripe/stripe-js @stripe/react-stripe-js\` \u2014 \`loadStripe(publicKey, { stripeAccount })\`
|
|
436
450
|
- **Grow:** No SDK \u2014 uses iframe with payment URL. Supports credit cards, Bit, Apple Pay, Google Pay.
|
|
437
|
-
- **PayPal:** \`npm install @paypal/react-paypal-js\` \u2014 \`PayPalScriptProvider\` + \`PayPalButtons
|
|
451
|
+
- **PayPal:** \`npm install @paypal/react-paypal-js\` \u2014 \`PayPalScriptProvider\` + \`PayPalButtons\`
|
|
452
|
+
- **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\`.`;
|
|
438
453
|
}
|
|
439
454
|
function getProductsSection(_currency) {
|
|
440
455
|
return `## Products & Variants
|
|
@@ -3155,7 +3170,12 @@ export default function CartPage() {
|
|
|
3155
3170
|
<div className="mt-6 space-y-3">
|
|
3156
3171
|
<h3 className="text-foreground text-sm font-semibold">{t('bundleOffers')}</h3>
|
|
3157
3172
|
{bundles.bundles.map((offer) => (
|
|
3158
|
-
<CartBundleOfferCard
|
|
3173
|
+
<CartBundleOfferCard
|
|
3174
|
+
key={offer.id}
|
|
3175
|
+
offer={offer}
|
|
3176
|
+
cartId={cart.id}
|
|
3177
|
+
onAdd={refreshCart}
|
|
3178
|
+
/>
|
|
3159
3179
|
))}
|
|
3160
3180
|
</div>
|
|
3161
3181
|
)}
|
|
@@ -7652,8 +7672,9 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
|
|
|
7652
7672
|
})
|
|
7653
7673
|
.catch(() => null);
|
|
7654
7674
|
|
|
7655
|
-
// B) Load + init SDK as early as possible
|
|
7675
|
+
// B) Load + init SDK as early as possible (skip for sandbox)
|
|
7656
7676
|
providerPromise.then((providerSdk) => {
|
|
7677
|
+
if (providerSdk?.renderType === 'sandbox') return;
|
|
7657
7678
|
if (providerSdk?.renderType === 'sdk-widget' && providerSdk.scriptUrl) {
|
|
7658
7679
|
currentSdk = providerSdk;
|
|
7659
7680
|
loadScript(providerSdk);
|
|
@@ -7680,6 +7701,9 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
|
|
|
7680
7701
|
const sdk = resolveClientSdk(intent, providerSdk);
|
|
7681
7702
|
currentSdk = sdk;
|
|
7682
7703
|
|
|
7704
|
+
// Sandbox mode \u2014 no SDK to load, UI handles it
|
|
7705
|
+
if (sdk.renderType === 'sandbox') return;
|
|
7706
|
+
|
|
7683
7707
|
if (sdk.renderType === 'redirect') {
|
|
7684
7708
|
window.location.href = intent.clientSecret;
|
|
7685
7709
|
return;
|
|
@@ -7774,6 +7798,48 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
|
|
|
7774
7798
|
|
|
7775
7799
|
const sdk = resolveClientSdk(paymentIntent, preloadedSdk);
|
|
7776
7800
|
|
|
7801
|
+
if (sdk.renderType === 'sandbox') {
|
|
7802
|
+
const handleCompleteSandbox = async () => {
|
|
7803
|
+
setLoading(true);
|
|
7804
|
+
try {
|
|
7805
|
+
const client = getClient();
|
|
7806
|
+
await client.completeGuestCheckout(checkoutId);
|
|
7807
|
+
window.location.href = \`/order-confirmation?checkout_id=\${checkoutId}\`;
|
|
7808
|
+
} catch (err) {
|
|
7809
|
+
setError(err instanceof Error ? err.message : t('paymentError'));
|
|
7810
|
+
setLoading(false);
|
|
7811
|
+
}
|
|
7812
|
+
};
|
|
7813
|
+
|
|
7814
|
+
return (
|
|
7815
|
+
<div className={cn('py-8 text-center', className)}>
|
|
7816
|
+
<div className="mx-auto max-w-md rounded-lg border border-amber-200 bg-amber-50 p-6">
|
|
7817
|
+
<svg
|
|
7818
|
+
className="mx-auto mb-3 h-10 w-10 text-amber-500"
|
|
7819
|
+
fill="none"
|
|
7820
|
+
viewBox="0 0 24 24"
|
|
7821
|
+
stroke="currentColor"
|
|
7822
|
+
>
|
|
7823
|
+
<path
|
|
7824
|
+
strokeLinecap="round"
|
|
7825
|
+
strokeLinejoin="round"
|
|
7826
|
+
strokeWidth={1.5}
|
|
7827
|
+
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4.5c-.77-.833-2.694-.833-3.464 0L3.34 16.5c-.77.833.192 2.5 1.732 2.5z"
|
|
7828
|
+
/>
|
|
7829
|
+
</svg>
|
|
7830
|
+
<h3 className="text-foreground mb-1 text-lg font-semibold">{t('sandboxTitle')}</h3>
|
|
7831
|
+
<p className="text-muted-foreground mb-4 text-sm">{t('sandboxDescription')}</p>
|
|
7832
|
+
<button
|
|
7833
|
+
onClick={handleCompleteSandbox}
|
|
7834
|
+
className="inline-flex items-center rounded-md bg-amber-500 px-6 py-2.5 text-sm font-medium text-white transition-colors hover:bg-amber-600"
|
|
7835
|
+
>
|
|
7836
|
+
{t('completeTestOrder')}
|
|
7837
|
+
</button>
|
|
7838
|
+
</div>
|
|
7839
|
+
</div>
|
|
7840
|
+
);
|
|
7841
|
+
}
|
|
7842
|
+
|
|
7777
7843
|
if (sdk.renderType === 'sdk-widget') {
|
|
7778
7844
|
const containerId =
|
|
7779
7845
|
sdk.containerId || \`\${paymentIntent.provider || 'payment'}-payment-container\`;
|
|
@@ -9931,18 +9997,18 @@ import { useState } from 'react';
|
|
|
9931
9997
|
import Image from 'next/image';
|
|
9932
9998
|
import type { CartBundleOffer as CartBundleOfferType } from 'brainerce';
|
|
9933
9999
|
import { formatPrice } from 'brainerce';
|
|
9934
|
-
import { getClient } from '@/lib/brainerce';
|
|
9935
10000
|
import { useStoreInfo } from '@/providers/store-provider';
|
|
9936
10001
|
import { useTranslations } from '@/lib/translations';
|
|
9937
10002
|
import { cn } from '@/lib/utils';
|
|
9938
10003
|
|
|
9939
10004
|
interface CartBundleOfferCardProps {
|
|
9940
10005
|
offer: CartBundleOfferType;
|
|
10006
|
+
cartId: string;
|
|
9941
10007
|
onAdd: () => void;
|
|
9942
10008
|
className?: string;
|
|
9943
10009
|
}
|
|
9944
10010
|
|
|
9945
|
-
export function CartBundleOfferCard({ offer, onAdd, className }: CartBundleOfferCardProps) {
|
|
10011
|
+
export function CartBundleOfferCard({ offer, cartId, onAdd, className }: CartBundleOfferCardProps) {
|
|
9946
10012
|
const { storeInfo } = useStoreInfo();
|
|
9947
10013
|
const t = useTranslations('cart');
|
|
9948
10014
|
const currency = storeInfo?.currency || 'USD';
|
|
@@ -9966,12 +10032,9 @@ export function CartBundleOfferCard({ offer, onAdd, className }: CartBundleOffer
|
|
|
9966
10032
|
if (adding) return;
|
|
9967
10033
|
try {
|
|
9968
10034
|
setAdding(true);
|
|
10035
|
+
const { getClient } = await import('@/lib/brainerce');
|
|
9969
10036
|
const client = getClient();
|
|
9970
|
-
await client.
|
|
9971
|
-
productId: product.id,
|
|
9972
|
-
variantId: offer.bundleVariantId || undefined,
|
|
9973
|
-
quantity: 1,
|
|
9974
|
-
});
|
|
10037
|
+
await client.addBundleToCart(cartId, offer.id);
|
|
9975
10038
|
onAdd();
|
|
9976
10039
|
} catch (err) {
|
|
9977
10040
|
console.error('Failed to add bundle item:', err);
|
|
@@ -10483,7 +10546,8 @@ var DETAILED = `# Required Pages \u2014 Detailed Guide
|
|
|
10483
10546
|
- Step 1: Customer info (email, name)
|
|
10484
10547
|
- Step 2: Shipping address form
|
|
10485
10548
|
- Step 3: Shipping method selection (from rates returned by \`setShippingAddress()\`)
|
|
10486
|
-
- Step 4: Payment (auto-detects Stripe/Grow/PayPal via \`getPaymentProviders()\`)
|
|
10549
|
+
- Step 4: Payment (auto-detects Stripe/Grow/PayPal/Sandbox via \`getPaymentProviders()\`)
|
|
10550
|
+
- 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.
|
|
10487
10551
|
- Order summary sidebar showing \`checkout.lineItems\` (NOT cart.items!)
|
|
10488
10552
|
- Tax display after address entry
|
|
10489
10553
|
- Reservation countdown
|
|
@@ -10725,6 +10789,9 @@ function formatCapabilities(caps) {
|
|
|
10725
10789
|
lines.push(
|
|
10726
10790
|
`- Email verification: ${caps.connection.requireEmailVerification ? "required" : "not required"}`
|
|
10727
10791
|
);
|
|
10792
|
+
lines.push(
|
|
10793
|
+
`- Sandbox payments: ${caps.connection.sandboxPaymentsEnabled ? "enabled (test orders without real payment)" : "disabled"}`
|
|
10794
|
+
);
|
|
10728
10795
|
lines.push(
|
|
10729
10796
|
`- Inventory reservation: ${caps.connection.reservationStrategy} (${caps.connection.reservationTimeout} min timeout)`
|
|
10730
10797
|
);
|
|
@@ -10764,6 +10831,11 @@ function formatCapabilities(caps) {
|
|
|
10764
10831
|
'Email verification is required \u2014 ALWAYS build the verify-email page. Use get-code-example("verify-email").'
|
|
10765
10832
|
);
|
|
10766
10833
|
}
|
|
10834
|
+
if (caps.connection.sandboxPaymentsEnabled) {
|
|
10835
|
+
suggestions.push(
|
|
10836
|
+
`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.`
|
|
10837
|
+
);
|
|
10838
|
+
}
|
|
10767
10839
|
if (caps.connection.reservationStrategy !== "ON_PAYMENT") {
|
|
10768
10840
|
suggestions.push(
|
|
10769
10841
|
'Inventory reservation is active \u2014 show a countdown timer. Use get-code-example("reservation-countdown").'
|
|
@@ -10917,6 +10989,9 @@ function formatCapabilitiesSummary(caps) {
|
|
|
10917
10989
|
lines.push(
|
|
10918
10990
|
`- Guest checkout tracking: ${caps.connection.guestCheckoutTracking ? "enabled" : "disabled"}`
|
|
10919
10991
|
);
|
|
10992
|
+
lines.push(
|
|
10993
|
+
`- Sandbox payments: ${caps.connection.sandboxPaymentsEnabled ? "ENABLED \u2014 payment step shows test order button (no real charges)" : "disabled"}`
|
|
10994
|
+
);
|
|
10920
10995
|
return lines.join("\n");
|
|
10921
10996
|
}
|
|
10922
10997
|
function buildStoreBundle(options) {
|
package/dist/index.d.mts
CHANGED
|
@@ -50,6 +50,7 @@ interface StoreCapabilities {
|
|
|
50
50
|
ordersWriteEnabled: boolean;
|
|
51
51
|
guestCheckoutTracking: boolean;
|
|
52
52
|
requireEmailVerification: boolean;
|
|
53
|
+
sandboxPaymentsEnabled: boolean;
|
|
53
54
|
reservationStrategy: string;
|
|
54
55
|
reservationTimeout: number;
|
|
55
56
|
lowStockWarning: boolean;
|
package/dist/index.d.ts
CHANGED
|
@@ -50,6 +50,7 @@ interface StoreCapabilities {
|
|
|
50
50
|
ordersWriteEnabled: boolean;
|
|
51
51
|
guestCheckoutTracking: boolean;
|
|
52
52
|
requireEmailVerification: boolean;
|
|
53
|
+
sandboxPaymentsEnabled: boolean;
|
|
53
54
|
reservationStrategy: string;
|
|
54
55
|
reservationTimeout: number;
|
|
55
56
|
lowStockWarning: boolean;
|
package/dist/index.js
CHANGED
|
@@ -329,6 +329,20 @@ function CheckoutPage() {
|
|
|
329
329
|
if (!paymentData) return <div>Unable to initialize payment</div>;
|
|
330
330
|
|
|
331
331
|
// Render payment UI based on provider
|
|
332
|
+
if (paymentData.provider === 'sandbox') {
|
|
333
|
+
return (
|
|
334
|
+
<div className="text-center p-6 bg-amber-50 border border-amber-200 rounded-lg">
|
|
335
|
+
<h3 className="font-semibold mb-2">Test Mode</h3>
|
|
336
|
+
<p className="text-sm text-gray-600 mb-4">No real payment will be charged.</p>
|
|
337
|
+
<button onClick={async () => {
|
|
338
|
+
await client.completeGuestCheckout(checkoutId);
|
|
339
|
+
window.location.href = \\\`/order-confirmation?checkout_id=\\\${checkoutId}\\\`;
|
|
340
|
+
}} className="bg-amber-500 text-white px-6 py-2 rounded">
|
|
341
|
+
Complete Test Order
|
|
342
|
+
</button>
|
|
343
|
+
</div>
|
|
344
|
+
);
|
|
345
|
+
}
|
|
332
346
|
if (paymentData.provider === 'grow') {
|
|
333
347
|
return <GrowPaymentForm paymentUrl={paymentData.clientSecret} checkoutId={paymentData.checkoutId} />;
|
|
334
348
|
}
|
|
@@ -447,11 +461,12 @@ const growProvider = providers.find(p => p.provider === 'grow');
|
|
|
447
461
|
const paypalProvider = providers.find(p => p.provider === 'paypal');
|
|
448
462
|
\`\`\`
|
|
449
463
|
|
|
450
|
-
Each provider has: \`id\`, \`provider\` ('stripe'|'grow'|'paypal'), \`name\`, \`publicKey\`, \`stripeAccountId\` (Stripe only), \`supportedMethods\`, \`testMode\`, \`isDefault\`.
|
|
464
|
+
Each provider has: \`id\`, \`provider\` ('stripe'|'grow'|'paypal'|'sandbox'), \`name\`, \`publicKey\`, \`stripeAccountId\` (Stripe only), \`supportedMethods\`, \`testMode\`, \`isDefault\`.
|
|
451
465
|
|
|
452
466
|
- **Stripe:** \`npm install @stripe/stripe-js @stripe/react-stripe-js\` \u2014 \`loadStripe(publicKey, { stripeAccount })\`
|
|
453
467
|
- **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
|
|
468
|
+
- **PayPal:** \`npm install @paypal/react-paypal-js\` \u2014 \`PayPalScriptProvider\` + \`PayPalButtons\`
|
|
469
|
+
- **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
470
|
}
|
|
456
471
|
function getProductsSection(_currency) {
|
|
457
472
|
return `## Products & Variants
|
|
@@ -3172,7 +3187,12 @@ export default function CartPage() {
|
|
|
3172
3187
|
<div className="mt-6 space-y-3">
|
|
3173
3188
|
<h3 className="text-foreground text-sm font-semibold">{t('bundleOffers')}</h3>
|
|
3174
3189
|
{bundles.bundles.map((offer) => (
|
|
3175
|
-
<CartBundleOfferCard
|
|
3190
|
+
<CartBundleOfferCard
|
|
3191
|
+
key={offer.id}
|
|
3192
|
+
offer={offer}
|
|
3193
|
+
cartId={cart.id}
|
|
3194
|
+
onAdd={refreshCart}
|
|
3195
|
+
/>
|
|
3176
3196
|
))}
|
|
3177
3197
|
</div>
|
|
3178
3198
|
)}
|
|
@@ -7669,8 +7689,9 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
|
|
|
7669
7689
|
})
|
|
7670
7690
|
.catch(() => null);
|
|
7671
7691
|
|
|
7672
|
-
// B) Load + init SDK as early as possible
|
|
7692
|
+
// B) Load + init SDK as early as possible (skip for sandbox)
|
|
7673
7693
|
providerPromise.then((providerSdk) => {
|
|
7694
|
+
if (providerSdk?.renderType === 'sandbox') return;
|
|
7674
7695
|
if (providerSdk?.renderType === 'sdk-widget' && providerSdk.scriptUrl) {
|
|
7675
7696
|
currentSdk = providerSdk;
|
|
7676
7697
|
loadScript(providerSdk);
|
|
@@ -7697,6 +7718,9 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
|
|
|
7697
7718
|
const sdk = resolveClientSdk(intent, providerSdk);
|
|
7698
7719
|
currentSdk = sdk;
|
|
7699
7720
|
|
|
7721
|
+
// Sandbox mode \u2014 no SDK to load, UI handles it
|
|
7722
|
+
if (sdk.renderType === 'sandbox') return;
|
|
7723
|
+
|
|
7700
7724
|
if (sdk.renderType === 'redirect') {
|
|
7701
7725
|
window.location.href = intent.clientSecret;
|
|
7702
7726
|
return;
|
|
@@ -7791,6 +7815,48 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
|
|
|
7791
7815
|
|
|
7792
7816
|
const sdk = resolveClientSdk(paymentIntent, preloadedSdk);
|
|
7793
7817
|
|
|
7818
|
+
if (sdk.renderType === 'sandbox') {
|
|
7819
|
+
const handleCompleteSandbox = async () => {
|
|
7820
|
+
setLoading(true);
|
|
7821
|
+
try {
|
|
7822
|
+
const client = getClient();
|
|
7823
|
+
await client.completeGuestCheckout(checkoutId);
|
|
7824
|
+
window.location.href = \`/order-confirmation?checkout_id=\${checkoutId}\`;
|
|
7825
|
+
} catch (err) {
|
|
7826
|
+
setError(err instanceof Error ? err.message : t('paymentError'));
|
|
7827
|
+
setLoading(false);
|
|
7828
|
+
}
|
|
7829
|
+
};
|
|
7830
|
+
|
|
7831
|
+
return (
|
|
7832
|
+
<div className={cn('py-8 text-center', className)}>
|
|
7833
|
+
<div className="mx-auto max-w-md rounded-lg border border-amber-200 bg-amber-50 p-6">
|
|
7834
|
+
<svg
|
|
7835
|
+
className="mx-auto mb-3 h-10 w-10 text-amber-500"
|
|
7836
|
+
fill="none"
|
|
7837
|
+
viewBox="0 0 24 24"
|
|
7838
|
+
stroke="currentColor"
|
|
7839
|
+
>
|
|
7840
|
+
<path
|
|
7841
|
+
strokeLinecap="round"
|
|
7842
|
+
strokeLinejoin="round"
|
|
7843
|
+
strokeWidth={1.5}
|
|
7844
|
+
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4.5c-.77-.833-2.694-.833-3.464 0L3.34 16.5c-.77.833.192 2.5 1.732 2.5z"
|
|
7845
|
+
/>
|
|
7846
|
+
</svg>
|
|
7847
|
+
<h3 className="text-foreground mb-1 text-lg font-semibold">{t('sandboxTitle')}</h3>
|
|
7848
|
+
<p className="text-muted-foreground mb-4 text-sm">{t('sandboxDescription')}</p>
|
|
7849
|
+
<button
|
|
7850
|
+
onClick={handleCompleteSandbox}
|
|
7851
|
+
className="inline-flex items-center rounded-md bg-amber-500 px-6 py-2.5 text-sm font-medium text-white transition-colors hover:bg-amber-600"
|
|
7852
|
+
>
|
|
7853
|
+
{t('completeTestOrder')}
|
|
7854
|
+
</button>
|
|
7855
|
+
</div>
|
|
7856
|
+
</div>
|
|
7857
|
+
);
|
|
7858
|
+
}
|
|
7859
|
+
|
|
7794
7860
|
if (sdk.renderType === 'sdk-widget') {
|
|
7795
7861
|
const containerId =
|
|
7796
7862
|
sdk.containerId || \`\${paymentIntent.provider || 'payment'}-payment-container\`;
|
|
@@ -9948,18 +10014,18 @@ import { useState } from 'react';
|
|
|
9948
10014
|
import Image from 'next/image';
|
|
9949
10015
|
import type { CartBundleOffer as CartBundleOfferType } from 'brainerce';
|
|
9950
10016
|
import { formatPrice } from 'brainerce';
|
|
9951
|
-
import { getClient } from '@/lib/brainerce';
|
|
9952
10017
|
import { useStoreInfo } from '@/providers/store-provider';
|
|
9953
10018
|
import { useTranslations } from '@/lib/translations';
|
|
9954
10019
|
import { cn } from '@/lib/utils';
|
|
9955
10020
|
|
|
9956
10021
|
interface CartBundleOfferCardProps {
|
|
9957
10022
|
offer: CartBundleOfferType;
|
|
10023
|
+
cartId: string;
|
|
9958
10024
|
onAdd: () => void;
|
|
9959
10025
|
className?: string;
|
|
9960
10026
|
}
|
|
9961
10027
|
|
|
9962
|
-
export function CartBundleOfferCard({ offer, onAdd, className }: CartBundleOfferCardProps) {
|
|
10028
|
+
export function CartBundleOfferCard({ offer, cartId, onAdd, className }: CartBundleOfferCardProps) {
|
|
9963
10029
|
const { storeInfo } = useStoreInfo();
|
|
9964
10030
|
const t = useTranslations('cart');
|
|
9965
10031
|
const currency = storeInfo?.currency || 'USD';
|
|
@@ -9983,12 +10049,9 @@ export function CartBundleOfferCard({ offer, onAdd, className }: CartBundleOffer
|
|
|
9983
10049
|
if (adding) return;
|
|
9984
10050
|
try {
|
|
9985
10051
|
setAdding(true);
|
|
10052
|
+
const { getClient } = await import('@/lib/brainerce');
|
|
9986
10053
|
const client = getClient();
|
|
9987
|
-
await client.
|
|
9988
|
-
productId: product.id,
|
|
9989
|
-
variantId: offer.bundleVariantId || undefined,
|
|
9990
|
-
quantity: 1,
|
|
9991
|
-
});
|
|
10054
|
+
await client.addBundleToCart(cartId, offer.id);
|
|
9992
10055
|
onAdd();
|
|
9993
10056
|
} catch (err) {
|
|
9994
10057
|
console.error('Failed to add bundle item:', err);
|
|
@@ -10500,7 +10563,8 @@ var DETAILED = `# Required Pages \u2014 Detailed Guide
|
|
|
10500
10563
|
- Step 1: Customer info (email, name)
|
|
10501
10564
|
- Step 2: Shipping address form
|
|
10502
10565
|
- Step 3: Shipping method selection (from rates returned by \`setShippingAddress()\`)
|
|
10503
|
-
- Step 4: Payment (auto-detects Stripe/Grow/PayPal via \`getPaymentProviders()\`)
|
|
10566
|
+
- Step 4: Payment (auto-detects Stripe/Grow/PayPal/Sandbox via \`getPaymentProviders()\`)
|
|
10567
|
+
- 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.
|
|
10504
10568
|
- Order summary sidebar showing \`checkout.lineItems\` (NOT cart.items!)
|
|
10505
10569
|
- Tax display after address entry
|
|
10506
10570
|
- Reservation countdown
|
|
@@ -10742,6 +10806,9 @@ function formatCapabilities(caps) {
|
|
|
10742
10806
|
lines.push(
|
|
10743
10807
|
`- Email verification: ${caps.connection.requireEmailVerification ? "required" : "not required"}`
|
|
10744
10808
|
);
|
|
10809
|
+
lines.push(
|
|
10810
|
+
`- Sandbox payments: ${caps.connection.sandboxPaymentsEnabled ? "enabled (test orders without real payment)" : "disabled"}`
|
|
10811
|
+
);
|
|
10745
10812
|
lines.push(
|
|
10746
10813
|
`- Inventory reservation: ${caps.connection.reservationStrategy} (${caps.connection.reservationTimeout} min timeout)`
|
|
10747
10814
|
);
|
|
@@ -10781,6 +10848,11 @@ function formatCapabilities(caps) {
|
|
|
10781
10848
|
'Email verification is required \u2014 ALWAYS build the verify-email page. Use get-code-example("verify-email").'
|
|
10782
10849
|
);
|
|
10783
10850
|
}
|
|
10851
|
+
if (caps.connection.sandboxPaymentsEnabled) {
|
|
10852
|
+
suggestions.push(
|
|
10853
|
+
`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.`
|
|
10854
|
+
);
|
|
10855
|
+
}
|
|
10784
10856
|
if (caps.connection.reservationStrategy !== "ON_PAYMENT") {
|
|
10785
10857
|
suggestions.push(
|
|
10786
10858
|
'Inventory reservation is active \u2014 show a countdown timer. Use get-code-example("reservation-countdown").'
|
|
@@ -10934,6 +11006,9 @@ function formatCapabilitiesSummary(caps) {
|
|
|
10934
11006
|
lines.push(
|
|
10935
11007
|
`- Guest checkout tracking: ${caps.connection.guestCheckoutTracking ? "enabled" : "disabled"}`
|
|
10936
11008
|
);
|
|
11009
|
+
lines.push(
|
|
11010
|
+
`- Sandbox payments: ${caps.connection.sandboxPaymentsEnabled ? "ENABLED \u2014 payment step shows test order button (no real charges)" : "disabled"}`
|
|
11011
|
+
);
|
|
10937
11012
|
return lines.join("\n");
|
|
10938
11013
|
}
|
|
10939
11014
|
function buildStoreBundle(options) {
|
package/dist/index.mjs
CHANGED
|
@@ -284,6 +284,20 @@ function CheckoutPage() {
|
|
|
284
284
|
if (!paymentData) return <div>Unable to initialize payment</div>;
|
|
285
285
|
|
|
286
286
|
// Render payment UI based on provider
|
|
287
|
+
if (paymentData.provider === 'sandbox') {
|
|
288
|
+
return (
|
|
289
|
+
<div className="text-center p-6 bg-amber-50 border border-amber-200 rounded-lg">
|
|
290
|
+
<h3 className="font-semibold mb-2">Test Mode</h3>
|
|
291
|
+
<p className="text-sm text-gray-600 mb-4">No real payment will be charged.</p>
|
|
292
|
+
<button onClick={async () => {
|
|
293
|
+
await client.completeGuestCheckout(checkoutId);
|
|
294
|
+
window.location.href = \\\`/order-confirmation?checkout_id=\\\${checkoutId}\\\`;
|
|
295
|
+
}} className="bg-amber-500 text-white px-6 py-2 rounded">
|
|
296
|
+
Complete Test Order
|
|
297
|
+
</button>
|
|
298
|
+
</div>
|
|
299
|
+
);
|
|
300
|
+
}
|
|
287
301
|
if (paymentData.provider === 'grow') {
|
|
288
302
|
return <GrowPaymentForm paymentUrl={paymentData.clientSecret} checkoutId={paymentData.checkoutId} />;
|
|
289
303
|
}
|
|
@@ -402,11 +416,12 @@ const growProvider = providers.find(p => p.provider === 'grow');
|
|
|
402
416
|
const paypalProvider = providers.find(p => p.provider === 'paypal');
|
|
403
417
|
\`\`\`
|
|
404
418
|
|
|
405
|
-
Each provider has: \`id\`, \`provider\` ('stripe'|'grow'|'paypal'), \`name\`, \`publicKey\`, \`stripeAccountId\` (Stripe only), \`supportedMethods\`, \`testMode\`, \`isDefault\`.
|
|
419
|
+
Each provider has: \`id\`, \`provider\` ('stripe'|'grow'|'paypal'|'sandbox'), \`name\`, \`publicKey\`, \`stripeAccountId\` (Stripe only), \`supportedMethods\`, \`testMode\`, \`isDefault\`.
|
|
406
420
|
|
|
407
421
|
- **Stripe:** \`npm install @stripe/stripe-js @stripe/react-stripe-js\` \u2014 \`loadStripe(publicKey, { stripeAccount })\`
|
|
408
422
|
- **Grow:** No SDK \u2014 uses iframe with payment URL. Supports credit cards, Bit, Apple Pay, Google Pay.
|
|
409
|
-
- **PayPal:** \`npm install @paypal/react-paypal-js\` \u2014 \`PayPalScriptProvider\` + \`PayPalButtons
|
|
423
|
+
- **PayPal:** \`npm install @paypal/react-paypal-js\` \u2014 \`PayPalScriptProvider\` + \`PayPalButtons\`
|
|
424
|
+
- **Sandbox:** No SDK needed \u2014 when \`sandboxPaymentsEnabled\` is true, \`getPaymentProviders()\` includes a sandbox provider with \`renderType: 'sandbox'\`. Show a "Complete Test Order" button. Call \`completeGuestCheckout(checkoutId)\` to finalize. Orders are marked \`isTestOrder: true\`.`;
|
|
410
425
|
}
|
|
411
426
|
function getProductsSection(_currency) {
|
|
412
427
|
return `## Products & Variants
|
|
@@ -3127,7 +3142,12 @@ export default function CartPage() {
|
|
|
3127
3142
|
<div className="mt-6 space-y-3">
|
|
3128
3143
|
<h3 className="text-foreground text-sm font-semibold">{t('bundleOffers')}</h3>
|
|
3129
3144
|
{bundles.bundles.map((offer) => (
|
|
3130
|
-
<CartBundleOfferCard
|
|
3145
|
+
<CartBundleOfferCard
|
|
3146
|
+
key={offer.id}
|
|
3147
|
+
offer={offer}
|
|
3148
|
+
cartId={cart.id}
|
|
3149
|
+
onAdd={refreshCart}
|
|
3150
|
+
/>
|
|
3131
3151
|
))}
|
|
3132
3152
|
</div>
|
|
3133
3153
|
)}
|
|
@@ -7624,8 +7644,9 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
|
|
|
7624
7644
|
})
|
|
7625
7645
|
.catch(() => null);
|
|
7626
7646
|
|
|
7627
|
-
// B) Load + init SDK as early as possible
|
|
7647
|
+
// B) Load + init SDK as early as possible (skip for sandbox)
|
|
7628
7648
|
providerPromise.then((providerSdk) => {
|
|
7649
|
+
if (providerSdk?.renderType === 'sandbox') return;
|
|
7629
7650
|
if (providerSdk?.renderType === 'sdk-widget' && providerSdk.scriptUrl) {
|
|
7630
7651
|
currentSdk = providerSdk;
|
|
7631
7652
|
loadScript(providerSdk);
|
|
@@ -7652,6 +7673,9 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
|
|
|
7652
7673
|
const sdk = resolveClientSdk(intent, providerSdk);
|
|
7653
7674
|
currentSdk = sdk;
|
|
7654
7675
|
|
|
7676
|
+
// Sandbox mode \u2014 no SDK to load, UI handles it
|
|
7677
|
+
if (sdk.renderType === 'sandbox') return;
|
|
7678
|
+
|
|
7655
7679
|
if (sdk.renderType === 'redirect') {
|
|
7656
7680
|
window.location.href = intent.clientSecret;
|
|
7657
7681
|
return;
|
|
@@ -7746,6 +7770,48 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
|
|
|
7746
7770
|
|
|
7747
7771
|
const sdk = resolveClientSdk(paymentIntent, preloadedSdk);
|
|
7748
7772
|
|
|
7773
|
+
if (sdk.renderType === 'sandbox') {
|
|
7774
|
+
const handleCompleteSandbox = async () => {
|
|
7775
|
+
setLoading(true);
|
|
7776
|
+
try {
|
|
7777
|
+
const client = getClient();
|
|
7778
|
+
await client.completeGuestCheckout(checkoutId);
|
|
7779
|
+
window.location.href = \`/order-confirmation?checkout_id=\${checkoutId}\`;
|
|
7780
|
+
} catch (err) {
|
|
7781
|
+
setError(err instanceof Error ? err.message : t('paymentError'));
|
|
7782
|
+
setLoading(false);
|
|
7783
|
+
}
|
|
7784
|
+
};
|
|
7785
|
+
|
|
7786
|
+
return (
|
|
7787
|
+
<div className={cn('py-8 text-center', className)}>
|
|
7788
|
+
<div className="mx-auto max-w-md rounded-lg border border-amber-200 bg-amber-50 p-6">
|
|
7789
|
+
<svg
|
|
7790
|
+
className="mx-auto mb-3 h-10 w-10 text-amber-500"
|
|
7791
|
+
fill="none"
|
|
7792
|
+
viewBox="0 0 24 24"
|
|
7793
|
+
stroke="currentColor"
|
|
7794
|
+
>
|
|
7795
|
+
<path
|
|
7796
|
+
strokeLinecap="round"
|
|
7797
|
+
strokeLinejoin="round"
|
|
7798
|
+
strokeWidth={1.5}
|
|
7799
|
+
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4.5c-.77-.833-2.694-.833-3.464 0L3.34 16.5c-.77.833.192 2.5 1.732 2.5z"
|
|
7800
|
+
/>
|
|
7801
|
+
</svg>
|
|
7802
|
+
<h3 className="text-foreground mb-1 text-lg font-semibold">{t('sandboxTitle')}</h3>
|
|
7803
|
+
<p className="text-muted-foreground mb-4 text-sm">{t('sandboxDescription')}</p>
|
|
7804
|
+
<button
|
|
7805
|
+
onClick={handleCompleteSandbox}
|
|
7806
|
+
className="inline-flex items-center rounded-md bg-amber-500 px-6 py-2.5 text-sm font-medium text-white transition-colors hover:bg-amber-600"
|
|
7807
|
+
>
|
|
7808
|
+
{t('completeTestOrder')}
|
|
7809
|
+
</button>
|
|
7810
|
+
</div>
|
|
7811
|
+
</div>
|
|
7812
|
+
);
|
|
7813
|
+
}
|
|
7814
|
+
|
|
7749
7815
|
if (sdk.renderType === 'sdk-widget') {
|
|
7750
7816
|
const containerId =
|
|
7751
7817
|
sdk.containerId || \`\${paymentIntent.provider || 'payment'}-payment-container\`;
|
|
@@ -9903,18 +9969,18 @@ import { useState } from 'react';
|
|
|
9903
9969
|
import Image from 'next/image';
|
|
9904
9970
|
import type { CartBundleOffer as CartBundleOfferType } from 'brainerce';
|
|
9905
9971
|
import { formatPrice } from 'brainerce';
|
|
9906
|
-
import { getClient } from '@/lib/brainerce';
|
|
9907
9972
|
import { useStoreInfo } from '@/providers/store-provider';
|
|
9908
9973
|
import { useTranslations } from '@/lib/translations';
|
|
9909
9974
|
import { cn } from '@/lib/utils';
|
|
9910
9975
|
|
|
9911
9976
|
interface CartBundleOfferCardProps {
|
|
9912
9977
|
offer: CartBundleOfferType;
|
|
9978
|
+
cartId: string;
|
|
9913
9979
|
onAdd: () => void;
|
|
9914
9980
|
className?: string;
|
|
9915
9981
|
}
|
|
9916
9982
|
|
|
9917
|
-
export function CartBundleOfferCard({ offer, onAdd, className }: CartBundleOfferCardProps) {
|
|
9983
|
+
export function CartBundleOfferCard({ offer, cartId, onAdd, className }: CartBundleOfferCardProps) {
|
|
9918
9984
|
const { storeInfo } = useStoreInfo();
|
|
9919
9985
|
const t = useTranslations('cart');
|
|
9920
9986
|
const currency = storeInfo?.currency || 'USD';
|
|
@@ -9938,12 +10004,9 @@ export function CartBundleOfferCard({ offer, onAdd, className }: CartBundleOffer
|
|
|
9938
10004
|
if (adding) return;
|
|
9939
10005
|
try {
|
|
9940
10006
|
setAdding(true);
|
|
10007
|
+
const { getClient } = await import('@/lib/brainerce');
|
|
9941
10008
|
const client = getClient();
|
|
9942
|
-
await client.
|
|
9943
|
-
productId: product.id,
|
|
9944
|
-
variantId: offer.bundleVariantId || undefined,
|
|
9945
|
-
quantity: 1,
|
|
9946
|
-
});
|
|
10009
|
+
await client.addBundleToCart(cartId, offer.id);
|
|
9947
10010
|
onAdd();
|
|
9948
10011
|
} catch (err) {
|
|
9949
10012
|
console.error('Failed to add bundle item:', err);
|
|
@@ -10455,7 +10518,8 @@ var DETAILED = `# Required Pages \u2014 Detailed Guide
|
|
|
10455
10518
|
- Step 1: Customer info (email, name)
|
|
10456
10519
|
- Step 2: Shipping address form
|
|
10457
10520
|
- Step 3: Shipping method selection (from rates returned by \`setShippingAddress()\`)
|
|
10458
|
-
- Step 4: Payment (auto-detects Stripe/Grow/PayPal via \`getPaymentProviders()\`)
|
|
10521
|
+
- Step 4: Payment (auto-detects Stripe/Grow/PayPal/Sandbox via \`getPaymentProviders()\`)
|
|
10522
|
+
- 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.
|
|
10459
10523
|
- Order summary sidebar showing \`checkout.lineItems\` (NOT cart.items!)
|
|
10460
10524
|
- Tax display after address entry
|
|
10461
10525
|
- Reservation countdown
|
|
@@ -10697,6 +10761,9 @@ function formatCapabilities(caps) {
|
|
|
10697
10761
|
lines.push(
|
|
10698
10762
|
`- Email verification: ${caps.connection.requireEmailVerification ? "required" : "not required"}`
|
|
10699
10763
|
);
|
|
10764
|
+
lines.push(
|
|
10765
|
+
`- Sandbox payments: ${caps.connection.sandboxPaymentsEnabled ? "enabled (test orders without real payment)" : "disabled"}`
|
|
10766
|
+
);
|
|
10700
10767
|
lines.push(
|
|
10701
10768
|
`- Inventory reservation: ${caps.connection.reservationStrategy} (${caps.connection.reservationTimeout} min timeout)`
|
|
10702
10769
|
);
|
|
@@ -10736,6 +10803,11 @@ function formatCapabilities(caps) {
|
|
|
10736
10803
|
'Email verification is required \u2014 ALWAYS build the verify-email page. Use get-code-example("verify-email").'
|
|
10737
10804
|
);
|
|
10738
10805
|
}
|
|
10806
|
+
if (caps.connection.sandboxPaymentsEnabled) {
|
|
10807
|
+
suggestions.push(
|
|
10808
|
+
`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.`
|
|
10809
|
+
);
|
|
10810
|
+
}
|
|
10739
10811
|
if (caps.connection.reservationStrategy !== "ON_PAYMENT") {
|
|
10740
10812
|
suggestions.push(
|
|
10741
10813
|
'Inventory reservation is active \u2014 show a countdown timer. Use get-code-example("reservation-countdown").'
|
|
@@ -10889,6 +10961,9 @@ function formatCapabilitiesSummary(caps) {
|
|
|
10889
10961
|
lines.push(
|
|
10890
10962
|
`- Guest checkout tracking: ${caps.connection.guestCheckoutTracking ? "enabled" : "disabled"}`
|
|
10891
10963
|
);
|
|
10964
|
+
lines.push(
|
|
10965
|
+
`- Sandbox payments: ${caps.connection.sandboxPaymentsEnabled ? "ENABLED \u2014 payment step shows test order button (no real charges)" : "disabled"}`
|
|
10966
|
+
);
|
|
10892
10967
|
return lines.join("\n");
|
|
10893
10968
|
}
|
|
10894
10969
|
function buildStoreBundle(options) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brainerce/mcp-server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "MCP server for AI-powered Brainerce store building. Provides SDK docs, types, and code examples to AI tools like Lovable, Cursor, and Claude Code.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"brainerce-mcp": "dist/bin/stdio.js"
|