create-brainerce-store 1.14.0 → 1.14.2

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.14.0",
34
+ version: "1.14.2",
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-brainerce-store",
3
- "version": "1.14.0",
3
+ "version": "1.14.2",
4
4
  "description": "Scaffold a production-ready e-commerce storefront connected to Brainerce",
5
5
  "bin": {
6
6
  "create-brainerce-store": "dist/index.js"
@@ -1,6 +1,10 @@
1
1
  # Brainerce Connection
2
2
  NEXT_PUBLIC_BRAINERCE_CONNECTION_ID=<%= connectionId %>
3
3
 
4
+ # Store info (pre-fetched during setup to avoid flash on first load)
5
+ NEXT_PUBLIC_STORE_NAME=<%= storeName %>
6
+ NEXT_PUBLIC_STORE_CURRENCY=<%= currency %>
7
+
4
8
  # Backend API URL (server-side only — used by BFF proxy and SSR, never exposed to browser)
5
9
  BRAINERCE_API_URL=<%= apiBaseUrl %>
6
10
 
@@ -6,7 +6,8 @@
6
6
  "dev": "next dev",
7
7
  "build": "next build",
8
8
  "start": "next start",
9
- "lint": "next lint"
9
+ "lint": "next lint",
10
+ "setup": "node scripts/fetch-store-info.mjs"
10
11
  },
