brainerce 1.7.0 → 1.10.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.
@@ -1,842 +1,843 @@
1
- # Brainerce Store Builder
2
-
3
- Build a **{store_type}** store called "{store_name}" | Style: **{style}** | Currency: **{currency}**
4
-
5
- ---
6
-
7
- ## ⛔ STOP! Read These 3 Rules First (Breaking = Store Won't Work)
8
-
9
- ### Rule 1: Guest vs Logged-In = Different Checkout Methods!
10
-
11
- ```typescript
12
- // ❌ THIS WILL FAIL - "Cart not found" error!
13
- const cart = await client.smartGetCart(); // Guest cart has id: "__local__"
14
- await client.createCheckout({ cartId: cart.id }); // 💥 "__local__" doesn't exist on server!
15
-
16
- // ✅ CORRECT - Check user type first!
17
- if (client.isCustomerLoggedIn()) {
18
- // Logged-in user → server cart exists
19
- const checkout = await client.createCheckout({ cartId: cart.id });
20
- const checkoutId = checkout.id;
21
- } else {
22
- // Guest user → use startGuestCheckout()
23
- const result = await client.startGuestCheckout();
24
- const checkoutId = result.checkoutId;
25
- }
26
- ```
27
-
28
- | User Type | Cart Location | Checkout Method | Get Checkout ID |
29
- | ------------- | ------------- | ---------------------------- | ------------------- |
30
- | **Guest** | localStorage | `startGuestCheckout()` | `result.checkoutId` |
31
- | **Logged-in** | Server | `createCheckout({ cartId })` | `checkout.id` |
32
-
33
- ### Rule 2: Complete Checkout & Clear Cart After Payment!
34
-
35
- ```typescript
36
- // On /checkout/success page - MUST DO THIS!
37
- export default function CheckoutSuccessPage() {
38
- const checkoutId = new URLSearchParams(window.location.search).get('checkout_id');
39
-
40
- useEffect(() => {
41
- if (checkoutId) {
42
- // ⚠️ CRITICAL: This sends the order to the server AND clears the cart!
43
- // handlePaymentSuccess() only clears the local cart - it does NOT create the order!
44
- client.completeGuestCheckout(checkoutId);
45
- }
46
- }, []);
47
-
48
- return <div>Thank you for your order!</div>;
49
- }
50
- ```
51
-
52
- > **WARNING:** Do NOT use `handlePaymentSuccess()` to complete an order. It only clears
53
- > the local cart (localStorage) and does NOT communicate with the server.
54
- > Always use `completeGuestCheckout()` after payment succeeds.
55
-
56
- ### Rule 3: Never Hardcode Products!
57
-
58
- ```typescript
59
- // ❌ FORBIDDEN - Store will show fake data!
60
- const products = [{ id: '1', name: 'T-Shirt', price: 29.99 }];
61
-
62
- // ✅ CORRECT - Fetch from API
63
- const { data: products } = await client.getProducts();
64
- ```
65
-
66
- ---
67
-
68
- ## Quick Setup
69
-
70
- ```bash
71
- npm install brainerce
72
- ```
73
-
74
- ```typescript
75
- // lib/brainerce.ts
76
- import { BrainerceClient } from 'brainerce';
77
-
78
- export const client = new BrainerceClient({
79
- connectionId: '{connection_id}',
80
- baseUrl: '{api_url}',
81
- });
82
-
83
- // Restore customer session on page load
84
- export function initBrainerce() {
85
- if (typeof window === 'undefined') return;
86
- const token = localStorage.getItem('customerToken');
87
- if (token) client.setCustomerToken(token);
88
- }
89
-
90
- // Save/clear customer token
91
- export function setCustomerToken(token: string | null) {
92
- if (token) {
93
- localStorage.setItem('customerToken', token);
94
- client.setCustomerToken(token);
95
- } else {
96
- localStorage.removeItem('customerToken');
97
- client.clearCustomerToken();
98
- }
99
- }
100
- ```
101
-
102
- ---
103
-
104
- ## Cart (Works for Both Guest & Logged-in)
105
-
106
- ```typescript
107
- // Get or create cart - handles both guest (localStorage) and logged-in (server) automatically
108
- const cart = await client.smartGetCart();
109
-
110
- // Add to cart - ALWAYS pass name, price, image for guest cart display!
111
- await client.smartAddToCart({
112
- productId: product.id,
113
- variantId: selectedVariant?.id,
114
- quantity: 1,
115
- // IMPORTANT: Pass product info for guest cart display
116
- name: selectedVariant?.name ? `${product.name} - ${selectedVariant.name}` : product.name,
117
- price: getVariantPrice(selectedVariant, product.basePrice),
118
- image: selectedVariant?.image
119
- ? typeof selectedVariant.image === 'string'
120
- ? selectedVariant.image
121
- : selectedVariant.image.url
122
- : product.images?.[0]?.url,
123
- });
124
-
125
- // Update quantity (by productId, not itemId!)
126
- await client.smartUpdateCartItem('prod_xxx', 2); // productId, quantity
127
- await client.smartUpdateCartItem('prod_xxx', 3, 'var_xxx'); // with variant
128
-
129
- // Remove item (by productId, not itemId!)
130
- await client.smartRemoveFromCart('prod_xxx');
131
- await client.smartRemoveFromCart('prod_xxx', 'var_xxx'); // with variant
132
-
133
- // Get cart totals (cart doesn't have .total field!)
134
- import { getCartTotals } from 'brainerce';
135
- const totals = getCartTotals(cart);
136
- // { subtotal: 59.98, discount: 10, shipping: 0, total: 49.98 }
137
-
138
- // All smart* methods return a server Cart (even for guests via session carts)
139
- // Cart has: id, itemCount, subtotal, discountAmount, items, couponCode
140
- ```
141
-
142
- ### 🏷️ Coupon Code (Add to Cart Page!)
143
-
144
- ```typescript
145
- // Apply coupon to cart
146
- const cart = await client.smartGetCart();
147
- const updatedCart = await client.applyCoupon(cart.id, 'SAVE20');
148
- console.log(updatedCart.discountAmount); // "10.00" (string)
149
- console.log(updatedCart.couponCode); // "SAVE20"
150
-
151
- // Remove coupon
152
- const updatedCart = await client.removeCoupon(cartId);
153
-
154
- // Calculate totals including discount
155
- import { getCartTotals } from 'brainerce';
156
- const totals = getCartTotals(cart); // { subtotal, discount, shipping, total }
157
- ```
158
-
159
- **Cart page coupon UI:**
160
-
161
- ```typescript
162
- // State
163
- const [couponCode, setCouponCode] = useState('');
164
- const [couponError, setCouponError] = useState('');
165
- const [isApplying, setIsApplying] = useState(false);
166
-
167
- // Apply handler
168
- async function handleApplyCoupon() {
169
- if (!couponCode.trim() || !('id' in cart)) return;
170
- setIsApplying(true);
171
- setCouponError('');
172
- try {
173
- const updatedCart = await client.applyCoupon(cart.id, couponCode.trim());
174
- setCart(updatedCart);
175
- setCouponCode('');
176
- } catch (err: any) {
177
- setCouponError(err.message || 'Invalid coupon code');
178
- } finally {
179
- setIsApplying(false);
180
- }
181
- }
182
-
183
- // Remove handler
184
- async function handleRemoveCoupon() {
185
- if (!('id' in cart)) return;
186
- const updatedCart = await client.removeCoupon(cart.id);
187
- setCart(updatedCart);
188
- }
189
-
190
- // UI - place in cart order summary
191
- {('id' in cart) && (
192
- <div>
193
- {cart.couponCode ? (
194
- <div className="flex items-center justify-between bg-green-50 p-2 rounded">
195
- <span className="text-green-700 text-sm">🏷️ {cart.couponCode}</span>
196
- <button onClick={handleRemoveCoupon} className="text-red-500 text-sm">✕</button>
197
- </div>
198
- ) : (
199
- <div className="flex gap-2">
200
- <input value={couponCode} onChange={(e) => setCouponCode(e.target.value)}
201
- placeholder="Coupon code" className="flex-1 border rounded px-3 py-2 text-sm" />
202
- <button onClick={handleApplyCoupon} disabled={isApplying}
203
- className="px-4 py-2 bg-gray-800 text-white rounded text-sm">
204
- {isApplying ? '...' : 'Apply'}
205
- </button>
206
- </div>
207
- )}
208
- {couponError && <p className="text-red-500 text-xs mt-1">{couponError}</p>}
209
- </div>
210
- )}
211
-
212
- // Order summary - show discount line
213
- {('id' in cart) && parseFloat(cart.discountAmount) > 0 && (
214
- <div className="text-green-600">Discount: -{formatPrice(cart.discountAmount)}</div>
215
- )}
216
- ```
217
-
218
- **Checkout order summary - coupon carries over from cart:**
219
-
220
- ```typescript
221
- // Checkout already includes coupon from cart
222
- <div>Subtotal: {formatPrice(checkout.subtotal)}</div>
223
- {parseFloat(checkout.discountAmount) > 0 && (
224
- <div className="text-green-600">
225
- Discount ({checkout.couponCode}): -{formatPrice(checkout.discountAmount)}
226
- </div>
227
- )}
228
- <div>Shipping: {formatPrice(selectedRate?.price || '0')}</div>
229
- <div className="font-bold">Total: {formatPrice(checkout.total)}</div>
230
- ```
231
-
232
- ---
233
-
234
- ## 🛒 Partial Checkout (AliExpress Style) - REQUIRED!
235
-
236
- Cart page MUST have checkboxes so users can select which items to buy:
237
-
238
- ```typescript
239
- // Cart page - track selected items
240
- const [selectedIndices, setSelectedIndices] = useState<number[]>(
241
- cart.items.map((_, i) => i) // All selected by default
242
- );
243
-
244
- const toggleItem = (index: number) => {
245
- setSelectedIndices(prev =>
246
- prev.includes(index)
247
- ? prev.filter(i => i !== index)
248
- : [...prev, index]
249
- );
250
- };
251
-
252
- const toggleAll = () => {
253
- if (selectedIndices.length === cart.items.length) {
254
- setSelectedIndices([]); // Deselect all
255
- } else {
256
- setSelectedIndices(cart.items.map((_, i) => i)); // Select all
257
- }
258
- };
259
-
260
- // In your cart UI:
261
- <div>
262
- <label>
263
- <input
264
- type="checkbox"
265
- checked={selectedIndices.length === cart.items.length}
266
- onChange={toggleAll}
267
- />
268
- Select All
269
- </label>
270
- </div>
271
-
272
- {cart.items.map((item, index) => (
273
- <div key={index}>
274
- <input
275
- type="checkbox"
276
- checked={selectedIndices.includes(index)}
277
- onChange={() => toggleItem(index)}
278
- />
279
- {/* ... item details ... */}
280
- </div>
281
- ))}
282
-
283
- // On checkout button - pass selected items!
284
- const handleCheckout = async () => {
285
- if (selectedIndices.length === 0) {
286
- alert('Please select items to checkout');
287
- return;
288
- }
289
-
290
- const result = await client.startGuestCheckout({ selectedIndices });
291
- // Only selected items go to checkout, others stay in cart!
292
- };
293
- ```
294
-
295
- **Why this matters:**
296
-
297
- - Users can buy some items now, leave others for later
298
- - After payment, `completeGuestCheckout()` sends the order and only removes purchased items
299
- - Remaining items stay in cart for future purchase
300
-
301
- **⚠️ Order Summary on Checkout Page - Use checkout.lineItems!**
302
-
303
- ```typescript
304
- // ❌ WRONG - Shows ALL cart items (even unselected ones!)
305
- <div className="order-summary">
306
- {cart.items.map(item => (
307
- <div>{item.product.name} - ${item.price}</div>
308
- ))}
309
- </div>
310
-
311
- // ✅ CORRECT - Shows only items being purchased in this checkout
312
- <div className="order-summary">
313
- {checkout.lineItems.map(item => (
314
- <div>{item.product.name} - ${item.price}</div>
315
- ))}
316
- </div>
317
- ```
318
-
319
- The `checkout` object's `lineItems` array contains ONLY the items selected for this checkout!
320
-
321
- ---
322
-
323
- ## Shipping Destinations (Country/Region Dropdowns)
324
-
325
- Before showing a checkout form, fetch where the store ships to and render `<select>` dropdowns instead of free-text inputs:
326
-
327
- ```typescript
328
- import type { ShippingDestinations } from 'brainerce';
329
-
330
- // Fetch on page load (no checkout needed)
331
- const destinations: ShippingDestinations = await client.getShippingDestinations();
332
- // {
333
- // worldwide: boolean, // true if store ships everywhere
334
- // countries: [{ code: 'US', name: 'United States' }, ...],
335
- // regions: { 'US': [{ code: 'CA', name: 'California' }, ...] }
336
- // }
337
-
338
- // Country <select>
339
- <select value={country} onChange={(e) => setCountry(e.target.value)}>
340
- <option value="">Select country</option>
341
- {destinations.countries.map((c) => (
342
- <option key={c.code} value={c.code}>{c.name}</option>
343
- ))}
344
- </select>
345
-
346
- // Region <select> — only show when regions exist for selected country
347
- {destinations.regions[country]?.length > 0 ? (
348
- <select value={region} onChange={(e) => setRegion(e.target.value)}>
349
- <option value="">Select region</option>
350
- {destinations.regions[country].map((r) => (
351
- <option key={r.code} value={r.code}>{r.name}</option>
352
- ))}
353
- </select>
354
- ) : (
355
- <input type="text" value={region} onChange={(e) => setRegion(e.target.value)} />
356
- )}
357
- ```
358
-
359
- > **Note:** `regions` is an object keyed by country code. If a country has no region restrictions, it won't appear in `regions` — use a free-text input as fallback.
360
-
361
- ---
362
-
363
- ## Complete Checkout Flow
364
-
365
- ### Step 1: Start Checkout (Different for Guest vs Logged-in!)
366
-
367
- ```typescript
368
- async function startCheckout() {
369
- const cart = await client.smartGetCart();
370
-
371
- if (cart.items.length === 0) {
372
- alert('Cart is empty');
373
- return;
374
- }
375
-
376
- let checkoutId: string;
377
-
378
- if (client.isCustomerLoggedIn()) {
379
- // Logged-in: create checkout from server cart
380
- const checkout = await client.createCheckout({ cartId: cart.id });
381
- checkoutId = checkout.id;
382
- } else {
383
- // Guest: use startGuestCheckout (syncs local cart to server)
384
- const result = await client.startGuestCheckout();
385
- if (!result.tracked || !result.checkoutId) {
386
- throw new Error('Failed to create checkout');
387
- }
388
- checkoutId = result.checkoutId;
389
- }
390
-
391
- // Save for payment page
392
- localStorage.setItem('checkoutId', checkoutId);
393
-
394
- // Navigate to checkout
395
- window.location.href = '/checkout';
396
- }
397
- ```
398
-
399
- ### Step 2: Shipping Address
400
-
401
- ```typescript
402
- const checkoutId = localStorage.getItem('checkoutId')!;
403
-
404
- // Set shipping address (email is required!)
405
- const { checkout, rates } = await client.setShippingAddress(checkoutId, {
406
- email: 'customer@example.com',
407
- firstName: 'John',
408
- lastName: 'Doe',
409
- line1: '123 Main St',
410
- city: 'New York',
411
- region: 'NY', // ⚠️ Use 'region', NOT 'state'!
412
- postalCode: '10001',
413
- country: 'US',
414
- });
415
-
416
- // Show available shipping rates
417
- rates.forEach((rate) => {
418
- console.log(`${rate.name}: $${rate.price}`);
419
- });
420
- ```
421
-
422
- ### Step 3: Select Shipping Method
423
-
424
- ```typescript
425
- await client.selectShippingMethod(checkoutId, selectedRateId);
426
- ```
427
-
428
- ### Step 4: Payment (Multi-Provider)
429
-
430
- ```typescript
431
- // 1. Check if payment is configured
432
- const { hasPayments, providers } = await client.getPaymentProviders();
433
- if (!hasPayments) {
434
- return <div>Payment not configured for this store</div>;
435
- }
436
-
437
- // 2. Create payment intent — returns provider type!
438
- const intent = await client.createPaymentIntent(checkoutId, {
439
- successUrl: `${window.location.origin}/checkout/success?checkout_id=${checkoutId}`,
440
- cancelUrl: `${window.location.origin}/checkout?error=cancelled`,
441
- });
442
-
443
- // 3. Branch by provider
444
- if (intent.provider === 'grow') {
445
- // Grow: clientSecret is a payment URL — show in iframe
446
- // <iframe src={intent.clientSecret} style={{ width: '100%', minHeight: '600px', border: 'none' }} allow="payment" />
447
- // Supports credit cards, Bit, Apple Pay, Google Pay, bank transfers
448
- // Add fallback: <a href={intent.clientSecret} target="_blank">Open payment in new tab</a>
449
- // Order created automatically via webhook!
450
- } else {
451
- // Stripe: install @stripe/stripe-js @stripe/react-stripe-js
452
- import { loadStripe } from '@stripe/stripe-js';
453
- const stripeProvider = providers.find(p => p.provider === 'stripe');
454
- const stripe = await loadStripe(stripeProvider.publicKey, {
455
- stripeAccount: stripeProvider.stripeAccountId,
456
- });
457
-
458
- // Confirm payment (in your payment form)
459
- const { error } = await stripe.confirmPayment({
460
- elements,
461
- confirmParams: {
462
- return_url: `${window.location.origin}/checkout/success?checkout_id=${checkoutId}`,
463
- },
464
- });
465
-
466
- if (error) {
467
- setError(error.message);
468
- }
469
- // If no error, Stripe redirects to success page
470
- }
471
- ```
472
-
473
- ### Step 5: Success Page (Complete Order & Clear Cart!)
474
-
475
- ```typescript
476
- // /checkout/success/page.tsx
477
- 'use client';
478
- import { useEffect, useState } from 'react';
479
- import { client } from '@/lib/brainerce';
480
-
481
- export default function CheckoutSuccessPage() {
482
- const [orderNumber, setOrderNumber] = useState<string>();
483
- const [loading, setLoading] = useState(true);
484
-
485
- useEffect(() => {
486
- // Break out of iframe if redirected here from Grow payment page
487
- if (window.top !== window.self) {
488
- window.top!.location.href = window.location.href;
489
- return;
490
- }
491
-
492
- const checkoutId = new URLSearchParams(window.location.search).get('checkout_id');
493
-
494
- if (checkoutId) {
495
- // ⚠️ CRITICAL: Complete the order on the server AND clear the cart!
496
- // Do NOT use handlePaymentSuccess() - it only clears localStorage!
497
- client.completeGuestCheckout(checkoutId).then(result => {
498
- setOrderNumber(result.orderNumber);
499
- setLoading(false);
500
- }).catch(() => {
501
- // Order may already be completed (e.g., page refresh) - check status
502
- client.getPaymentStatus(checkoutId).then(status => {
503
- if (status.orderNumber) {
504
- setOrderNumber(status.orderNumber);
505
- }
506
- setLoading(false);
507
- });
508
- });
509
- }
510
- }, []);
511
-
512
- return (
513
- <div className="text-center py-12">
514
- <h1 className="text-2xl font-bold text-green-600">Thank you for your order!</h1>
515
- {loading && <p className="mt-2">Processing your order...</p>}
516
- {orderNumber && <p className="mt-2">Order #{orderNumber}</p>}
517
- <p className="mt-4">A confirmation email will be sent shortly.</p>
518
- </div>
519
- );
520
- }
521
- ```
522
-
523
- ---
524
-
525
- ## Partial Checkout (AliExpress Style)
526
-
527
- Allow customers to buy only some items from their cart:
528
-
529
- ```typescript
530
- // Start checkout with only selected items (by index)
531
- const result = await client.startGuestCheckout({
532
- selectedIndices: [0, 2], // Buy items at index 0 and 2 only
533
- });
534
-
535
- // After payment, completeGuestCheckout() sends the order AND removes only those items!
536
- // Other items stay in cart.
537
- ```
538
-
539
- ---
540
-
541
- ## Products API
542
-
543
- ```typescript
544
- // List products with pagination
545
- const { data: products, meta } = await client.getProducts({
546
- page: 1,
547
- limit: 20,
548
- search: 'blue shirt', // Searches name, description, SKU, categories, tags
549
- });
550
- // meta = { page: 1, limit: 20, total: 150, totalPages: 8 }
551
-
552
- // Get single product by slug (for product detail page)
553
- const product = await client.getProductBySlug('blue-cotton-shirt');
554
-
555
- // Search suggestions (for autocomplete)
556
- const suggestions = await client.getSearchSuggestions('blue', 5);
557
- // { products: [...], categories: [...] }
558
- ```
559
-
560
- ---
561
-
562
- ## Product Recommendations (Cross-Sells, Upsells, Related)
563
-
564
- Stores can configure product relations. Use these to boost sales with smart product suggestions:
565
-
566
- | Type | Where to Show | Purpose |
567
- |------|--------------|---------|
568
- | **Cross-sells** | Cart page | Complementary products ("You might also need") |
569
- | **Upsells** | Product page | Premium alternatives ("Upgrade your choice") |
570
- | **Related** | Bottom of product page | Similar products |
571
-
572
- ```typescript
573
- import type { ProductRecommendationsResponse, CartRecommendationsResponse } from 'brainerce';
574
-
575
- // Product page — get upsells + related products
576
- const recs: ProductRecommendationsResponse = await client.getProductRecommendations(product.id);
577
- // recs.upsells premium alternatives (show on product page)
578
- // recs.relatedsimilar products (show at bottom of product page)
579
- // recs.crossSellscomplementary products (typically used on cart page)
580
-
581
- // Each recommendation has: id, name, slug, basePrice, salePrice, images, type, inventory
582
-
583
- // Cart page — cross-sell suggestions for cart items
584
- const cart = await client.smartGetCart();
585
- const cartRecs: CartRecommendationsResponse = await client.getCartRecommendations(cart.id, 4);
586
- // cartRecs.recommendations deduplicated cross-sells (excludes items already in cart)
587
-
588
- // Render recommendation cards
589
- {recs.upsells.length > 0 && (
590
- <section>
591
- <h2>Upgrade Your Choice</h2>
592
- <div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
593
- {recs.upsells.map(item => (
594
- <a key={item.id} href={`/products/${item.slug}`}>
595
- <img src={item.images?.[0]?.url} alt={item.name} />
596
- <p>{item.name}</p>
597
- <p>{formatPrice(item.salePrice || item.basePrice)}</p>
598
- </a>
599
- ))}
600
- </div>
601
- </section>
602
- )}
603
- ```
604
-
605
- > **Note:** These methods return empty arrays when no relations are configured — the UI just renders nothing. Always build the sections.
606
-
607
- ---
608
-
609
- ## Product Custom Fields (Metafields)
610
-
611
- Products may have custom fields defined by the store owner (e.g., "Material", "Care Instructions", "Warranty").
612
-
613
- **Important:** Each metafield has a `type` field. When rendering, you **must** check `field.type` and render accordingly:
614
-
615
- | Type | Rendering |
616
- | ---------------------------------- | ------------------------------------------------------- |
617
- | `IMAGE` | `<img>` thumbnail (value is URL) |
618
- | `GALLERY` | Row of `<img>` thumbnails (value is JSON array of URLs) |
619
- | `URL` | `<a>` clickable link |
620
- | `COLOR` | Color swatch + hex value |
621
- | `BOOLEAN` | "Yes" / "No" |
622
- | `DATE` | `new Date(value).toLocaleDateString()` |
623
- | `DATETIME` | `new Date(value).toLocaleString()` |
624
- | `TEXT`, `TEXTAREA`, `NUMBER`, etc. | Plain text |
625
-
626
- ```typescript
627
- import { getProductMetafield, getProductMetafieldValue } from 'brainerce';
628
- import type { ProductMetafield } from 'brainerce';
629
-
630
- // Access metafields on a product
631
- const product = await client.getProductBySlug('blue-shirt');
632
-
633
- // ⚠️ MUST render based on type! Don't just show field.value as text for all types.
634
- function MetafieldValue({ field }: { field: ProductMetafield }) {
635
- switch (field.type) {
636
- case 'IMAGE':
637
- return field.value ? <img src={field.value} alt={field.definitionName} className="h-16 w-16 rounded object-cover" /> : <>-</>;
638
- case 'GALLERY': {
639
- let urls: string[] = [];
640
- try { urls = JSON.parse(field.value); } catch { urls = field.value ? [field.value] : []; }
641
- return <div className="flex gap-2">{urls.map((url, i) => <img key={i} src={url} className="h-16 w-16 rounded object-cover" />)}</div>;
642
- }
643
- case 'URL':
644
- return field.value ? <a href={field.value} target="_blank" rel="noopener noreferrer">{field.value}</a> : <>-</>;
645
- case 'COLOR':
646
- return <span><span className="inline-block h-4 w-4 rounded-full border" style={{ backgroundColor: field.value }} /> {field.value}</span>;
647
- case 'BOOLEAN':
648
- return <>{field.value === 'true' ? 'Yes' : 'No'}</>;
649
- case 'DATE':
650
- return <>{field.value ? new Date(field.value).toLocaleDateString() : '-'}</>;
651
- case 'DATETIME':
652
- return <>{field.value ? new Date(field.value).toLocaleString() : '-'}</>;
653
- default:
654
- return <>{field.value || '-'}</>;
655
- }
656
- }
657
-
658
- // Display in spec table
659
- {product.metafields?.map(mf => (
660
- <tr key={mf.id}>
661
- <td>{mf.definitionName}</td>
662
- <td><MetafieldValue field={mf} /></td>
663
- </tr>
664
- ))}
665
-
666
- // Get specific field by key
667
- const material = getProductMetafieldValue(product, 'material');
668
- const careInstructions = getProductMetafield(product, 'care_instructions');
669
-
670
- // Get available metafield definitions (schema)
671
- const { definitions } = await client.getPublicMetafieldDefinitions();
672
- // Use definitions to build dynamic UI (filters, forms, etc.)
673
- ```
674
-
675
- > **Tip:** `metafields` may be empty if the store hasn't defined custom fields. Always use optional chaining.
676
-
677
- ---
678
-
679
- ## Customer Authentication
680
-
681
- ```typescript
682
- // Register
683
- const auth = await client.registerCustomer({
684
- email: 'john@example.com',
685
- password: 'securepass123',
686
- firstName: 'John',
687
- lastName: 'Doe',
688
- });
689
-
690
- if (auth.requiresVerification) {
691
- // Store token for verification step
692
- localStorage.setItem('verificationToken', auth.token);
693
- localStorage.setItem('verificationEmail', 'john@example.com');
694
- window.location.href = '/verify-email';
695
- } else {
696
- client.setCustomerToken(auth.token);
697
- localStorage.setItem('customerToken', auth.token);
698
- }
699
-
700
- // Login
701
- const auth = await client.loginCustomer('john@example.com', 'password');
702
-
703
- if (auth.requiresVerification) {
704
- localStorage.setItem('verificationToken', auth.token);
705
- localStorage.setItem('verificationEmail', 'john@example.com');
706
- window.location.href = '/verify-email';
707
- } else {
708
- client.setCustomerToken(auth.token);
709
- localStorage.setItem('customerToken', auth.token);
710
- }
711
-
712
- // Verify email (on /verify-email page)
713
- const result = await client.verifyEmail(code, token);
714
- if (result.verified) {
715
- client.setCustomerToken(token);
716
- localStorage.setItem('customerToken', token);
717
- localStorage.removeItem('verificationToken');
718
- localStorage.removeItem('verificationEmail');
719
- window.location.href = '/account';
720
- }
721
-
722
- // Resend verification code
723
- await client.resendVerificationEmail(token);
724
-
725
- // Forgot password (on /forgot-password page)
726
- await client.forgotPassword(email);
727
- // Always succeeds (prevents email enumeration)
728
-
729
- // Reset password (on /reset-password page — user arrives via email link)
730
- const token = new URLSearchParams(window.location.search).get('token');
731
- await client.resetPassword(token!, newPassword);
732
- // On success: redirect to /login
733
-
734
- // Logout
735
- client.setCustomerToken(null);
736
- localStorage.removeItem('customerToken');
737
-
738
- // Get profile (requires token)
739
- const profile = await client.getMyProfile();
740
-
741
- // Get order history
742
- const { data: orders, meta } = await client.getMyOrders({ page: 1, limit: 10 });
743
- ```
744
-
745
- ---
746
-
747
- ## OAuth / Social Login
748
-
749
- ```typescript
750
- // Get available providers for this store
751
- const { providers } = await client.getAvailableOAuthProviders();
752
- // providers = ['GOOGLE', 'FACEBOOK', 'GITHUB']
753
-
754
- // Redirect to OAuth provider
755
- const { authorizationUrl } = await client.getOAuthAuthorizeUrl('GOOGLE', {
756
- redirectUrl: `${window.location.origin}/auth/callback`,
757
- });
758
- window.location.href = authorizationUrl;
759
-
760
- // Handle callback (on /auth/callback page — backend redirects here with params)
761
- const params = new URLSearchParams(window.location.search);
762
- if (params.get('oauth_success') === 'true') {
763
- const token = params.get('token');
764
- client.setCustomerToken(token!);
765
- // Also available: customer_id, customer_email, is_new
766
- } else if (params.get('oauth_error')) {
767
- // Show error to user
768
- }
769
- ```
770
-
771
- ---
772
-
773
- ## Required Pages Checklist
774
-
775
- - [ ] **Home** (`/`) - Featured products grid
776
- - [ ] **Products** (`/products`) - Product list with infinite scroll
777
- - [ ] **Product Detail** (`/products/[slug]`) - Use `getProductBySlug(slug)`. Show **upsells** + **related products** via `getProductRecommendations()`
778
- - [ ] **Cart** (`/cart`) - Show items, quantities, totals, **coupon code input**, discount display, **cross-sell recommendations** via `getCartRecommendations()`
779
- - [ ] **Checkout** (`/checkout`) - Address Shipping Payment. **Show discount in order summary!**
780
- - [ ] **Success** (`/checkout/success`) - **Must call `completeGuestCheckout()`!**
781
- - [ ] **Login** (`/login`) - Email/password + social buttons, handle `requiresVerification`
782
- - [ ] **Register** (`/register`) - Registration form, handle `requiresVerification`
783
- - [ ] **Verify Email** (`/verify-email`) - 6-digit code input + resend button. **ALWAYS create this page** even if verification is currently disabled — the store owner can enable it at any time
784
- - [ ] **Forgot Password** (`/forgot-password`) - Email input, shows success message
785
- - [ ] **Reset Password** (`/reset-password`) - Token from URL + new password form
786
- - [ ] **OAuth Callback** (`/auth/callback`) - Handle OAuth redirect with token from URL params
787
- - [ ] **Account** (`/account`) - Profile + order history
788
- - [ ] **Header** - Logo, nav, cart icon with count, search
789
-
790
- ### ALWAYS Build These (Even If Currently Disabled)
791
-
792
- Some features may not be configured yet, but the store owner can enable them at any time. **Always create the UI** — SDK methods return empty/null when not configured:
793
-
794
- - **Email Verification** → `/verify-email` page. `requiresVerification` is checked in login/register flows.
795
- - **OAuth Buttons** → Social login buttons on Login & Register + `/auth/callback` page. `getAvailableOAuthProviders()` returns `[]` when none configured — buttons just don't render.
796
- - **Password Reset** → `/forgot-password` + `/reset-password` pages. `forgotPassword()` silently succeeds when no account exists.
797
- - **Discount Banners** → `getDiscountBanners()` returns `[]` when no rules — component renders nothing.
798
- - **Product Discount Badges** → `getProductDiscountBadge(id)` returns `null` — renders nothing.
799
- - **Cart Nudges** → `cart.nudges` is `[]` — renders nothing.
800
- - **Coupon Input** → Always show in cart. Works even with no coupons configured.
801
- - **Product Recommendations** → Upsells + related on product page, cross-sells on cart page. `getProductRecommendations()` / `getCartRecommendations()` return empty arrays when none configured.
802
-
803
- ---
804
-
805
- ## Common Type Gotchas
806
-
807
- ```typescript
808
- // ❌ WRONG // ✅ CORRECT
809
- address.state address.region
810
- cart.total getCartTotals(cart).total
811
- cart.discount cart.discountAmount
812
- item.name (in cart) item.product.name
813
- response.url (OAuth) response.authorizationUrl
814
- providers.forEach (OAuth) response.providers.forEach
815
- status === 'completed' status === 'succeeded'
816
- product.metafields.name product.metafields[0].definitionName
817
- product.metafields.key product.metafields[0].definitionKey
818
- orderItem.unitPrice orderItem.price (OrderItem is FLAT, not nested!)
819
- cartItem.price cartItem.unitPrice (Cart/Checkout items use unitPrice)
820
- waitResult.orderNumber waitResult.status.orderNumber (nested in PaymentStatus)
821
- variant.attributes.map(...) Object.entries(variant.attributes || {}) (it's an object!)
822
- categorySuggestion.slug // doesn't exist! Only: id, name, productCount
823
- order.status === 'COMPLETED' order.status === 'delivered' (OrderStatus is lowercase!)
824
- getCartTotals(cart) // Works all carts are server carts now
825
- result.checkoutId (guest checkout) // ⚠️ Check result.tracked first! It's a union type
826
- ```
827
-
828
- **Key distinctions:**
829
-
830
- - **OrderItem** (from orders): Flat structure — `item.price`, `item.name`, `item.image`
831
- - **CartItem / CheckoutLineItem**: Nested structure — `item.unitPrice`, `item.product.name`, `item.product.images`
832
- - **`getCartTotals()`** works on all cartsguests now use server-side session carts with full `subtotal`/`discountAmount` fields.
833
- - **`GuestCheckoutStartResponse`** is a union typealways check `result.tracked` before accessing `result.checkoutId`
834
- - **`WaitForOrderResult`** has `result.status.orderNumber`, NOT `result.orderNumber`. But `completeGuestCheckout()` returns `GuestOrderResponse` which DOES have `result.orderNumber` directly.
835
- - **Cart state**: Use `useState<Cart | null>(null)` and load with `smartGetCart()` in `useEffect` all carts are server-side now, no hydration mismatch issues.
836
-
837
- ---
838
-
839
- ## Full SDK Documentation
840
-
841
- For complete API reference and working code examples:
842
- **https://brainerce.com/docs/sdk**
1
+ # Brainerce Store Builder
2
+
3
+ Build a **{store_type}** store called "{store_name}" | Style: **{style}** | Currency: **{currency}**
4
+
5
+ ---
6
+
7
+ ## ⛔ STOP! Read These 3 Rules First (Breaking = Store Won't Work)
8
+
9
+ ### Rule 1: Guest vs Logged-In = Different Checkout Methods!
10
+
11
+ ```typescript
12
+ // ❌ THIS WILL FAIL - "Cart not found" error!
13
+ const cart = await client.smartGetCart(); // Guest cart has id: "__local__"
14
+ await client.createCheckout({ cartId: cart.id }); // 💥 "__local__" doesn't exist on server!
15
+
16
+ // ✅ CORRECT - Check user type first!
17
+ if (client.isCustomerLoggedIn()) {
18
+ // Logged-in user → server cart exists
19
+ const checkout = await client.createCheckout({ cartId: cart.id });
20
+ const checkoutId = checkout.id;
21
+ } else {
22
+ // Guest user → use startGuestCheckout()
23
+ const result = await client.startGuestCheckout();
24
+ const checkoutId = result.checkoutId;
25
+ }
26
+ ```
27
+
28
+ | User Type | Cart Location | Checkout Method | Get Checkout ID |
29
+ | ------------- | ------------- | ---------------------------- | ------------------- |
30
+ | **Guest** | localStorage | `startGuestCheckout()` | `result.checkoutId` |
31
+ | **Logged-in** | Server | `createCheckout({ cartId })` | `checkout.id` |
32
+
33
+ ### Rule 2: Complete Checkout & Clear Cart After Payment!
34
+
35
+ ```typescript
36
+ // On /checkout/success page - MUST DO THIS!
37
+ export default function CheckoutSuccessPage() {
38
+ const checkoutId = new URLSearchParams(window.location.search).get('checkout_id');
39
+
40
+ useEffect(() => {
41
+ if (checkoutId) {
42
+ // ⚠️ CRITICAL: This sends the order to the server AND clears the cart!
43
+ // handlePaymentSuccess() only clears the local cart - it does NOT create the order!
44
+ client.completeGuestCheckout(checkoutId);
45
+ }
46
+ }, []);
47
+
48
+ return <div>Thank you for your order!</div>;
49
+ }
50
+ ```
51
+
52
+ > **WARNING:** Do NOT use `handlePaymentSuccess()` to complete an order. It only clears
53
+ > the local cart (localStorage) and does NOT communicate with the server.
54
+ > Always use `completeGuestCheckout()` after payment succeeds.
55
+
56
+ ### Rule 3: Never Hardcode Products!
57
+
58
+ ```typescript
59
+ // ❌ FORBIDDEN - Store will show fake data!
60
+ const products = [{ id: '1', name: 'T-Shirt', price: 29.99 }];
61
+
62
+ // ✅ CORRECT - Fetch from API
63
+ const { data: products } = await client.getProducts();
64
+ ```
65
+
66
+ ---
67
+
68
+ ## Quick Setup
69
+
70
+ ```bash
71
+ npm install brainerce
72
+ ```
73
+
74
+ ```typescript
75
+ // lib/brainerce.ts
76
+ import { BrainerceClient } from 'brainerce';
77
+
78
+ export const client = new BrainerceClient({
79
+ connectionId: '{connection_id}',
80
+ baseUrl: '{api_url}',
81
+ });
82
+
83
+ // Restore customer session on page load
84
+ export function initBrainerce() {
85
+ if (typeof window === 'undefined') return;
86
+ const token = localStorage.getItem('customerToken');
87
+ if (token) client.setCustomerToken(token);
88
+ }
89
+
90
+ // Save/clear customer token
91
+ export function setCustomerToken(token: string | null) {
92
+ if (token) {
93
+ localStorage.setItem('customerToken', token);
94
+ client.setCustomerToken(token);
95
+ } else {
96
+ localStorage.removeItem('customerToken');
97
+ client.clearCustomerToken();
98
+ }
99
+ }
100
+ ```
101
+
102
+ ---
103
+
104
+ ## Cart (Works for Both Guest & Logged-in)
105
+
106
+ ```typescript
107
+ // Get or create cart - handles both guest (localStorage) and logged-in (server) automatically
108
+ const cart = await client.smartGetCart();
109
+
110
+ // Add to cart - ALWAYS pass name, price, image for guest cart display!
111
+ await client.smartAddToCart({
112
+ productId: product.id,
113
+ variantId: selectedVariant?.id,
114
+ quantity: 1,
115
+ // IMPORTANT: Pass product info for guest cart display
116
+ name: selectedVariant?.name ? `${product.name} - ${selectedVariant.name}` : product.name,
117
+ price: getVariantPrice(selectedVariant, product.basePrice),
118
+ image: selectedVariant?.image
119
+ ? typeof selectedVariant.image === 'string'
120
+ ? selectedVariant.image
121
+ : selectedVariant.image.url
122
+ : product.images?.[0]?.url,
123
+ });
124
+
125
+ // Update quantity (by productId, not itemId!)
126
+ await client.smartUpdateCartItem('prod_xxx', 2); // productId, quantity
127
+ await client.smartUpdateCartItem('prod_xxx', 3, 'var_xxx'); // with variant
128
+
129
+ // Remove item (by productId, not itemId!)
130
+ await client.smartRemoveFromCart('prod_xxx');
131
+ await client.smartRemoveFromCart('prod_xxx', 'var_xxx'); // with variant
132
+
133
+ // Get cart totals (cart doesn't have .total field!)
134
+ import { getCartTotals } from 'brainerce';
135
+ const totals = getCartTotals(cart);
136
+ // { subtotal: 59.98, discount: 10, shipping: 0, total: 49.98 }
137
+
138
+ // All smart* methods return a server Cart (even for guests via session carts)
139
+ // Cart has: id, itemCount, subtotal, discountAmount, items, couponCode
140
+ ```
141
+
142
+ ### 🏷️ Coupon Code (Add to Cart Page!)
143
+
144
+ ```typescript
145
+ // Apply coupon to cart
146
+ const cart = await client.smartGetCart();
147
+ const updatedCart = await client.applyCoupon(cart.id, 'SAVE20');
148
+ console.log(updatedCart.discountAmount); // "10.00" (string)
149
+ console.log(updatedCart.couponCode); // "SAVE20"
150
+
151
+ // Remove coupon
152
+ const updatedCart = await client.removeCoupon(cartId);
153
+
154
+ // Calculate totals including discount
155
+ import { getCartTotals } from 'brainerce';
156
+ const totals = getCartTotals(cart); // { subtotal, discount, shipping, total }
157
+ ```
158
+
159
+ **Cart page coupon UI:**
160
+
161
+ ```typescript
162
+ // State
163
+ const [couponCode, setCouponCode] = useState('');
164
+ const [couponError, setCouponError] = useState('');
165
+ const [isApplying, setIsApplying] = useState(false);
166
+
167
+ // Apply handler
168
+ async function handleApplyCoupon() {
169
+ if (!couponCode.trim() || !('id' in cart)) return;
170
+ setIsApplying(true);
171
+ setCouponError('');
172
+ try {
173
+ const updatedCart = await client.applyCoupon(cart.id, couponCode.trim());
174
+ setCart(updatedCart);
175
+ setCouponCode('');
176
+ } catch (err: any) {
177
+ setCouponError(err.message || 'Invalid coupon code');
178
+ } finally {
179
+ setIsApplying(false);
180
+ }
181
+ }
182
+
183
+ // Remove handler
184
+ async function handleRemoveCoupon() {
185
+ if (!('id' in cart)) return;
186
+ const updatedCart = await client.removeCoupon(cart.id);
187
+ setCart(updatedCart);
188
+ }
189
+
190
+ // UI - place in cart order summary
191
+ {('id' in cart) && (
192
+ <div>
193
+ {cart.couponCode ? (
194
+ <div className="flex items-center justify-between bg-green-50 p-2 rounded">
195
+ <span className="text-green-700 text-sm">🏷️ {cart.couponCode}</span>
196
+ <button onClick={handleRemoveCoupon} className="text-red-500 text-sm">✕</button>
197
+ </div>
198
+ ) : (
199
+ <div className="flex gap-2">
200
+ <input value={couponCode} onChange={(e) => setCouponCode(e.target.value)}
201
+ placeholder="Coupon code" className="flex-1 border rounded px-3 py-2 text-sm" />
202
+ <button onClick={handleApplyCoupon} disabled={isApplying}
203
+ className="px-4 py-2 bg-gray-800 text-white rounded text-sm">
204
+ {isApplying ? '...' : 'Apply'}
205
+ </button>
206
+ </div>
207
+ )}
208
+ {couponError && <p className="text-red-500 text-xs mt-1">{couponError}</p>}
209
+ </div>
210
+ )}
211
+
212
+ // Order summary - show discount line
213
+ {('id' in cart) && parseFloat(cart.discountAmount) > 0 && (
214
+ <div className="text-green-600">Discount: -{formatPrice(cart.discountAmount)}</div>
215
+ )}
216
+ ```
217
+
218
+ **Checkout order summary - coupon carries over from cart:**
219
+
220
+ ```typescript
221
+ // Checkout already includes coupon from cart
222
+ <div>Subtotal: {formatPrice(checkout.subtotal)}</div>
223
+ {parseFloat(checkout.discountAmount) > 0 && (
224
+ <div className="text-green-600">
225
+ Discount ({checkout.couponCode}): -{formatPrice(checkout.discountAmount)}
226
+ </div>
227
+ )}
228
+ <div>Shipping: {formatPrice(selectedRate?.price || '0')}</div>
229
+ <div className="font-bold">Total: {formatPrice(checkout.total)}</div>
230
+ ```
231
+
232
+ ---
233
+
234
+ ## 🛒 Partial Checkout (AliExpress Style) - REQUIRED!
235
+
236
+ Cart page MUST have checkboxes so users can select which items to buy:
237
+
238
+ ```typescript
239
+ // Cart page - track selected items
240
+ const [selectedIndices, setSelectedIndices] = useState<number[]>(
241
+ cart.items.map((_, i) => i) // All selected by default
242
+ );
243
+
244
+ const toggleItem = (index: number) => {
245
+ setSelectedIndices(prev =>
246
+ prev.includes(index)
247
+ ? prev.filter(i => i !== index)
248
+ : [...prev, index]
249
+ );
250
+ };
251
+
252
+ const toggleAll = () => {
253
+ if (selectedIndices.length === cart.items.length) {
254
+ setSelectedIndices([]); // Deselect all
255
+ } else {
256
+ setSelectedIndices(cart.items.map((_, i) => i)); // Select all
257
+ }
258
+ };
259
+
260
+ // In your cart UI:
261
+ <div>
262
+ <label>
263
+ <input
264
+ type="checkbox"
265
+ checked={selectedIndices.length === cart.items.length}
266
+ onChange={toggleAll}
267
+ />
268
+ Select All
269
+ </label>
270
+ </div>
271
+
272
+ {cart.items.map((item, index) => (
273
+ <div key={index}>
274
+ <input
275
+ type="checkbox"
276
+ checked={selectedIndices.includes(index)}
277
+ onChange={() => toggleItem(index)}
278
+ />
279
+ {/* ... item details ... */}
280
+ </div>
281
+ ))}
282
+
283
+ // On checkout button - pass selected items!
284
+ const handleCheckout = async () => {
285
+ if (selectedIndices.length === 0) {
286
+ alert('Please select items to checkout');
287
+ return;
288
+ }
289
+
290
+ const result = await client.startGuestCheckout({ selectedIndices });
291
+ // Only selected items go to checkout, others stay in cart!
292
+ };
293
+ ```
294
+
295
+ **Why this matters:**
296
+
297
+ - Users can buy some items now, leave others for later
298
+ - After payment, `completeGuestCheckout()` sends the order and only removes purchased items
299
+ - Remaining items stay in cart for future purchase
300
+
301
+ **⚠️ Order Summary on Checkout Page - Use checkout.lineItems!**
302
+
303
+ ```typescript
304
+ // ❌ WRONG - Shows ALL cart items (even unselected ones!)
305
+ <div className="order-summary">
306
+ {cart.items.map(item => (
307
+ <div>{item.product.name} - ${item.price}</div>
308
+ ))}
309
+ </div>
310
+
311
+ // ✅ CORRECT - Shows only items being purchased in this checkout
312
+ <div className="order-summary">
313
+ {checkout.lineItems.map(item => (
314
+ <div>{item.product.name} - ${item.price}</div>
315
+ ))}
316
+ </div>
317
+ ```
318
+
319
+ The `checkout` object's `lineItems` array contains ONLY the items selected for this checkout!
320
+
321
+ ---
322
+
323
+ ## Shipping Destinations (Country/Region Dropdowns)
324
+
325
+ Before showing a checkout form, fetch where the store ships to and render `<select>` dropdowns instead of free-text inputs:
326
+
327
+ ```typescript
328
+ import type { ShippingDestinations } from 'brainerce';
329
+
330
+ // Fetch on page load (no checkout needed)
331
+ const destinations: ShippingDestinations = await client.getShippingDestinations();
332
+ // {
333
+ // worldwide: boolean, // true if store ships everywhere
334
+ // countries: [{ code: 'US', name: 'United States' }, ...],
335
+ // regions: { 'US': [{ code: 'CA', name: 'California' }, ...] }
336
+ // }
337
+
338
+ // Country <select>
339
+ <select value={country} onChange={(e) => setCountry(e.target.value)}>
340
+ <option value="">Select country</option>
341
+ {destinations.countries.map((c) => (
342
+ <option key={c.code} value={c.code}>{c.name}</option>
343
+ ))}
344
+ </select>
345
+
346
+ // Region <select> — only show when regions exist for selected country
347
+ {destinations.regions[country]?.length > 0 ? (
348
+ <select value={region} onChange={(e) => setRegion(e.target.value)}>
349
+ <option value="">Select region</option>
350
+ {destinations.regions[country].map((r) => (
351
+ <option key={r.code} value={r.code}>{r.name}</option>
352
+ ))}
353
+ </select>
354
+ ) : (
355
+ <input type="text" value={region} onChange={(e) => setRegion(e.target.value)} />
356
+ )}
357
+ ```
358
+
359
+ > **Note:** `regions` is an object keyed by country code. If a country has no region restrictions, it won't appear in `regions` — use a free-text input as fallback.
360
+
361
+ ---
362
+
363
+ ## Complete Checkout Flow
364
+
365
+ ### Step 1: Start Checkout (Different for Guest vs Logged-in!)
366
+
367
+ ```typescript
368
+ async function startCheckout() {
369
+ const cart = await client.smartGetCart();
370
+
371
+ if (cart.items.length === 0) {
372
+ alert('Cart is empty');
373
+ return;
374
+ }
375
+
376
+ let checkoutId: string;
377
+
378
+ if (client.isCustomerLoggedIn()) {
379
+ // Logged-in: create checkout from server cart
380
+ const checkout = await client.createCheckout({ cartId: cart.id });
381
+ checkoutId = checkout.id;
382
+ } else {
383
+ // Guest: use startGuestCheckout (syncs local cart to server)
384
+ const result = await client.startGuestCheckout();
385
+ if (!result.tracked || !result.checkoutId) {
386
+ throw new Error('Failed to create checkout');
387
+ }
388
+ checkoutId = result.checkoutId;
389
+ }
390
+
391
+ // Save for payment page
392
+ localStorage.setItem('checkoutId', checkoutId);
393
+
394
+ // Navigate to checkout
395
+ window.location.href = '/checkout';
396
+ }
397
+ ```
398
+
399
+ ### Step 2: Shipping Address
400
+
401
+ ```typescript
402
+ const checkoutId = localStorage.getItem('checkoutId')!;
403
+
404
+ // Set shipping address (email is required!)
405
+ const { checkout, rates } = await client.setShippingAddress(checkoutId, {
406
+ email: 'customer@example.com',
407
+ firstName: 'John',
408
+ lastName: 'Doe',
409
+ line1: '123 Main St',
410
+ city: 'New York',
411
+ region: 'NY', // ⚠️ Use 'region', NOT 'state'!
412
+ postalCode: '10001',
413
+ country: 'US',
414
+ });
415
+
416
+ // Show available shipping rates
417
+ rates.forEach((rate) => {
418
+ console.log(`${rate.name}: $${rate.price}`);
419
+ });
420
+ ```
421
+
422
+ ### Step 3: Select Shipping Method
423
+
424
+ ```typescript
425
+ await client.selectShippingMethod(checkoutId, selectedRateId);
426
+ ```
427
+
428
+ ### Step 4: Payment (Multi-Provider)
429
+
430
+ ```typescript
431
+ // 1. Check if payment is configured
432
+ const { hasPayments, providers } = await client.getPaymentProviders();
433
+ if (!hasPayments) {
434
+ return <div>Payment not configured for this store</div>;
435
+ }
436
+
437
+ // 2. Create payment intent — returns provider type!
438
+ const intent = await client.createPaymentIntent(checkoutId, {
439
+ successUrl: `${window.location.origin}/checkout/success?checkout_id=${checkoutId}`,
440
+ cancelUrl: `${window.location.origin}/checkout?error=cancelled`,
441
+ });
442
+
443
+ // 3. Branch by provider
444
+ if (intent.provider === 'grow') {
445
+ // Grow: clientSecret is a payment URL — show in iframe
446
+ // <iframe src={intent.clientSecret} style={{ width: '100%', minHeight: '600px', border: 'none' }} allow="payment" />
447
+ // Supports credit cards, Bit, Apple Pay, Google Pay, bank transfers
448
+ // Add fallback: <a href={intent.clientSecret} target="_blank">Open payment in new tab</a>
449
+ // Order created automatically via webhook!
450
+ } else {
451
+ // Stripe: install @stripe/stripe-js @stripe/react-stripe-js
452
+ import { loadStripe } from '@stripe/stripe-js';
453
+ const stripeProvider = providers.find(p => p.provider === 'stripe');
454
+ const stripe = await loadStripe(stripeProvider.publicKey, {
455
+ stripeAccount: stripeProvider.stripeAccountId,
456
+ });
457
+
458
+ // Confirm payment (in your payment form)
459
+ const { error } = await stripe.confirmPayment({
460
+ elements,
461
+ confirmParams: {
462
+ return_url: `${window.location.origin}/checkout/success?checkout_id=${checkoutId}`,
463
+ },
464
+ });
465
+
466
+ if (error) {
467
+ setError(error.message);
468
+ }
469
+ // If no error, Stripe redirects to success page
470
+ }
471
+ ```
472
+
473
+ ### Step 5: Success Page (Complete Order & Clear Cart!)
474
+
475
+ ```typescript
476
+ // /checkout/success/page.tsx
477
+ 'use client';
478
+ import { useEffect, useState } from 'react';
479
+ import { client } from '@/lib/brainerce';
480
+
481
+ export default function CheckoutSuccessPage() {
482
+ const [orderNumber, setOrderNumber] = useState<string>();
483
+ const [loading, setLoading] = useState(true);
484
+
485
+ useEffect(() => {
486
+ // Break out of iframe if redirected here from Grow payment page
487
+ if (window.top !== window.self) {
488
+ window.top!.location.href = window.location.href;
489
+ return;
490
+ }
491
+
492
+ const checkoutId = new URLSearchParams(window.location.search).get('checkout_id');
493
+
494
+ if (checkoutId) {
495
+ // ⚠️ CRITICAL: Complete the order on the server AND clear the cart!
496
+ // Do NOT use handlePaymentSuccess() - it only clears localStorage!
497
+ client.completeGuestCheckout(checkoutId).then(result => {
498
+ setOrderNumber(result.orderNumber);
499
+ setLoading(false);
500
+ }).catch(() => {
501
+ // Order may already be completed (e.g., page refresh) - check status
502
+ client.getPaymentStatus(checkoutId).then(status => {
503
+ if (status.orderNumber) {
504
+ setOrderNumber(status.orderNumber);
505
+ }
506
+ setLoading(false);
507
+ });
508
+ });
509
+ }
510
+ }, []);
511
+
512
+ return (
513
+ <div className="text-center py-12">
514
+ <h1 className="text-2xl font-bold text-green-600">Thank you for your order!</h1>
515
+ {loading && <p className="mt-2">Processing your order...</p>}
516
+ {orderNumber && <p className="mt-2">Order #{orderNumber}</p>}
517
+ <p className="mt-4">A confirmation email will be sent shortly.</p>
518
+ </div>
519
+ );
520
+ }
521
+ ```
522
+
523
+ ---
524
+
525
+ ## Partial Checkout (AliExpress Style)
526
+
527
+ Allow customers to buy only some items from their cart:
528
+
529
+ ```typescript
530
+ // Start checkout with only selected items (by index)
531
+ const result = await client.startGuestCheckout({
532
+ selectedIndices: [0, 2], // Buy items at index 0 and 2 only
533
+ });
534
+
535
+ // After payment, completeGuestCheckout() sends the order AND removes only those items!
536
+ // Other items stay in cart.
537
+ ```
538
+
539
+ ---
540
+
541
+ ## Products API
542
+
543
+ ```typescript
544
+ // List products with pagination
545
+ const { data: products, meta } = await client.getProducts({
546
+ page: 1,
547
+ limit: 20,
548
+ search: 'blue shirt', // Searches name, description, SKU, categories, tags
549
+ });
550
+ // meta = { page: 1, limit: 20, total: 150, totalPages: 8 }
551
+
552
+ // Get single product by slug (for product detail page)
553
+ const product = await client.getProductBySlug('blue-cotton-shirt');
554
+
555
+ // Search suggestions (for autocomplete)
556
+ const suggestions = await client.getSearchSuggestions('blue', 5);
557
+ // { products: [...], categories: [...] }
558
+ ```
559
+
560
+ ---
561
+
562
+ ## Product Recommendations (Cross-Sells, Upsells, Related)
563
+
564
+ Stores can configure product relations. Use these to boost sales with smart product suggestions:
565
+
566
+ | Type | Where to Show | Purpose |
567
+ | --------------- | ---------------------- | ---------------------------------------------- |
568
+ | **Cross-sells** | Cart page | Complementary products ("You might also need") |
569
+ | **Upsells** | Product page | Premium alternatives ("Upgrade your choice") |
570
+ | **Related** | Bottom of product page | Similar products |
571
+
572
+ ```typescript
573
+ import type { ProductRecommendationsResponse, CartRecommendationsResponse } from 'brainerce';
574
+
575
+ // Product page — recommendations come EMBEDDED in the product response (no extra API call needed)
576
+ const product = await client.getProductBySlug('some-slug');
577
+ const recs = (product as any).recommendations as ProductRecommendationsResponse | undefined;
578
+ // recs?.upsellspremium alternatives (show on product page)
579
+ // recs?.relatedsimilar products (show at bottom of product page)
580
+ // recs?.crossSells — complementary products (typically used on cart page)
581
+
582
+ // Each recommendation has: id, name, slug, basePrice, salePrice, images[], type, inventory
583
+
584
+ // Cart page — cross-sell suggestions for cart items (separate call, since cart has no embedded recs)
585
+ const cart = await client.smartGetCart();
586
+ const cartRecs: CartRecommendationsResponse = await client.getCartRecommendations(cart.id, 4);
587
+ // cartRecs.recommendations — deduplicated cross-sells (excludes items already in cart)
588
+
589
+ // Render recommendation cards on product page
590
+ {recs?.upsells && recs.upsells.length > 0 && (
591
+ <section>
592
+ <h2>Upgrade Your Choice</h2>
593
+ <div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
594
+ {recs.upsells.map(item => (
595
+ <a key={item.id} href={`/products/${item.slug}`}>
596
+ <img src={item.images?.[0]?.url} alt={item.name} />
597
+ <p>{item.name}</p>
598
+ <p>{formatPrice(item.salePrice || item.basePrice)}</p>
599
+ </a>
600
+ ))}
601
+ </div>
602
+ </section>
603
+ )}
604
+ ```
605
+
606
+ > **Note:** Recommendations return empty arrays when no relations are configured — the UI just renders nothing. Always build the sections. The `getProductRecommendations()` SDK method still works as a fallback for older storefronts.
607
+
608
+ ---
609
+
610
+ ## Product Custom Fields (Metafields)
611
+
612
+ Products may have custom fields defined by the store owner (e.g., "Material", "Care Instructions", "Warranty").
613
+
614
+ **Important:** Each metafield has a `type` field. When rendering, you **must** check `field.type` and render accordingly:
615
+
616
+ | Type | Rendering |
617
+ | ---------------------------------- | ------------------------------------------------------- |
618
+ | `IMAGE` | `<img>` thumbnail (value is URL) |
619
+ | `GALLERY` | Row of `<img>` thumbnails (value is JSON array of URLs) |
620
+ | `URL` | `<a>` clickable link |
621
+ | `COLOR` | Color swatch + hex value |
622
+ | `BOOLEAN` | "Yes" / "No" |
623
+ | `DATE` | `new Date(value).toLocaleDateString()` |
624
+ | `DATETIME` | `new Date(value).toLocaleString()` |
625
+ | `TEXT`, `TEXTAREA`, `NUMBER`, etc. | Plain text |
626
+
627
+ ```typescript
628
+ import { getProductMetafield, getProductMetafieldValue } from 'brainerce';
629
+ import type { ProductMetafield } from 'brainerce';
630
+
631
+ // Access metafields on a product
632
+ const product = await client.getProductBySlug('blue-shirt');
633
+
634
+ // ⚠️ MUST render based on type! Don't just show field.value as text for all types.
635
+ function MetafieldValue({ field }: { field: ProductMetafield }) {
636
+ switch (field.type) {
637
+ case 'IMAGE':
638
+ return field.value ? <img src={field.value} alt={field.definitionName} className="h-16 w-16 rounded object-cover" /> : <>-</>;
639
+ case 'GALLERY': {
640
+ let urls: string[] = [];
641
+ try { urls = JSON.parse(field.value); } catch { urls = field.value ? [field.value] : []; }
642
+ return <div className="flex gap-2">{urls.map((url, i) => <img key={i} src={url} className="h-16 w-16 rounded object-cover" />)}</div>;
643
+ }
644
+ case 'URL':
645
+ return field.value ? <a href={field.value} target="_blank" rel="noopener noreferrer">{field.value}</a> : <>-</>;
646
+ case 'COLOR':
647
+ return <span><span className="inline-block h-4 w-4 rounded-full border" style={{ backgroundColor: field.value }} /> {field.value}</span>;
648
+ case 'BOOLEAN':
649
+ return <>{field.value === 'true' ? 'Yes' : 'No'}</>;
650
+ case 'DATE':
651
+ return <>{field.value ? new Date(field.value).toLocaleDateString() : '-'}</>;
652
+ case 'DATETIME':
653
+ return <>{field.value ? new Date(field.value).toLocaleString() : '-'}</>;
654
+ default:
655
+ return <>{field.value || '-'}</>;
656
+ }
657
+ }
658
+
659
+ // Display in spec table
660
+ {product.metafields?.map(mf => (
661
+ <tr key={mf.id}>
662
+ <td>{mf.definitionName}</td>
663
+ <td><MetafieldValue field={mf} /></td>
664
+ </tr>
665
+ ))}
666
+
667
+ // Get specific field by key
668
+ const material = getProductMetafieldValue(product, 'material');
669
+ const careInstructions = getProductMetafield(product, 'care_instructions');
670
+
671
+ // Get available metafield definitions (schema)
672
+ const { definitions } = await client.getPublicMetafieldDefinitions();
673
+ // Use definitions to build dynamic UI (filters, forms, etc.)
674
+ ```
675
+
676
+ > **Tip:** `metafields` may be empty if the store hasn't defined custom fields. Always use optional chaining.
677
+
678
+ ---
679
+
680
+ ## Customer Authentication
681
+
682
+ ```typescript
683
+ // Register
684
+ const auth = await client.registerCustomer({
685
+ email: 'john@example.com',
686
+ password: 'securepass123',
687
+ firstName: 'John',
688
+ lastName: 'Doe',
689
+ });
690
+
691
+ if (auth.requiresVerification) {
692
+ // Store token for verification step
693
+ localStorage.setItem('verificationToken', auth.token);
694
+ localStorage.setItem('verificationEmail', 'john@example.com');
695
+ window.location.href = '/verify-email';
696
+ } else {
697
+ client.setCustomerToken(auth.token);
698
+ localStorage.setItem('customerToken', auth.token);
699
+ }
700
+
701
+ // Login
702
+ const auth = await client.loginCustomer('john@example.com', 'password');
703
+
704
+ if (auth.requiresVerification) {
705
+ localStorage.setItem('verificationToken', auth.token);
706
+ localStorage.setItem('verificationEmail', 'john@example.com');
707
+ window.location.href = '/verify-email';
708
+ } else {
709
+ client.setCustomerToken(auth.token);
710
+ localStorage.setItem('customerToken', auth.token);
711
+ }
712
+
713
+ // Verify email (on /verify-email page)
714
+ const result = await client.verifyEmail(code, token);
715
+ if (result.verified) {
716
+ client.setCustomerToken(token);
717
+ localStorage.setItem('customerToken', token);
718
+ localStorage.removeItem('verificationToken');
719
+ localStorage.removeItem('verificationEmail');
720
+ window.location.href = '/account';
721
+ }
722
+
723
+ // Resend verification code
724
+ await client.resendVerificationEmail(token);
725
+
726
+ // Forgot password (on /forgot-password page)
727
+ await client.forgotPassword(email);
728
+ // Always succeeds (prevents email enumeration)
729
+
730
+ // Reset password (on /reset-password page — user arrives via email link)
731
+ const token = new URLSearchParams(window.location.search).get('token');
732
+ await client.resetPassword(token!, newPassword);
733
+ // On success: redirect to /login
734
+
735
+ // Logout
736
+ client.setCustomerToken(null);
737
+ localStorage.removeItem('customerToken');
738
+
739
+ // Get profile (requires token)
740
+ const profile = await client.getMyProfile();
741
+
742
+ // Get order history
743
+ const { data: orders, meta } = await client.getMyOrders({ page: 1, limit: 10 });
744
+ ```
745
+
746
+ ---
747
+
748
+ ## OAuth / Social Login
749
+
750
+ ```typescript
751
+ // Get available providers for this store
752
+ const { providers } = await client.getAvailableOAuthProviders();
753
+ // providers = ['GOOGLE', 'FACEBOOK', 'GITHUB']
754
+
755
+ // Redirect to OAuth provider
756
+ const { authorizationUrl } = await client.getOAuthAuthorizeUrl('GOOGLE', {
757
+ redirectUrl: `${window.location.origin}/auth/callback`,
758
+ });
759
+ window.location.href = authorizationUrl;
760
+
761
+ // Handle callback (on /auth/callback page — backend redirects here with params)
762
+ const params = new URLSearchParams(window.location.search);
763
+ if (params.get('oauth_success') === 'true') {
764
+ const token = params.get('token');
765
+ client.setCustomerToken(token!);
766
+ // Also available: customer_id, customer_email, is_new
767
+ } else if (params.get('oauth_error')) {
768
+ // Show error to user
769
+ }
770
+ ```
771
+
772
+ ---
773
+
774
+ ## Required Pages Checklist
775
+
776
+ - [ ] **Home** (`/`) - Featured products grid
777
+ - [ ] **Products** (`/products`) - Product list with infinite scroll
778
+ - [ ] **Product Detail** (`/products/[slug]`) - Use `getProductBySlug(slug)`. Show **upsells** + **related products** from `product.recommendations` (embedded in response)
779
+ - [ ] **Cart** (`/cart`) - Show items, quantities, totals, **coupon code input**, discount display, **cross-sell recommendations** via `getCartRecommendations()`
780
+ - [ ] **Checkout** (`/checkout`) - Address → Shipping → Payment. **Show discount in order summary!**
781
+ - [ ] **Success** (`/checkout/success`) - **Must call `completeGuestCheckout()`!**
782
+ - [ ] **Login** (`/login`) - Email/password + social buttons, handle `requiresVerification`
783
+ - [ ] **Register** (`/register`) - Registration form, handle `requiresVerification`
784
+ - [ ] **Verify Email** (`/verify-email`) - 6-digit code input + resend button. **ALWAYS create this page** even if verification is currently disabled — the store owner can enable it at any time
785
+ - [ ] **Forgot Password** (`/forgot-password`) - Email input, shows success message
786
+ - [ ] **Reset Password** (`/reset-password`) - Token from URL + new password form
787
+ - [ ] **OAuth Callback** (`/auth/callback`) - Handle OAuth redirect with token from URL params
788
+ - [ ] **Account** (`/account`) - Profile + order history
789
+ - [ ] **Header** - Logo, nav, cart icon with count, search
790
+
791
+ ### ALWAYS Build These (Even If Currently Disabled)
792
+
793
+ Some features may not be configured yet, but the store owner can enable them at any time. **Always create the UI** — SDK methods return empty/null when not configured:
794
+
795
+ - **Email Verification** → `/verify-email` page. `requiresVerification` is checked in login/register flows.
796
+ - **OAuth Buttons** → Social login buttons on Login & Register + `/auth/callback` page. `getAvailableOAuthProviders()` returns `[]` when none configured — buttons just don't render.
797
+ - **Password Reset** → `/forgot-password` + `/reset-password` pages. `forgotPassword()` silently succeeds when no account exists.
798
+ - **Discount Banners** → `getDiscountBanners()` returns `[]` when no rules component renders nothing.
799
+ - **Product Discount Badges** → `getProductDiscountBadge(id)` returns `null` — renders nothing.
800
+ - **Cart Nudges** → `cart.nudges` is `[]` renders nothing.
801
+ - **Coupon Input** → Always show in cart. Works even with no coupons configured.
802
+ - **Product Recommendations** → Upsells + related on product page (from `product.recommendations`), cross-sells on cart page (`getCartRecommendations()`). Returns empty arrays when none configured.
803
+
804
+ ---
805
+
806
+ ## Common Type Gotchas
807
+
808
+ ```typescript
809
+ // ❌ WRONG // ✅ CORRECT
810
+ address.state address.region
811
+ cart.total getCartTotals(cart).total
812
+ cart.discount cart.discountAmount
813
+ item.name (in cart) item.product.name
814
+ response.url (OAuth) response.authorizationUrl
815
+ providers.forEach (OAuth) response.providers.forEach
816
+ status === 'completed' status === 'succeeded'
817
+ product.metafields.name product.metafields[0].definitionName
818
+ product.metafields.key product.metafields[0].definitionKey
819
+ orderItem.unitPrice orderItem.price (OrderItem is FLAT, not nested!)
820
+ cartItem.price cartItem.unitPrice (Cart/Checkout items use unitPrice)
821
+ waitResult.orderNumber waitResult.status.orderNumber (nested in PaymentStatus)
822
+ variant.attributes.map(...) Object.entries(variant.attributes || {}) (it's an object!)
823
+ categorySuggestion.slug // doesn't exist! Only: id, name, productCount
824
+ order.status === 'COMPLETED' order.status === 'delivered' (OrderStatus is lowercase!)
825
+ getCartTotals(cart) // Works all carts are server carts now
826
+ result.checkoutId (guest checkout) // ⚠️ Check result.tracked first! It's a union type
827
+ ```
828
+
829
+ **Key distinctions:**
830
+
831
+ - **OrderItem** (from orders): Flat structure — `item.price`, `item.name`, `item.image`
832
+ - **CartItem / CheckoutLineItem**: Nested structure`item.unitPrice`, `item.product.name`, `item.product.images`
833
+ - **`getCartTotals()`** works on all cartsguests now use server-side session carts with full `subtotal`/`discountAmount` fields.
834
+ - **`GuestCheckoutStartResponse`** is a union type always check `result.tracked` before accessing `result.checkoutId`
835
+ - **`WaitForOrderResult`** has `result.status.orderNumber`, NOT `result.orderNumber`. But `completeGuestCheckout()` returns `GuestOrderResponse` which DOES have `result.orderNumber` directly.
836
+ - **Cart state**: Use `useState<Cart | null>(null)` and load with `smartGetCart()` in `useEffect` — all carts are server-side now, no hydration mismatch issues.
837
+
838
+ ---
839
+
840
+ ## Full SDK Documentation
841
+
842
+ For complete API reference and working code examples:
843
+ **https://brainerce.com/docs/sdk**