create-brainerce-store 1.16.0 → 1.18.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/index.js +1 -1
- package/messages/en.json +3 -0
- package/messages/he.json +3 -0
- package/package.json +1 -1
- package/templates/nextjs/base/src/app/cart/page.tsx +16 -3
- package/templates/nextjs/base/src/components/cart/cart-bundle-offer.tsx +16 -10
- package/templates/nextjs/base/src/components/cart/cart-upgrade-banner.tsx +24 -5
- package/templates/nextjs/base/src/components/cart/free-shipping-bar.tsx +1 -6
- package/templates/nextjs/base/src/components/checkout/order-bump-card.tsx +5 -1
- package/templates/nextjs/base/src/components/checkout/payment-step.tsx +47 -1
- package/templates/nextjs/base/src/components/products/frequently-bought-together.tsx +15 -3
package/dist/index.js
CHANGED
|
@@ -31,7 +31,7 @@ var require_package = __commonJS({
|
|
|
31
31
|
"package.json"(exports2, module2) {
|
|
32
32
|
module2.exports = {
|
|
33
33
|
name: "create-brainerce-store",
|
|
34
|
-
version: "1.
|
|
34
|
+
version: "1.18.0",
|
|
35
35
|
description: "Scaffold a production-ready e-commerce storefront connected to Brainerce",
|
|
36
36
|
bin: {
|
|
37
37
|
"create-brainerce-store": "dist/index.js"
|
package/messages/en.json
CHANGED
|
@@ -154,6 +154,9 @@
|
|
|
154
154
|
"paymentNotConfigured": "Payment Not Configured",
|
|
155
155
|
"paymentNotConfiguredDesc": "Payment has not been set up for this store yet. Please contact the store owner.",
|
|
156
156
|
"paymentError": "Payment Error",
|
|
157
|
+
"sandboxTitle": "Sandbox Mode",
|
|
158
|
+
"sandboxDescription": "This is a test order. No real payment will be processed.",
|
|
159
|
+
"completeTestOrder": "Complete Test Order",
|
|
157
160
|
"redirectingToPayment": "Redirecting to payment provider...",
|
|
158
161
|
"redirectingHint": "If you are not redirected automatically, ",
|
|
159
162
|
"clickHere": "click here",
|
package/messages/he.json
CHANGED
|
@@ -154,6 +154,9 @@
|
|
|
154
154
|
"paymentNotConfigured": "תשלום לא מוגדר",
|
|
155
155
|
"paymentNotConfiguredDesc": "התשלום עדיין לא הוגדר לחנות זו. אנא פנו לבעל החנות.",
|
|
156
156
|
"paymentError": "שגיאת תשלום",
|
|
157
|
+
"sandboxTitle": "מצב בדיקה",
|
|
158
|
+
"sandboxDescription": "זוהי הזמנת בדיקה. לא יבוצע תשלום אמיתי.",
|
|
159
|
+
"completeTestOrder": "השלם הזמנת בדיקה",
|
|
157
160
|
"redirectingToPayment": "...מפנה לספק התשלום",
|
|
158
161
|
"redirectingHint": "אם אינכם מופנים אוטומטית, ",
|
|
159
162
|
"clickHere": "לחצו כאן",
|
package/package.json
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState } from 'react';
|
|
4
4
|
import Link from 'next/link';
|
|
5
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
CartRecommendationsResponse,
|
|
7
|
+
CartUpgradesResponse,
|
|
8
|
+
CartBundlesResponse,
|
|
9
|
+
} from 'brainerce';
|
|
6
10
|
import { getClient } from '@/lib/brainerce';
|
|
7
11
|
import { useCart } from '@/providers/store-provider';
|
|
8
12
|
import { useStoreInfo } from '@/providers/store-provider';
|
|
@@ -42,7 +46,11 @@ export default function CartPage() {
|
|
|
42
46
|
|
|
43
47
|
// Load upgrade suggestions when cart changes
|
|
44
48
|
useEffect(() => {
|
|
45
|
-
if (
|
|
49
|
+
if (
|
|
50
|
+
!cart?.id ||
|
|
51
|
+
cart.items.length === 0 ||
|
|
52
|
+
storeInfo?.upsell?.cartUpgradeBannerEnabled === false
|
|
53
|
+
) {
|
|
46
54
|
setUpgrades(null);
|
|
47
55
|
return;
|
|
48
56
|
}
|
|
@@ -144,7 +152,12 @@ export default function CartPage() {
|
|
|
144
152
|
<div className="mt-6 space-y-3">
|
|
145
153
|
<h3 className="text-foreground text-sm font-semibold">{t('bundleOffers')}</h3>
|
|
146
154
|
{bundles.bundles.map((offer) => (
|
|
147
|
-
<CartBundleOfferCard
|
|
155
|
+
<CartBundleOfferCard
|
|
156
|
+
key={offer.id}
|
|
157
|
+
offer={offer}
|
|
158
|
+
cartId={cart.id}
|
|
159
|
+
onAdd={refreshCart}
|
|
160
|
+
/>
|
|
148
161
|
))}
|
|
149
162
|
</div>
|
|
150
163
|
)}
|
|
@@ -4,18 +4,18 @@ import { useState } from 'react';
|
|
|
4
4
|
import Image from 'next/image';
|
|
5
5
|
import type { CartBundleOffer as CartBundleOfferType } from 'brainerce';
|
|
6
6
|
import { formatPrice } from 'brainerce';
|
|
7
|
-
import { getClient } from '@/lib/brainerce';
|
|
8
7
|
import { useStoreInfo } from '@/providers/store-provider';
|
|
9
8
|
import { useTranslations } from '@/lib/translations';
|
|
10
9
|
import { cn } from '@/lib/utils';
|
|
11
10
|
|
|
12
11
|
interface CartBundleOfferCardProps {
|
|
13
12
|
offer: CartBundleOfferType;
|
|
13
|
+
cartId: string;
|
|
14
14
|
onAdd: () => void;
|
|
15
15
|
className?: string;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export function CartBundleOfferCard({ offer, onAdd, className }: CartBundleOfferCardProps) {
|
|
18
|
+
export function CartBundleOfferCard({ offer, cartId, onAdd, className }: CartBundleOfferCardProps) {
|
|
19
19
|
const { storeInfo } = useStoreInfo();
|
|
20
20
|
const t = useTranslations('cart');
|
|
21
21
|
const currency = storeInfo?.currency || 'USD';
|
|
@@ -23,24 +23,25 @@ export function CartBundleOfferCard({ offer, onAdd, className }: CartBundleOffer
|
|
|
23
23
|
|
|
24
24
|
const product = offer.bundleProduct;
|
|
25
25
|
const firstImage = product.images?.[0];
|
|
26
|
-
const imageUrl = firstImage
|
|
26
|
+
const imageUrl = firstImage
|
|
27
|
+
? typeof firstImage === 'string'
|
|
28
|
+
? firstImage
|
|
29
|
+
: firstImage.url
|
|
30
|
+
: null;
|
|
27
31
|
const originalPrice = parseFloat(offer.originalPrice);
|
|
28
32
|
const discountedPrice = parseFloat(offer.discountedPrice);
|
|
29
33
|
const discountLabel =
|
|
30
34
|
offer.discountType === 'PERCENTAGE'
|
|
31
35
|
? `${offer.discountValue}%`
|
|
32
|
-
: formatPrice(parseFloat(offer.discountValue), { currency }) as string;
|
|
36
|
+
: (formatPrice(parseFloat(offer.discountValue), { currency }) as string);
|
|
33
37
|
|
|
34
38
|
async function handleAdd() {
|
|
35
39
|
if (adding) return;
|
|
36
40
|
try {
|
|
37
41
|
setAdding(true);
|
|
42
|
+
const { getClient } = await import('@/lib/brainerce');
|
|
38
43
|
const client = getClient();
|
|
39
|
-
await client.
|
|
40
|
-
productId: product.id,
|
|
41
|
-
variantId: offer.bundleVariantId || undefined,
|
|
42
|
-
quantity: 1,
|
|
43
|
-
});
|
|
44
|
+
await client.addBundleToCart(cartId, offer.id);
|
|
44
45
|
onAdd();
|
|
45
46
|
} catch (err) {
|
|
46
47
|
console.error('Failed to add bundle item:', err);
|
|
@@ -63,7 +64,12 @@ export function CartBundleOfferCard({ offer, onAdd, className }: CartBundleOffer
|
|
|
63
64
|
) : (
|
|
64
65
|
<div className="text-muted-foreground flex h-full w-full items-center justify-center">
|
|
65
66
|
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
66
|
-
<path
|
|
67
|
+
<path
|
|
68
|
+
strokeLinecap="round"
|
|
69
|
+
strokeLinejoin="round"
|
|
70
|
+
strokeWidth={1.5}
|
|
71
|
+
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"
|
|
72
|
+
/>
|
|
67
73
|
</svg>
|
|
68
74
|
</div>
|
|
69
75
|
)}
|
|
@@ -35,20 +35,28 @@ export function CartUpgradeBanner({
|
|
|
35
35
|
if (sessionStorage.getItem(storageKey)) {
|
|
36
36
|
setDismissed(true);
|
|
37
37
|
}
|
|
38
|
-
} catch {
|
|
38
|
+
} catch {
|
|
39
|
+
/* ignore */
|
|
40
|
+
}
|
|
39
41
|
}, [storageKey]);
|
|
40
42
|
|
|
41
43
|
if (dismissed) return null;
|
|
42
44
|
|
|
43
45
|
const target = suggestion.targetProduct;
|
|
44
46
|
const firstImage = target.images?.[0];
|
|
45
|
-
const imageUrl = firstImage
|
|
47
|
+
const imageUrl = firstImage
|
|
48
|
+
? typeof firstImage === 'string'
|
|
49
|
+
? firstImage
|
|
50
|
+
: firstImage.url
|
|
51
|
+
: null;
|
|
46
52
|
const formattedDelta = formatPrice(parseFloat(suggestion.priceDelta), { currency }) as string;
|
|
47
53
|
|
|
48
54
|
function handleDismiss() {
|
|
49
55
|
try {
|
|
50
56
|
sessionStorage.setItem(storageKey, '1');
|
|
51
|
-
} catch {
|
|
57
|
+
} catch {
|
|
58
|
+
/* ignore */
|
|
59
|
+
}
|
|
52
60
|
setDismissed(true);
|
|
53
61
|
}
|
|
54
62
|
|
|
@@ -81,7 +89,13 @@ export function CartUpgradeBanner({
|
|
|
81
89
|
className="text-muted-foreground hover:text-foreground absolute end-2 top-2 text-xs"
|
|
82
90
|
aria-label={t('dismissUpgrade')}
|
|
83
91
|
>
|
|
84
|
-
<svg
|
|
92
|
+
<svg
|
|
93
|
+
className="h-4 w-4"
|
|
94
|
+
fill="none"
|
|
95
|
+
viewBox="0 0 24 24"
|
|
96
|
+
stroke="currentColor"
|
|
97
|
+
strokeWidth={2}
|
|
98
|
+
>
|
|
85
99
|
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
86
100
|
</svg>
|
|
87
101
|
</button>
|
|
@@ -93,7 +107,12 @@ export function CartUpgradeBanner({
|
|
|
93
107
|
) : (
|
|
94
108
|
<div className="text-muted-foreground flex h-full w-full items-center justify-center">
|
|
95
109
|
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
96
|
-
<path
|
|
110
|
+
<path
|
|
111
|
+
strokeLinecap="round"
|
|
112
|
+
strokeLinejoin="round"
|
|
113
|
+
strokeWidth={1.5}
|
|
114
|
+
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"
|
|
115
|
+
/>
|
|
97
116
|
</svg>
|
|
98
117
|
</div>
|
|
99
118
|
)}
|
|
@@ -33,12 +33,7 @@ export function FreeShippingBar({ className }: FreeShippingBarProps) {
|
|
|
33
33
|
<div className={cn('rounded-lg border border-green-200 bg-green-50 p-3', className)}>
|
|
34
34
|
<div className="flex items-center gap-2 text-sm font-medium text-green-700">
|
|
35
35
|
<svg className="h-4 w-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
36
|
-
<path
|
|
37
|
-
strokeLinecap="round"
|
|
38
|
-
strokeLinejoin="round"
|
|
39
|
-
strokeWidth={2}
|
|
40
|
-
d="M5 13l4 4L19 7"
|
|
41
|
-
/>
|
|
36
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
42
37
|
</svg>
|
|
43
38
|
{t('freeShippingQualified')}
|
|
44
39
|
</div>
|
|
@@ -22,7 +22,11 @@ export function OrderBumpCard({ bump, isAdded, onToggle, loading, className }: O
|
|
|
22
22
|
|
|
23
23
|
const product = bump.bumpProduct;
|
|
24
24
|
const firstImage = product.images?.[0];
|
|
25
|
-
const imageUrl = firstImage
|
|
25
|
+
const imageUrl = firstImage
|
|
26
|
+
? typeof firstImage === 'string'
|
|
27
|
+
? firstImage
|
|
28
|
+
: firstImage.url
|
|
29
|
+
: null;
|
|
26
30
|
const originalPrice = parseFloat(bump.originalPrice);
|
|
27
31
|
const hasDiscount = bump.discountedPrice != null;
|
|
28
32
|
const discountedPrice = hasDiscount ? parseFloat(bump.discountedPrice!) : null;
|
|
@@ -305,8 +305,9 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
|
|
|
305
305
|
})
|
|
306
306
|
.catch(() => null);
|
|
307
307
|
|
|
308
|
-
// B) Load + init SDK as early as possible
|
|
308
|
+
// B) Load + init SDK as early as possible (skip for sandbox)
|
|
309
309
|
providerPromise.then((providerSdk) => {
|
|
310
|
+
if (providerSdk?.renderType === 'sandbox') return;
|
|
310
311
|
if (providerSdk?.renderType === 'sdk-widget' && providerSdk.scriptUrl) {
|
|
311
312
|
currentSdk = providerSdk;
|
|
312
313
|
loadScript(providerSdk);
|
|
@@ -333,6 +334,9 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
|
|
|
333
334
|
const sdk = resolveClientSdk(intent, providerSdk);
|
|
334
335
|
currentSdk = sdk;
|
|
335
336
|
|
|
337
|
+
// Sandbox mode — no SDK to load, UI handles it
|
|
338
|
+
if (sdk.renderType === 'sandbox') return;
|
|
339
|
+
|
|
336
340
|
if (sdk.renderType === 'redirect') {
|
|
337
341
|
window.location.href = intent.clientSecret;
|
|
338
342
|
return;
|
|
@@ -427,6 +431,48 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
|
|
|
427
431
|
|
|
428
432
|
const sdk = resolveClientSdk(paymentIntent, preloadedSdk);
|
|
429
433
|
|
|
434
|
+
if (sdk.renderType === 'sandbox') {
|
|
435
|
+
const handleCompleteSandbox = async () => {
|
|
436
|
+
setLoading(true);
|
|
437
|
+
try {
|
|
438
|
+
const client = getClient();
|
|
439
|
+
await client.completeGuestCheckout(checkoutId);
|
|
440
|
+
window.location.href = `/order-confirmation?checkout_id=${checkoutId}`;
|
|
441
|
+
} catch (err) {
|
|
442
|
+
setError(err instanceof Error ? err.message : t('paymentError'));
|
|
443
|
+
setLoading(false);
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
return (
|
|
448
|
+
<div className={cn('py-8 text-center', className)}>
|
|
449
|
+
<div className="mx-auto max-w-md rounded-lg border border-amber-200 bg-amber-50 p-6">
|
|
450
|
+
<svg
|
|
451
|
+
className="mx-auto mb-3 h-10 w-10 text-amber-500"
|
|
452
|
+
fill="none"
|
|
453
|
+
viewBox="0 0 24 24"
|
|
454
|
+
stroke="currentColor"
|
|
455
|
+
>
|
|
456
|
+
<path
|
|
457
|
+
strokeLinecap="round"
|
|
458
|
+
strokeLinejoin="round"
|
|
459
|
+
strokeWidth={1.5}
|
|
460
|
+
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"
|
|
461
|
+
/>
|
|
462
|
+
</svg>
|
|
463
|
+
<h3 className="text-foreground mb-1 text-lg font-semibold">{t('sandboxTitle')}</h3>
|
|
464
|
+
<p className="text-muted-foreground mb-4 text-sm">{t('sandboxDescription')}</p>
|
|
465
|
+
<button
|
|
466
|
+
onClick={handleCompleteSandbox}
|
|
467
|
+
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"
|
|
468
|
+
>
|
|
469
|
+
{t('completeTestOrder')}
|
|
470
|
+
</button>
|
|
471
|
+
</div>
|
|
472
|
+
</div>
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
|
|
430
476
|
if (sdk.renderType === 'sdk-widget') {
|
|
431
477
|
const containerId =
|
|
432
478
|
sdk.containerId || `${paymentIntent.provider || 'payment'}-payment-container`;
|
|
@@ -59,8 +59,18 @@ function ProductThumb({
|
|
|
59
59
|
<Image src={imageUrl} alt={name} fill sizes="80px" className="object-cover" />
|
|
60
60
|
) : (
|
|
61
61
|
<div className="flex h-full w-full items-center justify-center">
|
|
62
|
-
<svg
|
|
63
|
-
|
|
62
|
+
<svg
|
|
63
|
+
className="text-muted-foreground h-8 w-8"
|
|
64
|
+
fill="none"
|
|
65
|
+
viewBox="0 0 24 24"
|
|
66
|
+
stroke="currentColor"
|
|
67
|
+
>
|
|
68
|
+
<path
|
|
69
|
+
strokeLinecap="round"
|
|
70
|
+
strokeLinejoin="round"
|
|
71
|
+
strokeWidth={1.5}
|
|
72
|
+
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"
|
|
73
|
+
/>
|
|
64
74
|
</svg>
|
|
65
75
|
</div>
|
|
66
76
|
)}
|
|
@@ -137,7 +147,9 @@ export function FrequentlyBoughtTogether({
|
|
|
137
147
|
|
|
138
148
|
return (
|
|
139
149
|
<div className={cn('border-border rounded-lg border p-6', className)}>
|
|
140
|
-
<h2 className="text-foreground mb-4 text-xl font-semibold">
|
|
150
|
+
<h2 className="text-foreground mb-4 text-xl font-semibold">
|
|
151
|
+
{t('frequentlyBoughtTogether')}
|
|
152
|
+
</h2>
|
|
141
153
|
|
|
142
154
|
<div className="flex flex-wrap items-center gap-3">
|
|
143
155
|
{/* Current product (always included, no checkbox) */}
|