11
12
  "dependencies": {
12
13
  "brainerce": "^1.11.0",
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Setup script: fetches store info from Brainerce using the connection ID
3
+ * and saves NEXT_PUBLIC_STORE_NAME (and other public fields) to .env.local.
4
+ *
5
+ * Run: node scripts/fetch-store-info.mjs
6
+ */
7
+
8
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
9
+ import { join } from 'path';
10
+
11
+ const envPath = join(process.cwd(), '.env.local');
12
+
13
+ if (!existsSync(envPath)) {
14
+ console.error('❌ .env.local not found. Create it first with NEXT_PUBLIC_BRAINERCE_CONNECTION_ID set.');
15
+ process.exit(1);
16
+ }
17
+
18
+ const envContent = readFileSync(envPath, 'utf-8');
19
+
20
+ function getVar(content, key) {
21
+ const match = content.match(new RegExp(`^${key}=(.*)$`, 'm'));
22
+ return match ? match[1].trim() : null;
23
+ }
24
+
25
+ function setVar(content, key, value) {
26
+ const regex = new RegExp(`^${key}=.*$`, 'm');
27
+ if (regex.test(content)) {
28
+ return content.replace(regex, `${key}=${value}`);
29
+ }
30
+ return content.trimEnd() + `\n${key}=${value}\n`;
31
+ }
32
+
33
+ const connectionId = getVar(envContent, 'NEXT_PUBLIC_BRAINERCE_CONNECTION_ID');
34
+ const apiUrl = (getVar(envContent, 'BRAINERCE_API_URL') || 'https://api.brainerce.com').replace(/\/$/, '');
35
+
36
+ if (!connectionId) {
37
+ console.error('❌ NEXT_PUBLIC_BRAINERCE_CONNECTION_ID is not set in .env.local');
38
+ process.exit(1);
39
+ }
40
+
41
+ console.log(`Fetching store info for connection: ${connectionId} ...`);
42
+
43
+ let storeInfo;
44
+ try {
45
+ const res = await fetch(`${apiUrl}/api/vc/${connectionId}/info`);
46
+ if (!res.ok) {
47
+ console.error(`❌ API returned ${res.status}: ${await res.text()}`);
48
+ process.exit(1);
49
+ }
50
+ storeInfo = await res.json();
51
+ } catch (err) {
52
+ console.error(`❌ Failed to reach ${apiUrl}: ${err.message}`);
53
+ process.exit(1);
54
+ }
55
+
56
+ const name = storeInfo.name;
57
+ const currency = storeInfo.currency;
58
+
59
+ if (!name) {
60
+ console.error('❌ Store info response has no `name` field:', storeInfo);
61
+ process.exit(1);
62
+ }
63
+
64
+ let updated = envContent;
65
+ updated = setVar(updated, 'NEXT_PUBLIC_STORE_NAME', name);
66
+ if (currency) {
67
+ updated = setVar(updated, 'NEXT_PUBLIC_STORE_CURRENCY', currency);
68
+ }
69
+
70
+ writeFileSync(envPath, updated, 'utf-8');
71
+
72
+ console.log(`✓ NEXT_PUBLIC_STORE_NAME=${name}`);
73
+ if (currency) console.log(`✓ NEXT_PUBLIC_STORE_CURRENCY=${currency}`);
74
+ console.log('Done. Restart the dev server for changes to take effect.');
@@ -46,6 +46,7 @@ function CheckoutContent() {
46
46
  const [destinations, setDestinations] = useState<ShippingDestinations | null>(null);
47
47
  const [pickupLocations, setPickupLocations] = useState<PickupLocation[]>([]);
48
48
  const [deliveryType, setDeliveryType] = useState<'shipping' | 'pickup'>('shipping');
49
+ const [isAllDigital, setIsAllDigital] = useState(false);
49
50
 
50
51
  // Check for returning from canceled payment
51
52
  const canceled = searchParams.get('canceled') === 'true';
@@ -73,7 +74,13 @@ function CheckoutContent() {
73
74
  setCheckout(existing);
74
75
 
75
76
  // Determine step based on checkout state
76
- if (existing.deliveryType === 'pickup' && existing.pickupLocation) {
77
+ const allDigital = existing.lineItems.every(
78
+ (i) => (i.product as unknown as { isDownloadable?: boolean }).isDownloadable
79
+ );
80
+ setIsAllDigital(allDigital);
81
+ if (allDigital) {
82
+ setStep('payment');
83
+ } else if (existing.deliveryType === 'pickup' && existing.pickupLocation) {
77
84
  setDeliveryType('pickup');
78
85
  setStep('payment');
79
86
  } else if (existing.shippingAddress && existing.shippingRateId) {
@@ -93,6 +100,16 @@ function CheckoutContent() {
93
100
  if (cart && cart.id) {
94
101
  const newCheckout = await client.createCheckout({ cartId: cart.id });
95
102
  setCheckout(newCheckout);
103
+
104
+ // If all items are downloadable, skip shipping entirely
105
+ const allDigital = newCheckout.lineItems.every(
106
+ (i) => (i.product as unknown as { isDownloadable?: boolean }).isDownloadable
107
+ );
108
+ setIsAllDigital(allDigital);
109
+ if (allDigital) {
110
+ setStep('payment');
111
+ return;
112
+ }
96
113
  } else {
97
114
  setError(t('cartIsEmpty'));
98
115
  }
@@ -265,8 +282,9 @@ function CheckoutContent() {
265
282
  );
266
283
  }
267
284
 
268
- const steps: { key: CheckoutStep; label: string }[] =
269
- pickupLocations.length > 0
285
+ const steps: { key: CheckoutStep; label: string }[] = isAllDigital
286
+ ? [{ key: 'payment', label: t('stepPayment') }]
287
+ : pickupLocations.length > 0
270
288
  ? deliveryType === 'pickup'
271
289
  ? [
272
290
  { key: 'method', label: t('stepMethod') },
@@ -461,13 +479,15 @@ function CheckoutContent() {
461
479
  <div>
462
480
  <div className="mb-4 flex items-center justify-between">
463
481
  <h2 className="text-foreground text-lg font-semibold">{t('payment')}</h2>
464
- <button
465
- type="button"
466
- onClick={() => setStep(deliveryType === 'pickup' ? 'pickup' : 'shipping')}
467
- className="text-primary text-sm hover:underline"
468
- >
469
- {deliveryType === 'pickup' ? t('changePickup') : t('changeShipping')}
470
- </button>
482
+ {!isAllDigital && (
483
+ <button
484
+ type="button"
485
+ onClick={() => setStep(deliveryType === 'pickup' ? 'pickup' : 'shipping')}
486
+ className="text-primary text-sm hover:underline"
487
+ >
488
+ {deliveryType === 'pickup' ? t('changePickup') : t('changeShipping')}
489
+ </button>
490
+ )}
471
491
  </div>
472
492
 
473
493
  <PaymentStep checkoutId={checkout.id} />
@@ -543,8 +563,8 @@ function CheckoutContent() {
543
563
  )
544
564
  )}
545
565
 
546
- {/* Coupon input — show from shipping/pickup step onwards */}
547
- {cart && (step === 'shipping' || step === 'pickup' || step === 'payment') && (
566
+ {/* Coupon input — show from shipping/pickup step onwards (or immediately if digital) */}
567
+ {cart && (isAllDigital || step === 'shipping' || step === 'pickup' || step === 'payment') && (
548
568
  <div className="border-border border-t pt-4">
549
569
  <CouponInput cart={cart} onUpdate={handleCouponUpdate} />
550
570
  </div>
@@ -201,9 +201,6 @@ export function ProductClientSection({ product: initialProduct }: ProductClientS
201
201
  productId: product.id,
202
202
  variantId: selectedVariant?.id,
203
203
  quantity,
204
- name: product.name,
205
- price: String(priceInfo.price),
206
- image: mainImageUrl || undefined,
207
204
  });
208
205
  await refreshCart();
209
206
  setAddedMessage(true);
@@ -320,7 +317,7 @@ export function ProductClientSection({ product: initialProduct }: ProductClientS
320
317
  {product.isDownloadable && product.downloads && product.downloads.length > 0 && (
321
318
  <div className="bg-muted/50 rounded-lg border p-4">
322
319
  <p className="text-foreground mb-2 text-sm font-medium">
323
- {t('filesIncluded', { count: product.downloads.length })}
320
+ {t('filesIncluded')} ({product.downloads.length})
324
321
  </p>
325
322
  <ul className="space-y-1.5">
326
323
  {product.downloads.map((file: DownloadFile) => (
@@ -69,19 +69,19 @@ function ChevronDown({ className }: { className?: string }) {
69
69
 
70
70
  /** Recursive dropdown items for nested categories */
71
71
  function CategoryDropdownItems({
72
- children,
72
+ items,
73
73
  depth,
74
74
  selectedId,
75
75
  onSelect,
76
76
  }: {
77
- children: CategoryNode[];
77
+ items: CategoryNode[];
78
78
  depth: number;
79
79
  selectedId: string;
80
80
  onSelect: (id: string) => void;
81
81
  }) {
82
82
  return (
83
83
  <>
84
- {children.map((child) => (
84
+ {items.map((child) => (
85
85
  <div key={child.id}>
86
86
  <button
87
87
  onClick={() => onSelect(child.id)}
@@ -95,7 +95,7 @@ function CategoryDropdownItems({
95
95
  </button>
96
96
  {child.children.length > 0 && (
97
97
  <CategoryDropdownItems
98
- children={child.children}
98
+ items={child.children}
99
99
  depth={depth + 1}
100
100
  selectedId={selectedId}
101
101
  onSelect={onSelect}
@@ -204,7 +204,7 @@ function CategoryChip({
204
204
  {/* Recursive children */}
205
205
  <div onClick={() => setOpen(false)}>
206
206
  <CategoryDropdownItems
207
- children={category.children}
207
+ items={category.children}
208
208
  depth={0}
209
209
  selectedId={selectedId}
210
210
  onSelect={onSelect}
@@ -77,7 +77,7 @@ function OrderCard({ order }: { order: Order }) {
77
77
  const t = useTranslations('account');
78
78
  const tc = useTranslations('common');
79
79
  const [expanded, setExpanded] = useState(false);
80
- const statusConfig = STATUS_CONFIG[order.status] || STATUS_CONFIG.pending;
80
+ const statusConfig = STATUS_CONFIG[order.status?.toLowerCase() as OrderStatus] || STATUS_CONFIG.pending;
81
81
  const currency = order.currency || 'USD';
82
82
  const totalAmount = order.totalAmount || order.total || '0';
83
83
 
@@ -243,11 +243,11 @@ function OrderDownloads({ orderId }: { orderId: string }) {
243
243
  {link.productName}
244
244
  {' · '}
245
245
  {link.downloadLimit != null
246
- ? t('downloadsRemaining', { used: link.downloadsUsed, limit: link.downloadLimit })
246
+ ? `${link.downloadsUsed}/${link.downloadLimit} ${t('downloadsRemaining')}`
247
247
  : t('unlimitedDownloads')}
248
248
  {' · '}
249
249
  {link.expiresAt
250
- ? t('expiresAt', { date: new Date(link.expiresAt).toLocaleDateString() })
250
+ ? `${t('expiresAt')} ${new Date(link.expiresAt).toLocaleDateString()}`
251
251
  : t('noExpiry')}
252
252
  </p>
253
253
  </div>
@@ -243,9 +243,7 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
243
243
  },
244
244
  };
245
245
 
246
- console.info(
247
- `Payment SDK: ${method}({ environment: "${config.environment}", version: ${config.version} })`
248
- );
246
+ console.info(`Payment SDK: calling ${method}()`);
249
247
  global[method](config);
250
248
  sdkInitDone = true;
251
249
  }
@@ -81,7 +81,7 @@ export function Header() {
81
81
  <div className="flex h-16 items-center justify-between gap-4">
82
82
  {/* Logo / Store Name */}
83
83
  <Link href="/" className="text-foreground flex-shrink-0 text-xl font-bold">
84
- {storeInfo?.name || tc('store')}
84
+ {storeInfo?.name || process.env.NEXT_PUBLIC_STORE_NAME || tc('store')}
85
85
  </Link>
86
86
 
87
87
  {/* Desktop Navigation */}
@@ -1,6 +1,5 @@
1
1
  'use client';
2
2
 
3
- import { getStockStatus } from 'brainerce';
4
3
  import type { InventoryInfo } from 'brainerce';
5
4
  import { cn } from '@/lib/utils';
6
5
 
@@ -11,24 +10,44 @@ interface StockBadgeProps {
11
10
  }
12
11
 
13
12
  export function StockBadge({ inventory, lowStockThreshold = 5, className }: StockBadgeProps) {
14
- const status = getStockStatus(inventory, { lowStockThreshold });
15
-
16
- const colorClasses =
17
- status === 'Out of Stock' || status === 'Unavailable'
18
- ? 'bg-red-100 text-red-800'
19
- : status === 'Low Stock'
20
- ? 'bg-yellow-100 text-yellow-800'
21
- : 'bg-green-100 text-green-800';
13
+ const label = getStockLabel(inventory, lowStockThreshold);
14
+ const color = getStockColor(inventory, lowStockThreshold);
22
15
 
23
16
  return (
24
17
  <span
25
18
  className={cn(
26
19
  'inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium',
27
- colorClasses,
20
+ color,
28
21
  className
29
22
  )}
30
23
  >
31
- {status}
24
+ {label}
32
25
  </span>
33
26
  );
34
27
  }
28
+
29
+ function getStockLabel(inventory: InventoryInfo | null | undefined, lowStockThreshold: number): string {
30
+ if (!inventory) return 'Out of Stock';
31
+
32
+ const { trackingMode, inStock, available } = inventory;
33
+
34
+ if (trackingMode === 'DISABLED') return 'Unavailable';
35
+ if (!inStock) return 'Out of Stock';
36
+ if (trackingMode === 'UNLIMITED') return 'In Stock';
37
+
38
+ // TRACKED — show actual quantity
39
+ if (available <= lowStockThreshold) {
40
+ return `Only ${available} left`;
41
+ }
42
+ return `${available} in stock`;
43
+ }
44
+
45
+ function getStockColor(inventory: InventoryInfo | null | undefined, lowStockThreshold: number): string {
46
+ if (!inventory) return 'bg-red-100 text-red-800';
47
+
48
+ const { trackingMode, inStock, available } = inventory;
49
+
50
+ if (trackingMode === 'DISABLED' || !inStock) return 'bg-red-100 text-red-800';
51
+ if (trackingMode === 'TRACKED' && available <= lowStockThreshold) return 'bg-yellow-100 text-yellow-800';
52
+ return 'bg-green-100 text-green-800';
53
+ }