@brainerce/mcp-server 2.1.0 → 2.3.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 CHANGED
@@ -777,6 +777,12 @@ const { bundles } = await client.getCartBundles(cartId);
777
777
  // bundles[].bundleProduct \u2014 the product to offer
778
778
  // bundles[].originalPrice / bundles[].discountedPrice \u2014 prices
779
779
  // bundles[].discountType ('PERCENTAGE' | 'FIXED_AMOUNT') + discountValue
780
+
781
+ // Add a bundle to cart (applies the discount automatically):
782
+ await client.addBundleToCart(cartId, bundleOfferId);
783
+ // Remove a bundle from cart:
784
+ await client.removeBundleFromCart(cartId, bundleOfferId);
785
+ // Detect already-added bundles: check cart.items for metadata?.isBundleItem === true
780
786
  \`\`\`
781
787
 
782
788
  **Free shipping progress bar** (configured threshold in channel settings):
@@ -3001,7 +3007,11 @@ export default async function ProductDetailPage({ params }: Props) {
3001
3007
 
3002
3008
  import { useEffect, useState } from 'react';
3003
3009
  import Link from 'next/link';
3004
- import type { CartRecommendationsResponse, CartUpgradesResponse, CartBundlesResponse } from 'brainerce';
3010
+ import type {
3011
+ CartRecommendationsResponse,
3012
+ CartUpgradesResponse,
3013
+ CartBundlesResponse,
3014
+ } from 'brainerce';
3005
3015
  import { getClient } from '@/lib/brainerce';
3006
3016
  import { useCart } from '@/providers/store-provider';
3007
3017
  import { useStoreInfo } from '@/providers/store-provider';
@@ -3041,7 +3051,11 @@ export default function CartPage() {
3041
3051
 
3042
3052
  // Load upgrade suggestions when cart changes
3043
3053
  useEffect(() => {
3044
- if (!cart?.id || cart.items.length === 0 || storeInfo?.upsell?.cartUpgradeBannerEnabled === false) {
3054
+ if (
3055
+ !cart?.id ||
3056
+ cart.items.length === 0 ||
3057
+ storeInfo?.upsell?.cartUpgradeBannerEnabled === false
3058
+ ) {
3045
3059
  setUpgrades(null);
3046
3060
  return;
3047
3061
  }
@@ -3143,7 +3157,12 @@ export default function CartPage() {
3143
3157
  <div className="mt-6 space-y-3">
3144
3158
  <h3 className="text-foreground text-sm font-semibold">{t('bundleOffers')}</h3>
3145
3159
  {bundles.bundles.map((offer) => (
3146
- <CartBundleOfferCard key={offer.id} offer={offer} onAdd={refreshCart} />
3160
+ <CartBundleOfferCard
3161
+ key={offer.id}
3162
+ offer={offer}
3163
+ cartId={cart.id}
3164
+ onAdd={refreshCart}
3165
+ />
3147
3166
  ))}
3148
3167
  </div>
3149
3168
  )}
@@ -7640,8 +7659,9 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
7640
7659
  })
7641
7660
  .catch(() => null);
7642
7661
 
7643
- // B) Load + init SDK as early as possible
7662
+ // B) Load + init SDK as early as possible (skip for sandbox)
7644
7663
  providerPromise.then((providerSdk) => {
7664
+ if (providerSdk?.renderType === 'sandbox') return;
7645
7665
  if (providerSdk?.renderType === 'sdk-widget' && providerSdk.scriptUrl) {
7646
7666
  currentSdk = providerSdk;
7647
7667
  loadScript(providerSdk);
@@ -7668,6 +7688,9 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
7668
7688
  const sdk = resolveClientSdk(intent, providerSdk);
7669
7689
  currentSdk = sdk;
7670
7690
 
7691
+ // Sandbox mode \u2014 no SDK to load, UI handles it
7692
+ if (sdk.renderType === 'sandbox') return;
7693
+
7671
7694
  if (sdk.renderType === 'redirect') {
7672
7695
  window.location.href = intent.clientSecret;
7673
7696
  return;
@@ -7762,6 +7785,48 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
7762
7785
 
7763
7786
  const sdk = resolveClientSdk(paymentIntent, preloadedSdk);
7764
7787
 
7788
+ if (sdk.renderType === 'sandbox') {
7789
+ const handleCompleteSandbox = async () => {
7790
+ setLoading(true);
7791
+ try {
7792
+ const client = getClient();
7793
+ await client.completeGuestCheckout(checkoutId);
7794
+ window.location.href = \`/order-confirmation?checkout_id=\${checkoutId}\`;
7795
+ } catch (err) {
7796
+ setError(err instanceof Error ? err.message : t('paymentError'));
7797
+ setLoading(false);
7798
+ }
7799
+ };
7800
+
7801
+ return (
7802
+ <div className={cn('py-8 text-center', className)}>
7803
+ <div className="mx-auto max-w-md rounded-lg border border-amber-200 bg-amber-50 p-6">
7804
+ <svg
7805
+ className="mx-auto mb-3 h-10 w-10 text-amber-500"
7806
+ fill="none"
7807
+ viewBox="0 0 24 24"
7808
+ stroke="currentColor"
7809
+ >
7810
+ <path
7811
+ strokeLinecap="round"
7812
+ strokeLinejoin="round"
7813
+ strokeWidth={1.5}
7814
+ 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"
7815
+ />
7816
+ </svg>
7817
+ <h3 className="text-foreground mb-1 text-lg font-semibold">{t('sandboxTitle')}</h3>
7818
+ <p className="text-muted-foreground mb-4 text-sm">{t('sandboxDescription')}</p>
7819
+ <button
7820
+ onClick={handleCompleteSandbox}
7821
+ 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"
7822
+ >
7823
+ {t('completeTestOrder')}
7824
+ </button>
7825
+ </div>
7826
+ </div>
7827
+ );
7828
+ }
7829
+
7765
7830
  if (sdk.renderType === 'sdk-widget') {
7766
7831
  const containerId =
7767
7832
  sdk.containerId || \`\${paymentIntent.provider || 'payment'}-payment-container\`;
@@ -9568,8 +9633,18 @@ function ProductThumb({
9568
9633
  <Image src={imageUrl} alt={name} fill sizes="80px" className="object-cover" />
9569
9634
  ) : (
9570
9635
  <div className="flex h-full w-full items-center justify-center">
9571
- <svg className="text-muted-foreground h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
9572
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} 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" />
9636
+ <svg
9637
+ className="text-muted-foreground h-8 w-8"
9638
+ fill="none"
9639
+ viewBox="0 0 24 24"
9640
+ stroke="currentColor"
9641
+ >
9642
+ <path
9643
+ strokeLinecap="round"
9644
+ strokeLinejoin="round"
9645
+ strokeWidth={1.5}
9646
+ 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"
9647
+ />
9573
9648
  </svg>
9574
9649
  </div>
9575
9650
  )}
@@ -9646,7 +9721,9 @@ export function FrequentlyBoughtTogether({
9646
9721
 
9647
9722
  return (
9648
9723
  <div className={cn('border-border rounded-lg border p-6', className)}>
9649
- <h2 className="text-foreground mb-4 text-xl font-semibold">{t('frequentlyBoughtTogether')}</h2>
9724
+ <h2 className="text-foreground mb-4 text-xl font-semibold">
9725
+ {t('frequentlyBoughtTogether')}
9726
+ </h2>
9650
9727
 
9651
9728
  <div className="flex flex-wrap items-center gap-3">
9652
9729
  {/* Current product (always included, no checkbox) */}
@@ -9733,12 +9810,7 @@ export function FreeShippingBar({ className }: FreeShippingBarProps) {
9733
9810
  <div className={cn('rounded-lg border border-green-200 bg-green-50 p-3', className)}>
9734
9811
  <div className="flex items-center gap-2 text-sm font-medium text-green-700">
9735
9812
  <svg className="h-4 w-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
9736
- <path
9737
- strokeLinecap="round"
9738
- strokeLinejoin="round"
9739
- strokeWidth={2}
9740
- d="M5 13l4 4L19 7"
9741
- />
9813
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
9742
9814
  </svg>
9743
9815
  {t('freeShippingQualified')}
9744
9816
  </div>
@@ -9800,20 +9872,28 @@ export function CartUpgradeBanner({
9800
9872
  if (sessionStorage.getItem(storageKey)) {
9801
9873
  setDismissed(true);
9802
9874
  }
9803
- } catch {}
9875
+ } catch {
9876
+ /* ignore */
9877
+ }
9804
9878
  }, [storageKey]);
9805
9879
 
9806
9880
  if (dismissed) return null;
9807
9881
 
9808
9882
  const target = suggestion.targetProduct;
9809
9883
  const firstImage = target.images?.[0];
9810
- const imageUrl = firstImage ? (typeof firstImage === 'string' ? firstImage : firstImage.url) : null;
9884
+ const imageUrl = firstImage
9885
+ ? typeof firstImage === 'string'
9886
+ ? firstImage
9887
+ : firstImage.url
9888
+ : null;
9811
9889
  const formattedDelta = formatPrice(parseFloat(suggestion.priceDelta), { currency }) as string;
9812
9890
 
9813
9891
  function handleDismiss() {
9814
9892
  try {
9815
9893
  sessionStorage.setItem(storageKey, '1');
9816
- } catch {}
9894
+ } catch {
9895
+ /* ignore */
9896
+ }
9817
9897
  setDismissed(true);
9818
9898
  }
9819
9899
 
@@ -9846,7 +9926,13 @@ export function CartUpgradeBanner({
9846
9926
  className="text-muted-foreground hover:text-foreground absolute end-2 top-2 text-xs"
9847
9927
  aria-label={t('dismissUpgrade')}
9848
9928
  >
9849
- <svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
9929
+ <svg
9930
+ className="h-4 w-4"
9931
+ fill="none"
9932
+ viewBox="0 0 24 24"
9933
+ stroke="currentColor"
9934
+ strokeWidth={2}
9935
+ >
9850
9936
  <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
9851
9937
  </svg>
9852
9938
  </button>
@@ -9858,7 +9944,12 @@ export function CartUpgradeBanner({
9858
9944
  ) : (
9859
9945
  <div className="text-muted-foreground flex h-full w-full items-center justify-center">
9860
9946
  <svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
9861
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} 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" />
9947
+ <path
9948
+ strokeLinecap="round"
9949
+ strokeLinejoin="round"
9950
+ strokeWidth={1.5}
9951
+ 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"
9952
+ />
9862
9953
  </svg>
9863
9954
  </div>
9864
9955
  )}
@@ -9893,18 +9984,18 @@ import { useState } from 'react';
9893
9984
  import Image from 'next/image';
9894
9985
  import type { CartBundleOffer as CartBundleOfferType } from 'brainerce';
9895
9986
  import { formatPrice } from 'brainerce';
9896
- import { getClient } from '@/lib/brainerce';
9897
9987
  import { useStoreInfo } from '@/providers/store-provider';
9898
9988
  import { useTranslations } from '@/lib/translations';
9899
9989
  import { cn } from '@/lib/utils';
9900
9990
 
9901
9991
  interface CartBundleOfferCardProps {
9902
9992
  offer: CartBundleOfferType;
9993
+ cartId: string;
9903
9994
  onAdd: () => void;
9904
9995
  className?: string;
9905
9996
  }
9906
9997
 
9907
- export function CartBundleOfferCard({ offer, onAdd, className }: CartBundleOfferCardProps) {
9998
+ export function CartBundleOfferCard({ offer, cartId, onAdd, className }: CartBundleOfferCardProps) {
9908
9999
  const { storeInfo } = useStoreInfo();
9909
10000
  const t = useTranslations('cart');
9910
10001
  const currency = storeInfo?.currency || 'USD';
@@ -9912,24 +10003,25 @@ export function CartBundleOfferCard({ offer, onAdd, className }: CartBundleOffer
9912
10003
 
9913
10004
  const product = offer.bundleProduct;
9914
10005
  const firstImage = product.images?.[0];
9915
- const imageUrl = firstImage ? (typeof firstImage === 'string' ? firstImage : firstImage.url) : null;
10006
+ const imageUrl = firstImage
10007
+ ? typeof firstImage === 'string'
10008
+ ? firstImage
10009
+ : firstImage.url
10010
+ : null;
9916
10011
  const originalPrice = parseFloat(offer.originalPrice);
9917
10012
  const discountedPrice = parseFloat(offer.discountedPrice);
9918
10013
  const discountLabel =
9919
10014
  offer.discountType === 'PERCENTAGE'
9920
10015
  ? \`\${offer.discountValue}%\`
9921
- : formatPrice(parseFloat(offer.discountValue), { currency }) as string;
10016
+ : (formatPrice(parseFloat(offer.discountValue), { currency }) as string);
9922
10017
 
9923
10018
  async function handleAdd() {
9924
10019
  if (adding) return;
9925
10020
  try {
9926
10021
  setAdding(true);
10022
+ const { getClient } = await import('@/lib/brainerce');
9927
10023
  const client = getClient();
9928
- await client.smartAddToCart({
9929
- productId: product.id,
9930
- variantId: offer.bundleVariantId || undefined,
9931
- quantity: 1,
9932
- });
10024
+ await client.addBundleToCart(cartId, offer.id);
9933
10025
  onAdd();
9934
10026
  } catch (err) {
9935
10027
  console.error('Failed to add bundle item:', err);
@@ -9952,7 +10044,12 @@ export function CartBundleOfferCard({ offer, onAdd, className }: CartBundleOffer
9952
10044
  ) : (
9953
10045
  <div className="text-muted-foreground flex h-full w-full items-center justify-center">
9954
10046
  <svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
9955
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} 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" />
10047
+ <path
10048
+ strokeLinecap="round"
10049
+ strokeLinejoin="round"
10050
+ strokeWidth={1.5}
10051
+ 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"
10052
+ />
9956
10053
  </svg>
9957
10054
  </div>
9958
10055
  )}
@@ -10017,7 +10114,11 @@ export function OrderBumpCard({ bump, isAdded, onToggle, loading, className }: O
10017
10114
 
10018
10115
  const product = bump.bumpProduct;
10019
10116
  const firstImage = product.images?.[0];
10020
- const imageUrl = firstImage ? (typeof firstImage === 'string' ? firstImage : firstImage.url) : null;
10117
+ const imageUrl = firstImage
10118
+ ? typeof firstImage === 'string'
10119
+ ? firstImage
10120
+ : firstImage.url
10121
+ : null;
10021
10122
  const originalPrice = parseFloat(bump.originalPrice);
10022
10123
  const hasDiscount = bump.discountedPrice != null;
10023
10124
  const discountedPrice = hasDiscount ? parseFloat(bump.discountedPrice!) : null;
package/dist/bin/stdio.js CHANGED
@@ -775,6 +775,12 @@ const { bundles } = await client.getCartBundles(cartId);
775
775
  // bundles[].bundleProduct \u2014 the product to offer
776
776
  // bundles[].originalPrice / bundles[].discountedPrice \u2014 prices
777
777
  // bundles[].discountType ('PERCENTAGE' | 'FIXED_AMOUNT') + discountValue
778
+
779
+ // Add a bundle to cart (applies the discount automatically):
780
+ await client.addBundleToCart(cartId, bundleOfferId);
781
+ // Remove a bundle from cart:
782
+ await client.removeBundleFromCart(cartId, bundleOfferId);
783
+ // Detect already-added bundles: check cart.items for metadata?.isBundleItem === true
778
784
  \`\`\`
779
785
 
780
786
  **Free shipping progress bar** (configured threshold in channel settings):
@@ -2999,7 +3005,11 @@ export default async function ProductDetailPage({ params }: Props) {
2999
3005
 
3000
3006
  import { useEffect, useState } from 'react';
3001
3007
  import Link from 'next/link';
3002
- import type { CartRecommendationsResponse, CartUpgradesResponse, CartBundlesResponse } from 'brainerce';
3008
+ import type {
3009
+ CartRecommendationsResponse,
3010
+ CartUpgradesResponse,
3011
+ CartBundlesResponse,
3012
+ } from 'brainerce';
3003
3013
  import { getClient } from '@/lib/brainerce';
3004
3014
  import { useCart } from '@/providers/store-provider';
3005
3015
  import { useStoreInfo } from '@/providers/store-provider';
@@ -3039,7 +3049,11 @@ export default function CartPage() {
3039
3049
 
3040
3050
  // Load upgrade suggestions when cart changes
3041
3051
  useEffect(() => {
3042
- if (!cart?.id || cart.items.length === 0 || storeInfo?.upsell?.cartUpgradeBannerEnabled === false) {
3052
+ if (
3053
+ !cart?.id ||
3054
+ cart.items.length === 0 ||
3055
+ storeInfo?.upsell?.cartUpgradeBannerEnabled === false
3056
+ ) {
3043
3057
  setUpgrades(null);
3044
3058
  return;
3045
3059
  }
@@ -3141,7 +3155,12 @@ export default function CartPage() {
3141
3155
  <div className="mt-6 space-y-3">
3142
3156
  <h3 className="text-foreground text-sm font-semibold">{t('bundleOffers')}</h3>
3143
3157
  {bundles.bundles.map((offer) => (
3144
- <CartBundleOfferCard key={offer.id} offer={offer} onAdd={refreshCart} />
3158
+ <CartBundleOfferCard
3159
+ key={offer.id}
3160
+ offer={offer}
3161
+ cartId={cart.id}
3162
+ onAdd={refreshCart}
3163
+ />
3145
3164
  ))}
3146
3165
  </div>
3147
3166
  )}
@@ -7638,8 +7657,9 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
7638
7657
  })
7639
7658
  .catch(() => null);
7640
7659
 
7641
- // B) Load + init SDK as early as possible
7660
+ // B) Load + init SDK as early as possible (skip for sandbox)
7642
7661
  providerPromise.then((providerSdk) => {
7662
+ if (providerSdk?.renderType === 'sandbox') return;
7643
7663
  if (providerSdk?.renderType === 'sdk-widget' && providerSdk.scriptUrl) {
7644
7664
  currentSdk = providerSdk;
7645
7665
  loadScript(providerSdk);
@@ -7666,6 +7686,9 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
7666
7686
  const sdk = resolveClientSdk(intent, providerSdk);
7667
7687
  currentSdk = sdk;
7668
7688
 
7689
+ // Sandbox mode \u2014 no SDK to load, UI handles it
7690
+ if (sdk.renderType === 'sandbox') return;
7691
+
7669
7692
  if (sdk.renderType === 'redirect') {
7670
7693
  window.location.href = intent.clientSecret;
7671
7694
  return;
@@ -7760,6 +7783,48 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
7760
7783
 
7761
7784
  const sdk = resolveClientSdk(paymentIntent, preloadedSdk);
7762
7785
 
7786
+ if (sdk.renderType === 'sandbox') {
7787
+ const handleCompleteSandbox = async () => {
7788
+ setLoading(true);
7789
+ try {
7790
+ const client = getClient();
7791
+ await client.completeGuestCheckout(checkoutId);
7792
+ window.location.href = \`/order-confirmation?checkout_id=\${checkoutId}\`;
7793
+ } catch (err) {
7794
+ setError(err instanceof Error ? err.message : t('paymentError'));
7795
+ setLoading(false);
7796
+ }
7797
+ };
7798
+
7799
+ return (
7800
+ <div className={cn('py-8 text-center', className)}>
7801
+ <div className="mx-auto max-w-md rounded-lg border border-amber-200 bg-amber-50 p-6">
7802
+ <svg
7803
+ className="mx-auto mb-3 h-10 w-10 text-amber-500"
7804
+ fill="none"
7805
+ viewBox="0 0 24 24"
7806
+ stroke="currentColor"
7807
+ >
7808
+ <path
7809
+ strokeLinecap="round"
7810
+ strokeLinejoin="round"
7811
+ strokeWidth={1.5}
7812
+ 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"
7813
+ />
7814
+ </svg>
7815
+ <h3 className="text-foreground mb-1 text-lg font-semibold">{t('sandboxTitle')}</h3>
7816
+ <p className="text-muted-foreground mb-4 text-sm">{t('sandboxDescription')}</p>
7817
+ <button
7818
+ onClick={handleCompleteSandbox}
7819
+ 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"
7820
+ >
7821
+ {t('completeTestOrder')}
7822
+ </button>
7823
+ </div>
7824
+ </div>
7825
+ );
7826
+ }
7827
+
7763
7828
  if (sdk.renderType === 'sdk-widget') {
7764
7829
  const containerId =
7765
7830
  sdk.containerId || \`\${paymentIntent.provider || 'payment'}-payment-container\`;
@@ -9566,8 +9631,18 @@ function ProductThumb({
9566
9631
  <Image src={imageUrl} alt={name} fill sizes="80px" className="object-cover" />
9567
9632
  ) : (
9568
9633
  <div className="flex h-full w-full items-center justify-center">
9569
- <svg className="text-muted-foreground h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
9570
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} 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" />
9634
+ <svg
9635
+ className="text-muted-foreground h-8 w-8"
9636
+ fill="none"
9637
+ viewBox="0 0 24 24"
9638
+ stroke="currentColor"
9639
+ >
9640
+ <path
9641
+ strokeLinecap="round"
9642
+ strokeLinejoin="round"
9643
+ strokeWidth={1.5}
9644
+ 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"
9645
+ />
9571
9646
  </svg>
9572
9647
  </div>
9573
9648
  )}
@@ -9644,7 +9719,9 @@ export function FrequentlyBoughtTogether({
9644
9719
 
9645
9720
  return (
9646
9721
  <div className={cn('border-border rounded-lg border p-6', className)}>
9647
- <h2 className="text-foreground mb-4 text-xl font-semibold">{t('frequentlyBoughtTogether')}</h2>
9722
+ <h2 className="text-foreground mb-4 text-xl font-semibold">
9723
+ {t('frequentlyBoughtTogether')}
9724
+ </h2>
9648
9725
 
9649
9726
  <div className="flex flex-wrap items-center gap-3">
9650
9727
  {/* Current product (always included, no checkbox) */}
@@ -9731,12 +9808,7 @@ export function FreeShippingBar({ className }: FreeShippingBarProps) {
9731
9808
  <div className={cn('rounded-lg border border-green-200 bg-green-50 p-3', className)}>
9732
9809
  <div className="flex items-center gap-2 text-sm font-medium text-green-700">
9733
9810
  <svg className="h-4 w-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
9734
- <path
9735
- strokeLinecap="round"
9736
- strokeLinejoin="round"
9737
- strokeWidth={2}
9738
- d="M5 13l4 4L19 7"
9739
- />
9811
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
9740
9812
  </svg>
9741
9813
  {t('freeShippingQualified')}
9742
9814
  </div>
@@ -9798,20 +9870,28 @@ export function CartUpgradeBanner({
9798
9870
  if (sessionStorage.getItem(storageKey)) {
9799
9871
  setDismissed(true);
9800
9872
  }
9801
- } catch {}
9873
+ } catch {
9874
+ /* ignore */
9875
+ }
9802
9876
  }, [storageKey]);
9803
9877
 
9804
9878
  if (dismissed) return null;
9805
9879
 
9806
9880
  const target = suggestion.targetProduct;
9807
9881
  const firstImage = target.images?.[0];
9808
- const imageUrl = firstImage ? (typeof firstImage === 'string' ? firstImage : firstImage.url) : null;
9882
+ const imageUrl = firstImage
9883
+ ? typeof firstImage === 'string'
9884
+ ? firstImage
9885
+ : firstImage.url
9886
+ : null;
9809
9887
  const formattedDelta = formatPrice(parseFloat(suggestion.priceDelta), { currency }) as string;
9810
9888
 
9811
9889
  function handleDismiss() {
9812
9890
  try {
9813
9891
  sessionStorage.setItem(storageKey, '1');
9814
- } catch {}
9892
+ } catch {
9893
+ /* ignore */
9894
+ }
9815
9895
  setDismissed(true);
9816
9896
  }
9817
9897
 
@@ -9844,7 +9924,13 @@ export function CartUpgradeBanner({
9844
9924
  className="text-muted-foreground hover:text-foreground absolute end-2 top-2 text-xs"
9845
9925
  aria-label={t('dismissUpgrade')}
9846
9926
  >
9847
- <svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
9927
+ <svg
9928
+ className="h-4 w-4"
9929
+ fill="none"
9930
+ viewBox="0 0 24 24"
9931
+ stroke="currentColor"
9932
+ strokeWidth={2}
9933
+ >
9848
9934
  <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
9849
9935
  </svg>
9850
9936
  </button>
@@ -9856,7 +9942,12 @@ export function CartUpgradeBanner({
9856
9942
  ) : (
9857
9943
  <div className="text-muted-foreground flex h-full w-full items-center justify-center">
9858
9944
  <svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
9859
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} 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" />
9945
+ <path
9946
+ strokeLinecap="round"
9947
+ strokeLinejoin="round"
9948
+ strokeWidth={1.5}
9949
+ 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"
9950
+ />
9860
9951
  </svg>
9861
9952
  </div>
9862
9953
  )}
@@ -9891,18 +9982,18 @@ import { useState } from 'react';
9891
9982
  import Image from 'next/image';
9892
9983
  import type { CartBundleOffer as CartBundleOfferType } from 'brainerce';
9893
9984
  import { formatPrice } from 'brainerce';
9894
- import { getClient } from '@/lib/brainerce';
9895
9985
  import { useStoreInfo } from '@/providers/store-provider';
9896
9986
  import { useTranslations } from '@/lib/translations';
9897
9987
  import { cn } from '@/lib/utils';
9898
9988
 
9899
9989
  interface CartBundleOfferCardProps {
9900
9990
  offer: CartBundleOfferType;
9991
+ cartId: string;
9901
9992
  onAdd: () => void;
9902
9993
  className?: string;
9903
9994
  }
9904
9995
 
9905
- export function CartBundleOfferCard({ offer, onAdd, className }: CartBundleOfferCardProps) {
9996
+ export function CartBundleOfferCard({ offer, cartId, onAdd, className }: CartBundleOfferCardProps) {
9906
9997
  const { storeInfo } = useStoreInfo();
9907
9998
  const t = useTranslations('cart');
9908
9999
  const currency = storeInfo?.currency || 'USD';
@@ -9910,24 +10001,25 @@ export function CartBundleOfferCard({ offer, onAdd, className }: CartBundleOffer
9910
10001
 
9911
10002
  const product = offer.bundleProduct;
9912
10003
  const firstImage = product.images?.[0];
9913
- const imageUrl = firstImage ? (typeof firstImage === 'string' ? firstImage : firstImage.url) : null;
10004
+ const imageUrl = firstImage
10005
+ ? typeof firstImage === 'string'
10006
+ ? firstImage
10007
+ : firstImage.url
10008
+ : null;
9914
10009
  const originalPrice = parseFloat(offer.originalPrice);
9915
10010
  const discountedPrice = parseFloat(offer.discountedPrice);
9916
10011
  const discountLabel =
9917
10012
  offer.discountType === 'PERCENTAGE'
9918
10013
  ? \`\${offer.discountValue}%\`
9919
- : formatPrice(parseFloat(offer.discountValue), { currency }) as string;
10014
+ : (formatPrice(parseFloat(offer.discountValue), { currency }) as string);
9920
10015
 
9921
10016
  async function handleAdd() {
9922
10017
  if (adding) return;
9923
10018
  try {
9924
10019
  setAdding(true);
10020
+ const { getClient } = await import('@/lib/brainerce');
9925
10021
  const client = getClient();
9926
- await client.smartAddToCart({
9927
- productId: product.id,
9928
- variantId: offer.bundleVariantId || undefined,
9929
- quantity: 1,
9930
- });
10022
+ await client.addBundleToCart(cartId, offer.id);
9931
10023
  onAdd();
9932
10024
  } catch (err) {
9933
10025
  console.error('Failed to add bundle item:', err);
@@ -9950,7 +10042,12 @@ export function CartBundleOfferCard({ offer, onAdd, className }: CartBundleOffer
9950
10042
  ) : (
9951
10043
  <div className="text-muted-foreground flex h-full w-full items-center justify-center">
9952
10044
  <svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
9953
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} 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" />
10045
+ <path
10046
+ strokeLinecap="round"
10047
+ strokeLinejoin="round"
10048
+ strokeWidth={1.5}
10049
+ 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"
10050
+ />
9954
10051
  </svg>
9955
10052
  </div>
9956
10053
  )}
@@ -10015,7 +10112,11 @@ export function OrderBumpCard({ bump, isAdded, onToggle, loading, className }: O
10015
10112
 
10016
10113
  const product = bump.bumpProduct;
10017
10114
  const firstImage = product.images?.[0];
10018
- const imageUrl = firstImage ? (typeof firstImage === 'string' ? firstImage : firstImage.url) : null;
10115
+ const imageUrl = firstImage
10116
+ ? typeof firstImage === 'string'
10117
+ ? firstImage
10118
+ : firstImage.url
10119
+ : null;
10019
10120
  const originalPrice = parseFloat(bump.originalPrice);
10020
10121
  const hasDiscount = bump.discountedPrice != null;
10021
10122
  const discountedPrice = hasDiscount ? parseFloat(bump.discountedPrice!) : null;
package/dist/index.js CHANGED
@@ -792,6 +792,12 @@ const { bundles } = await client.getCartBundles(cartId);
792
792
  // bundles[].bundleProduct \u2014 the product to offer
793
793
  // bundles[].originalPrice / bundles[].discountedPrice \u2014 prices
794
794
  // bundles[].discountType ('PERCENTAGE' | 'FIXED_AMOUNT') + discountValue
795
+
796
+ // Add a bundle to cart (applies the discount automatically):
797
+ await client.addBundleToCart(cartId, bundleOfferId);
798
+ // Remove a bundle from cart:
799
+ await client.removeBundleFromCart(cartId, bundleOfferId);
800
+ // Detect already-added bundles: check cart.items for metadata?.isBundleItem === true
795
801
  \`\`\`
796
802
 
797
803
  **Free shipping progress bar** (configured threshold in channel settings):
@@ -3016,7 +3022,11 @@ export default async function ProductDetailPage({ params }: Props) {
3016
3022
 
3017
3023
  import { useEffect, useState } from 'react';
3018
3024
  import Link from 'next/link';
3019
- import type { CartRecommendationsResponse, CartUpgradesResponse, CartBundlesResponse } from 'brainerce';
3025
+ import type {
3026
+ CartRecommendationsResponse,
3027
+ CartUpgradesResponse,
3028
+ CartBundlesResponse,
3029
+ } from 'brainerce';
3020
3030
  import { getClient } from '@/lib/brainerce';
3021
3031
  import { useCart } from '@/providers/store-provider';
3022
3032
  import { useStoreInfo } from '@/providers/store-provider';
@@ -3056,7 +3066,11 @@ export default function CartPage() {
3056
3066
 
3057
3067
  // Load upgrade suggestions when cart changes
3058
3068
  useEffect(() => {
3059
- if (!cart?.id || cart.items.length === 0 || storeInfo?.upsell?.cartUpgradeBannerEnabled === false) {
3069
+ if (
3070
+ !cart?.id ||
3071
+ cart.items.length === 0 ||
3072
+ storeInfo?.upsell?.cartUpgradeBannerEnabled === false
3073
+ ) {
3060
3074
  setUpgrades(null);
3061
3075
  return;
3062
3076
  }
@@ -3158,7 +3172,12 @@ export default function CartPage() {
3158
3172
  <div className="mt-6 space-y-3">
3159
3173
  <h3 className="text-foreground text-sm font-semibold">{t('bundleOffers')}</h3>
3160
3174
  {bundles.bundles.map((offer) => (
3161
- <CartBundleOfferCard key={offer.id} offer={offer} onAdd={refreshCart} />
3175
+ <CartBundleOfferCard
3176
+ key={offer.id}
3177
+ offer={offer}
3178
+ cartId={cart.id}
3179
+ onAdd={refreshCart}
3180
+ />
3162
3181
  ))}
3163
3182
  </div>
3164
3183
  )}
@@ -7655,8 +7674,9 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
7655
7674
  })
7656
7675
  .catch(() => null);
7657
7676
 
7658
- // B) Load + init SDK as early as possible
7677
+ // B) Load + init SDK as early as possible (skip for sandbox)
7659
7678
  providerPromise.then((providerSdk) => {
7679
+ if (providerSdk?.renderType === 'sandbox') return;
7660
7680
  if (providerSdk?.renderType === 'sdk-widget' && providerSdk.scriptUrl) {
7661
7681
  currentSdk = providerSdk;
7662
7682
  loadScript(providerSdk);
@@ -7683,6 +7703,9 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
7683
7703
  const sdk = resolveClientSdk(intent, providerSdk);
7684
7704
  currentSdk = sdk;
7685
7705
 
7706
+ // Sandbox mode \u2014 no SDK to load, UI handles it
7707
+ if (sdk.renderType === 'sandbox') return;
7708
+
7686
7709
  if (sdk.renderType === 'redirect') {
7687
7710
  window.location.href = intent.clientSecret;
7688
7711
  return;
@@ -7777,6 +7800,48 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
7777
7800
 
7778
7801
  const sdk = resolveClientSdk(paymentIntent, preloadedSdk);
7779
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
+
7780
7845
  if (sdk.renderType === 'sdk-widget') {
7781
7846
  const containerId =
7782
7847
  sdk.containerId || \`\${paymentIntent.provider || 'payment'}-payment-container\`;
@@ -9583,8 +9648,18 @@ function ProductThumb({
9583
9648
  <Image src={imageUrl} alt={name} fill sizes="80px" className="object-cover" />
9584
9649
  ) : (
9585
9650
  <div className="flex h-full w-full items-center justify-center">
9586
- <svg className="text-muted-foreground h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
9587
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} 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" />
9651
+ <svg
9652
+ className="text-muted-foreground h-8 w-8"
9653
+ fill="none"
9654
+ viewBox="0 0 24 24"
9655
+ stroke="currentColor"
9656
+ >
9657
+ <path
9658
+ strokeLinecap="round"
9659
+ strokeLinejoin="round"
9660
+ strokeWidth={1.5}
9661
+ 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"
9662
+ />
9588
9663
  </svg>
9589
9664
  </div>
9590
9665
  )}
@@ -9661,7 +9736,9 @@ export function FrequentlyBoughtTogether({
9661
9736
 
9662
9737
  return (
9663
9738
  <div className={cn('border-border rounded-lg border p-6', className)}>
9664
- <h2 className="text-foreground mb-4 text-xl font-semibold">{t('frequentlyBoughtTogether')}</h2>
9739
+ <h2 className="text-foreground mb-4 text-xl font-semibold">
9740
+ {t('frequentlyBoughtTogether')}
9741
+ </h2>
9665
9742
 
9666
9743
  <div className="flex flex-wrap items-center gap-3">
9667
9744
  {/* Current product (always included, no checkbox) */}
@@ -9748,12 +9825,7 @@ export function FreeShippingBar({ className }: FreeShippingBarProps) {
9748
9825
  <div className={cn('rounded-lg border border-green-200 bg-green-50 p-3', className)}>
9749
9826
  <div className="flex items-center gap-2 text-sm font-medium text-green-700">
9750
9827
  <svg className="h-4 w-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
9751
- <path
9752
- strokeLinecap="round"
9753
- strokeLinejoin="round"
9754
- strokeWidth={2}
9755
- d="M5 13l4 4L19 7"
9756
- />
9828
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
9757
9829
  </svg>
9758
9830
  {t('freeShippingQualified')}
9759
9831
  </div>
@@ -9815,20 +9887,28 @@ export function CartUpgradeBanner({
9815
9887
  if (sessionStorage.getItem(storageKey)) {
9816
9888
  setDismissed(true);
9817
9889
  }
9818
- } catch {}
9890
+ } catch {
9891
+ /* ignore */
9892
+ }
9819
9893
  }, [storageKey]);
9820
9894
 
9821
9895
  if (dismissed) return null;
9822
9896
 
9823
9897
  const target = suggestion.targetProduct;
9824
9898
  const firstImage = target.images?.[0];
9825
- const imageUrl = firstImage ? (typeof firstImage === 'string' ? firstImage : firstImage.url) : null;
9899
+ const imageUrl = firstImage
9900
+ ? typeof firstImage === 'string'
9901
+ ? firstImage
9902
+ : firstImage.url
9903
+ : null;
9826
9904
  const formattedDelta = formatPrice(parseFloat(suggestion.priceDelta), { currency }) as string;
9827
9905
 
9828
9906
  function handleDismiss() {
9829
9907
  try {
9830
9908
  sessionStorage.setItem(storageKey, '1');
9831
- } catch {}
9909
+ } catch {
9910
+ /* ignore */
9911
+ }
9832
9912
  setDismissed(true);
9833
9913
  }
9834
9914
 
@@ -9861,7 +9941,13 @@ export function CartUpgradeBanner({
9861
9941
  className="text-muted-foreground hover:text-foreground absolute end-2 top-2 text-xs"
9862
9942
  aria-label={t('dismissUpgrade')}
9863
9943
  >
9864
- <svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
9944
+ <svg
9945
+ className="h-4 w-4"
9946
+ fill="none"
9947
+ viewBox="0 0 24 24"
9948
+ stroke="currentColor"
9949
+ strokeWidth={2}
9950
+ >
9865
9951
  <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
9866
9952
  </svg>
9867
9953
  </button>
@@ -9873,7 +9959,12 @@ export function CartUpgradeBanner({
9873
9959
  ) : (
9874
9960
  <div className="text-muted-foreground flex h-full w-full items-center justify-center">
9875
9961
  <svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
9876
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} 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" />
9962
+ <path
9963
+ strokeLinecap="round"
9964
+ strokeLinejoin="round"
9965
+ strokeWidth={1.5}
9966
+ 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"
9967
+ />
9877
9968
  </svg>
9878
9969
  </div>
9879
9970
  )}
@@ -9908,18 +9999,18 @@ import { useState } from 'react';
9908
9999
  import Image from 'next/image';
9909
10000
  import type { CartBundleOffer as CartBundleOfferType } from 'brainerce';
9910
10001
  import { formatPrice } from 'brainerce';
9911
- import { getClient } from '@/lib/brainerce';
9912
10002
  import { useStoreInfo } from '@/providers/store-provider';
9913
10003
  import { useTranslations } from '@/lib/translations';
9914
10004
  import { cn } from '@/lib/utils';
9915
10005
 
9916
10006
  interface CartBundleOfferCardProps {
9917
10007
  offer: CartBundleOfferType;
10008
+ cartId: string;
9918
10009
  onAdd: () => void;
9919
10010
  className?: string;
9920
10011
  }
9921
10012
 
9922
- export function CartBundleOfferCard({ offer, onAdd, className }: CartBundleOfferCardProps) {
10013
+ export function CartBundleOfferCard({ offer, cartId, onAdd, className }: CartBundleOfferCardProps) {
9923
10014
  const { storeInfo } = useStoreInfo();
9924
10015
  const t = useTranslations('cart');
9925
10016
  const currency = storeInfo?.currency || 'USD';
@@ -9927,24 +10018,25 @@ export function CartBundleOfferCard({ offer, onAdd, className }: CartBundleOffer
9927
10018
 
9928
10019
  const product = offer.bundleProduct;
9929
10020
  const firstImage = product.images?.[0];
9930
- const imageUrl = firstImage ? (typeof firstImage === 'string' ? firstImage : firstImage.url) : null;
10021
+ const imageUrl = firstImage
10022
+ ? typeof firstImage === 'string'
10023
+ ? firstImage
10024
+ : firstImage.url
10025
+ : null;
9931
10026
  const originalPrice = parseFloat(offer.originalPrice);
9932
10027
  const discountedPrice = parseFloat(offer.discountedPrice);
9933
10028
  const discountLabel =
9934
10029
  offer.discountType === 'PERCENTAGE'
9935
10030
  ? \`\${offer.discountValue}%\`
9936
- : formatPrice(parseFloat(offer.discountValue), { currency }) as string;
10031
+ : (formatPrice(parseFloat(offer.discountValue), { currency }) as string);
9937
10032
 
9938
10033
  async function handleAdd() {
9939
10034
  if (adding) return;
9940
10035
  try {
9941
10036
  setAdding(true);
10037
+ const { getClient } = await import('@/lib/brainerce');
9942
10038
  const client = getClient();
9943
- await client.smartAddToCart({
9944
- productId: product.id,
9945
- variantId: offer.bundleVariantId || undefined,
9946
- quantity: 1,
9947
- });
10039
+ await client.addBundleToCart(cartId, offer.id);
9948
10040
  onAdd();
9949
10041
  } catch (err) {
9950
10042
  console.error('Failed to add bundle item:', err);
@@ -9967,7 +10059,12 @@ export function CartBundleOfferCard({ offer, onAdd, className }: CartBundleOffer
9967
10059
  ) : (
9968
10060
  <div className="text-muted-foreground flex h-full w-full items-center justify-center">
9969
10061
  <svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
9970
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} 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" />
10062
+ <path
10063
+ strokeLinecap="round"
10064
+ strokeLinejoin="round"
10065
+ strokeWidth={1.5}
10066
+ 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"
10067
+ />
9971
10068
  </svg>
9972
10069
  </div>
9973
10070
  )}
@@ -10032,7 +10129,11 @@ export function OrderBumpCard({ bump, isAdded, onToggle, loading, className }: O
10032
10129
 
10033
10130
  const product = bump.bumpProduct;
10034
10131
  const firstImage = product.images?.[0];
10035
- const imageUrl = firstImage ? (typeof firstImage === 'string' ? firstImage : firstImage.url) : null;
10132
+ const imageUrl = firstImage
10133
+ ? typeof firstImage === 'string'
10134
+ ? firstImage
10135
+ : firstImage.url
10136
+ : null;
10036
10137
  const originalPrice = parseFloat(bump.originalPrice);
10037
10138
  const hasDiscount = bump.discountedPrice != null;
10038
10139
  const discountedPrice = hasDiscount ? parseFloat(bump.discountedPrice!) : null;
package/dist/index.mjs CHANGED
@@ -747,6 +747,12 @@ const { bundles } = await client.getCartBundles(cartId);
747
747
  // bundles[].bundleProduct \u2014 the product to offer
748
748
  // bundles[].originalPrice / bundles[].discountedPrice \u2014 prices
749
749
  // bundles[].discountType ('PERCENTAGE' | 'FIXED_AMOUNT') + discountValue
750
+
751
+ // Add a bundle to cart (applies the discount automatically):
752
+ await client.addBundleToCart(cartId, bundleOfferId);
753
+ // Remove a bundle from cart:
754
+ await client.removeBundleFromCart(cartId, bundleOfferId);
755
+ // Detect already-added bundles: check cart.items for metadata?.isBundleItem === true
750
756
  \`\`\`
751
757
 
752
758
  **Free shipping progress bar** (configured threshold in channel settings):
@@ -2971,7 +2977,11 @@ export default async function ProductDetailPage({ params }: Props) {
2971
2977
 
2972
2978
  import { useEffect, useState } from 'react';
2973
2979
  import Link from 'next/link';
2974
- import type { CartRecommendationsResponse, CartUpgradesResponse, CartBundlesResponse } from 'brainerce';
2980
+ import type {
2981
+ CartRecommendationsResponse,
2982
+ CartUpgradesResponse,
2983
+ CartBundlesResponse,
2984
+ } from 'brainerce';
2975
2985
  import { getClient } from '@/lib/brainerce';
2976
2986
  import { useCart } from '@/providers/store-provider';
2977
2987
  import { useStoreInfo } from '@/providers/store-provider';
@@ -3011,7 +3021,11 @@ export default function CartPage() {
3011
3021
 
3012
3022
  // Load upgrade suggestions when cart changes
3013
3023
  useEffect(() => {
3014
- if (!cart?.id || cart.items.length === 0 || storeInfo?.upsell?.cartUpgradeBannerEnabled === false) {
3024
+ if (
3025
+ !cart?.id ||
3026
+ cart.items.length === 0 ||
3027
+ storeInfo?.upsell?.cartUpgradeBannerEnabled === false
3028
+ ) {
3015
3029
  setUpgrades(null);
3016
3030
  return;
3017
3031
  }
@@ -3113,7 +3127,12 @@ export default function CartPage() {
3113
3127
  <div className="mt-6 space-y-3">
3114
3128
  <h3 className="text-foreground text-sm font-semibold">{t('bundleOffers')}</h3>
3115
3129
  {bundles.bundles.map((offer) => (
3116
- <CartBundleOfferCard key={offer.id} offer={offer} onAdd={refreshCart} />
3130
+ <CartBundleOfferCard
3131
+ key={offer.id}
3132
+ offer={offer}
3133
+ cartId={cart.id}
3134
+ onAdd={refreshCart}
3135
+ />
3117
3136
  ))}
3118
3137
  </div>
3119
3138
  )}
@@ -7610,8 +7629,9 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
7610
7629
  })
7611
7630
  .catch(() => null);
7612
7631
 
7613
- // B) Load + init SDK as early as possible
7632
+ // B) Load + init SDK as early as possible (skip for sandbox)
7614
7633
  providerPromise.then((providerSdk) => {
7634
+ if (providerSdk?.renderType === 'sandbox') return;
7615
7635
  if (providerSdk?.renderType === 'sdk-widget' && providerSdk.scriptUrl) {
7616
7636
  currentSdk = providerSdk;
7617
7637
  loadScript(providerSdk);
@@ -7638,6 +7658,9 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
7638
7658
  const sdk = resolveClientSdk(intent, providerSdk);
7639
7659
  currentSdk = sdk;
7640
7660
 
7661
+ // Sandbox mode \u2014 no SDK to load, UI handles it
7662
+ if (sdk.renderType === 'sandbox') return;
7663
+
7641
7664
  if (sdk.renderType === 'redirect') {
7642
7665
  window.location.href = intent.clientSecret;
7643
7666
  return;
@@ -7732,6 +7755,48 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
7732
7755
 
7733
7756
  const sdk = resolveClientSdk(paymentIntent, preloadedSdk);
7734
7757
 
7758
+ if (sdk.renderType === 'sandbox') {
7759
+ const handleCompleteSandbox = async () => {
7760
+ setLoading(true);
7761
+ try {
7762
+ const client = getClient();
7763
+ await client.completeGuestCheckout(checkoutId);
7764
+ window.location.href = \`/order-confirmation?checkout_id=\${checkoutId}\`;
7765
+ } catch (err) {
7766
+ setError(err instanceof Error ? err.message : t('paymentError'));
7767
+ setLoading(false);
7768
+ }
7769
+ };
7770
+
7771
+ return (
7772
+ <div className={cn('py-8 text-center', className)}>
7773
+ <div className="mx-auto max-w-md rounded-lg border border-amber-200 bg-amber-50 p-6">
7774
+ <svg
7775
+ className="mx-auto mb-3 h-10 w-10 text-amber-500"
7776
+ fill="none"
7777
+ viewBox="0 0 24 24"
7778
+ stroke="currentColor"
7779
+ >
7780
+ <path
7781
+ strokeLinecap="round"
7782
+ strokeLinejoin="round"
7783
+ strokeWidth={1.5}
7784
+ 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"
7785
+ />
7786
+ </svg>
7787
+ <h3 className="text-foreground mb-1 text-lg font-semibold">{t('sandboxTitle')}</h3>
7788
+ <p className="text-muted-foreground mb-4 text-sm">{t('sandboxDescription')}</p>
7789
+ <button
7790
+ onClick={handleCompleteSandbox}
7791
+ 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"
7792
+ >
7793
+ {t('completeTestOrder')}
7794
+ </button>
7795
+ </div>
7796
+ </div>
7797
+ );
7798
+ }
7799
+
7735
7800
  if (sdk.renderType === 'sdk-widget') {
7736
7801
  const containerId =
7737
7802
  sdk.containerId || \`\${paymentIntent.provider || 'payment'}-payment-container\`;
@@ -9538,8 +9603,18 @@ function ProductThumb({
9538
9603
  <Image src={imageUrl} alt={name} fill sizes="80px" className="object-cover" />
9539
9604
  ) : (
9540
9605
  <div className="flex h-full w-full items-center justify-center">
9541
- <svg className="text-muted-foreground h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
9542
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} 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" />
9606
+ <svg
9607
+ className="text-muted-foreground h-8 w-8"
9608
+ fill="none"
9609
+ viewBox="0 0 24 24"
9610
+ stroke="currentColor"
9611
+ >
9612
+ <path
9613
+ strokeLinecap="round"
9614
+ strokeLinejoin="round"
9615
+ strokeWidth={1.5}
9616
+ 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"
9617
+ />
9543
9618
  </svg>
9544
9619
  </div>
9545
9620
  )}
@@ -9616,7 +9691,9 @@ export function FrequentlyBoughtTogether({
9616
9691
 
9617
9692
  return (
9618
9693
  <div className={cn('border-border rounded-lg border p-6', className)}>
9619
- <h2 className="text-foreground mb-4 text-xl font-semibold">{t('frequentlyBoughtTogether')}</h2>
9694
+ <h2 className="text-foreground mb-4 text-xl font-semibold">
9695
+ {t('frequentlyBoughtTogether')}
9696
+ </h2>
9620
9697
 
9621
9698
  <div className="flex flex-wrap items-center gap-3">
9622
9699
  {/* Current product (always included, no checkbox) */}
@@ -9703,12 +9780,7 @@ export function FreeShippingBar({ className }: FreeShippingBarProps) {
9703
9780
  <div className={cn('rounded-lg border border-green-200 bg-green-50 p-3', className)}>
9704
9781
  <div className="flex items-center gap-2 text-sm font-medium text-green-700">
9705
9782
  <svg className="h-4 w-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
9706
- <path
9707
- strokeLinecap="round"
9708
- strokeLinejoin="round"
9709
- strokeWidth={2}
9710
- d="M5 13l4 4L19 7"
9711
- />
9783
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
9712
9784
  </svg>
9713
9785
  {t('freeShippingQualified')}
9714
9786
  </div>
@@ -9770,20 +9842,28 @@ export function CartUpgradeBanner({
9770
9842
  if (sessionStorage.getItem(storageKey)) {
9771
9843
  setDismissed(true);
9772
9844
  }
9773
- } catch {}
9845
+ } catch {
9846
+ /* ignore */
9847
+ }
9774
9848
  }, [storageKey]);
9775
9849
 
9776
9850
  if (dismissed) return null;
9777
9851
 
9778
9852
  const target = suggestion.targetProduct;
9779
9853
  const firstImage = target.images?.[0];
9780
- const imageUrl = firstImage ? (typeof firstImage === 'string' ? firstImage : firstImage.url) : null;
9854
+ const imageUrl = firstImage
9855
+ ? typeof firstImage === 'string'
9856
+ ? firstImage
9857
+ : firstImage.url
9858
+ : null;
9781
9859
  const formattedDelta = formatPrice(parseFloat(suggestion.priceDelta), { currency }) as string;
9782
9860
 
9783
9861
  function handleDismiss() {
9784
9862
  try {
9785
9863
  sessionStorage.setItem(storageKey, '1');
9786
- } catch {}
9864
+ } catch {
9865
+ /* ignore */
9866
+ }
9787
9867
  setDismissed(true);
9788
9868
  }
9789
9869
 
@@ -9816,7 +9896,13 @@ export function CartUpgradeBanner({
9816
9896
  className="text-muted-foreground hover:text-foreground absolute end-2 top-2 text-xs"
9817
9897
  aria-label={t('dismissUpgrade')}
9818
9898
  >
9819
- <svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
9899
+ <svg
9900
+ className="h-4 w-4"
9901
+ fill="none"
9902
+ viewBox="0 0 24 24"
9903
+ stroke="currentColor"
9904
+ strokeWidth={2}
9905
+ >
9820
9906
  <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
9821
9907
  </svg>
9822
9908
  </button>
@@ -9828,7 +9914,12 @@ export function CartUpgradeBanner({
9828
9914
  ) : (
9829
9915
  <div className="text-muted-foreground flex h-full w-full items-center justify-center">
9830
9916
  <svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
9831
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} 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" />
9917
+ <path
9918
+ strokeLinecap="round"
9919
+ strokeLinejoin="round"
9920
+ strokeWidth={1.5}
9921
+ 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"
9922
+ />
9832
9923
  </svg>
9833
9924
  </div>
9834
9925
  )}
@@ -9863,18 +9954,18 @@ import { useState } from 'react';
9863
9954
  import Image from 'next/image';
9864
9955
  import type { CartBundleOffer as CartBundleOfferType } from 'brainerce';
9865
9956
  import { formatPrice } from 'brainerce';
9866
- import { getClient } from '@/lib/brainerce';
9867
9957
  import { useStoreInfo } from '@/providers/store-provider';
9868
9958
  import { useTranslations } from '@/lib/translations';
9869
9959
  import { cn } from '@/lib/utils';
9870
9960
 
9871
9961
  interface CartBundleOfferCardProps {
9872
9962
  offer: CartBundleOfferType;
9963
+ cartId: string;
9873
9964
  onAdd: () => void;
9874
9965
  className?: string;
9875
9966
  }
9876
9967
 
9877
- export function CartBundleOfferCard({ offer, onAdd, className }: CartBundleOfferCardProps) {
9968
+ export function CartBundleOfferCard({ offer, cartId, onAdd, className }: CartBundleOfferCardProps) {
9878
9969
  const { storeInfo } = useStoreInfo();
9879
9970
  const t = useTranslations('cart');
9880
9971
  const currency = storeInfo?.currency || 'USD';
@@ -9882,24 +9973,25 @@ export function CartBundleOfferCard({ offer, onAdd, className }: CartBundleOffer
9882
9973
 
9883
9974
  const product = offer.bundleProduct;
9884
9975
  const firstImage = product.images?.[0];
9885
- const imageUrl = firstImage ? (typeof firstImage === 'string' ? firstImage : firstImage.url) : null;
9976
+ const imageUrl = firstImage
9977
+ ? typeof firstImage === 'string'
9978
+ ? firstImage
9979
+ : firstImage.url
9980
+ : null;
9886
9981
  const originalPrice = parseFloat(offer.originalPrice);
9887
9982
  const discountedPrice = parseFloat(offer.discountedPrice);
9888
9983
  const discountLabel =
9889
9984
  offer.discountType === 'PERCENTAGE'
9890
9985
  ? \`\${offer.discountValue}%\`
9891
- : formatPrice(parseFloat(offer.discountValue), { currency }) as string;
9986
+ : (formatPrice(parseFloat(offer.discountValue), { currency }) as string);
9892
9987
 
9893
9988
  async function handleAdd() {
9894
9989
  if (adding) return;
9895
9990
  try {
9896
9991
  setAdding(true);
9992
+ const { getClient } = await import('@/lib/brainerce');
9897
9993
  const client = getClient();
9898
- await client.smartAddToCart({
9899
- productId: product.id,
9900
- variantId: offer.bundleVariantId || undefined,
9901
- quantity: 1,
9902
- });
9994
+ await client.addBundleToCart(cartId, offer.id);
9903
9995
  onAdd();
9904
9996
  } catch (err) {
9905
9997
  console.error('Failed to add bundle item:', err);
@@ -9922,7 +10014,12 @@ export function CartBundleOfferCard({ offer, onAdd, className }: CartBundleOffer
9922
10014
  ) : (
9923
10015
  <div className="text-muted-foreground flex h-full w-full items-center justify-center">
9924
10016
  <svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
9925
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} 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" />
10017
+ <path
10018
+ strokeLinecap="round"
10019
+ strokeLinejoin="round"
10020
+ strokeWidth={1.5}
10021
+ d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
10022
+ />
9926
10023
  </svg>
9927
10024
  </div>
9928
10025
  )}
@@ -9987,7 +10084,11 @@ export function OrderBumpCard({ bump, isAdded, onToggle, loading, className }: O
9987
10084
 
9988
10085
  const product = bump.bumpProduct;
9989
10086
  const firstImage = product.images?.[0];
9990
- const imageUrl = firstImage ? (typeof firstImage === 'string' ? firstImage : firstImage.url) : null;
10087
+ const imageUrl = firstImage
10088
+ ? typeof firstImage === 'string'
10089
+ ? firstImage
10090
+ : firstImage.url
10091
+ : null;
9991
10092
  const originalPrice = parseFloat(bump.originalPrice);
9992
10093
  const hasDiscount = bump.discountedPrice != null;
9993
10094
  const discountedPrice = hasDiscount ? parseFloat(bump.discountedPrice!) : null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainerce/mcp-server",
3
- "version": "2.1.0",
3
+ "version": "2.3.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"