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 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.15.2",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-brainerce-store",
3
- "version": "1.16.0",
3
+ "version": "1.18.0",
4
4
  "description": "Scaffold a production-ready e-commerce storefront connected to Brainerce",
5
5
  "bin": {
6
6
  "create-brainerce-store": "dist/index.js"
@@ -2,7 +2,11 @@
2
2
 
3
3
  import { useEffect, useState } from 'react';
4
4
  import Link from 'next/link';
5
- import type { CartRecommendationsResponse, CartUpgradesResponse, CartBundlesResponse } from 'brainerce';
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 (!cart?.id || cart.items.length === 0 || storeInfo?.upsell?.cartUpgradeBannerEnabled === false) {
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 key={offer.id} offer={offer} onAdd={refreshCart} />
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 ? (typeof firstImage === 'string' ? firstImage : firstImage.url) : null;
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.smartAddToCart({
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 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" />
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 ? (typeof firstImage === 'string' ? firstImage : firstImage.url) : null;
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 className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
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 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" />
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 ? (typeof firstImage === 'string' ? firstImage : firstImage.url) : null;
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 className="text-muted-foreground h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
63
- <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" />
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">{t('frequentlyBoughtTogether')}</h2>
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) */}