@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 +130 -29
- package/dist/bin/stdio.js +130 -29
- package/dist/index.js +130 -29
- package/dist/index.mjs +130 -29
- package/package.json +1 -1
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 {
|
|
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 (
|
|
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
|
|
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
|
|
9572
|
-
|
|
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">
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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 {
|
|
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 (
|
|
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
|
|
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
|
|
9570
|
-
|
|
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">
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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 {
|
|
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 (
|
|
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
|
|
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
|
|
9587
|
-
|
|
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">
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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 {
|
|
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 (
|
|
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
|
|
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
|
|
9542
|
-
|
|
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">
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
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"
|