brainerce 1.0.0 → 1.0.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.
@@ -1,716 +1,700 @@
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
- // ⚠️ LocalCart vs Cart - KEY DIFFERENCES:
139
- // Server Cart has: id, itemCount, subtotal, discountAmount
140
- // Guest LocalCart has NONE of these! Only: items, couponCode, customer
141
- // To check type: if ('id' in cart) { /* server Cart */ } else { /* LocalCart */ }
142
- // Item count for both: cart.items.length
143
- ```
144
-
145
- ### 🏷️ Coupon Code (Add to Cart Page!)
146
-
147
- ```typescript
148
- // Apply coupon - only works on server Cart (has 'id' field)
149
- // Server cart is created after startGuestCheckout() or for logged-in users
150
- const updatedCart = await client.applyCoupon(cartId, 'SAVE20');
151
- console.log(updatedCart.discountAmount); // "10.00" (string)
152
- console.log(updatedCart.couponCode); // "SAVE20"
153
-
154
- // Remove coupon
155
- const updatedCart = await client.removeCoupon(cartId);
156
-
157
- // Calculate totals including discount
158
- import { getCartTotals } from 'brainerce';
159
- const totals = getCartTotals(cart); // { subtotal, discount, shipping, total }
160
- ```
161
-
162
- **Cart page coupon UI:**
163
-
164
- ```typescript
165
- // State
166
- const [couponCode, setCouponCode] = useState('');
167
- const [couponError, setCouponError] = useState('');
168
- const [isApplying, setIsApplying] = useState(false);
169
-
170
- // Apply handler
171
- async function handleApplyCoupon() {
172
- if (!couponCode.trim() || !('id' in cart)) return;
173
- setIsApplying(true);
174
- setCouponError('');
175
- try {
176
- const updatedCart = await client.applyCoupon(cart.id, couponCode.trim());
177
- setCart(updatedCart);
178
- setCouponCode('');
179
- } catch (err: any) {
180
- setCouponError(err.message || 'Invalid coupon code');
181
- } finally {
182
- setIsApplying(false);
183
- }
184
- }
185
-
186
- // Remove handler
187
- async function handleRemoveCoupon() {
188
- if (!('id' in cart)) return;
189
- const updatedCart = await client.removeCoupon(cart.id);
190
- setCart(updatedCart);
191
- }
192
-
193
- // UI - place in cart order summary
194
- {('id' in cart) && (
195
- <div>
196
- {cart.couponCode ? (
197
- <div className="flex items-center justify-between bg-green-50 p-2 rounded">
198
- <span className="text-green-700 text-sm">🏷️ {cart.couponCode}</span>
199
- <button onClick={handleRemoveCoupon} className="text-red-500 text-sm">✕</button>
200
- </div>
201
- ) : (
202
- <div className="flex gap-2">
203
- <input value={couponCode} onChange={(e) => setCouponCode(e.target.value)}
204
- placeholder="Coupon code" className="flex-1 border rounded px-3 py-2 text-sm" />
205
- <button onClick={handleApplyCoupon} disabled={isApplying}
206
- className="px-4 py-2 bg-gray-800 text-white rounded text-sm">
207
- {isApplying ? '...' : 'Apply'}
208
- </button>
209
- </div>
210
- )}
211
- {couponError && <p className="text-red-500 text-xs mt-1">{couponError}</p>}
212
- </div>
213
- )}
214
-
215
- // Order summary - show discount line
216
- {('id' in cart) && parseFloat(cart.discountAmount) > 0 && (
217
- <div className="text-green-600">Discount: -{formatPrice(cart.discountAmount)}</div>
218
- )}
219
- ```
220
-
221
- **Checkout order summary - coupon carries over from cart:**
222
-
223
- ```typescript
224
- // Checkout already includes coupon from cart
225
- <div>Subtotal: {formatPrice(checkout.subtotal)}</div>
226
- {parseFloat(checkout.discountAmount) > 0 && (
227
- <div className="text-green-600">
228
- Discount ({checkout.couponCode}): -{formatPrice(checkout.discountAmount)}
229
- </div>
230
- )}
231
- <div>Shipping: {formatPrice(selectedRate?.price || '0')}</div>
232
- <div className="font-bold">Total: {formatPrice(checkout.total)}</div>
233
- ```
234
-
235
- ---
236
-
237
- ## 🛒 Partial Checkout (AliExpress Style) - REQUIRED!
238
-
239
- Cart page MUST have checkboxes so users can select which items to buy:
240
-
241
- ```typescript
242
- // Cart page - track selected items
243
- const [selectedIndices, setSelectedIndices] = useState<number[]>(
244
- cart.items.map((_, i) => i) // All selected by default
245
- );
246
-
247
- const toggleItem = (index: number) => {
248
- setSelectedIndices(prev =>
249
- prev.includes(index)
250
- ? prev.filter(i => i !== index)
251
- : [...prev, index]
252
- );
253
- };
254
-
255
- const toggleAll = () => {
256
- if (selectedIndices.length === cart.items.length) {
257
- setSelectedIndices([]); // Deselect all
258
- } else {
259
- setSelectedIndices(cart.items.map((_, i) => i)); // Select all
260
- }
261
- };
262
-
263
- // In your cart UI:
264
- <div>
265
- <label>
266
- <input
267
- type="checkbox"
268
- checked={selectedIndices.length === cart.items.length}
269
- onChange={toggleAll}
270
- />
271
- Select All
272
- </label>
273
- </div>
274
-
275
- {cart.items.map((item, index) => (
276
- <div key={index}>
277
- <input
278
- type="checkbox"
279
- checked={selectedIndices.includes(index)}
280
- onChange={() => toggleItem(index)}
281
- />
282
- {/* ... item details ... */}
283
- </div>
284
- ))}
285
-
286
- // On checkout button - pass selected items!
287
- const handleCheckout = async () => {
288
- if (selectedIndices.length === 0) {
289
- alert('Please select items to checkout');
290
- return;
291
- }
292
-
293
- const result = await client.startGuestCheckout({ selectedIndices });
294
- // Only selected items go to checkout, others stay in cart!
295
- };
296
- ```
297
-
298
- **Why this matters:**
299
-
300
- - Users can buy some items now, leave others for later
301
- - After payment, `completeGuestCheckout()` sends the order and only removes purchased items
302
- - Remaining items stay in cart for future purchase
303
-
304
- **⚠️ Order Summary on Checkout Page - Use checkout.lineItems!**
305
-
306
- ```typescript
307
- // ❌ WRONG - Shows ALL cart items (even unselected ones!)
308
- <div className="order-summary">
309
- {cart.items.map(item => (
310
- <div>{item.product.name} - ${item.price}</div>
311
- ))}
312
- </div>
313
-
314
- // ✅ CORRECT - Shows only items being purchased in this checkout
315
- <div className="order-summary">
316
- {checkout.lineItems.map(item => (
317
- <div>{item.product.name} - ${item.price}</div>
318
- ))}
319
- </div>
320
- ```
321
-
322
- The `checkout` object's `lineItems` array contains ONLY the items selected for this checkout!
323
-
324
- ---
325
-
326
- ## Complete Checkout Flow
327
-
328
- ### Step 1: Start Checkout (Different for Guest vs Logged-in!)
329
-
330
- ```typescript
331
- async function startCheckout() {
332
- const cart = await client.smartGetCart();
333
-
334
- if (cart.items.length === 0) {
335
- alert('Cart is empty');
336
- return;
337
- }
338
-
339
- let checkoutId: string;
340
-
341
- if (client.isCustomerLoggedIn()) {
342
- // Logged-in: create checkout from server cart
343
- const checkout = await client.createCheckout({ cartId: cart.id });
344
- checkoutId = checkout.id;
345
- } else {
346
- // Guest: use startGuestCheckout (syncs local cart to server)
347
- const result = await client.startGuestCheckout();
348
- if (!result.tracked || !result.checkoutId) {
349
- throw new Error('Failed to create checkout');
350
- }
351
- checkoutId = result.checkoutId;
352
- }
353
-
354
- // Save for payment page
355
- localStorage.setItem('checkoutId', checkoutId);
356
-
357
- // Navigate to checkout
358
- window.location.href = '/checkout';
359
- }
360
- ```
361
-
362
- ### Step 2: Shipping Address
363
-
364
- ```typescript
365
- const checkoutId = localStorage.getItem('checkoutId')!;
366
-
367
- // Set shipping address (email is required!)
368
- const { checkout, rates } = await client.setShippingAddress(checkoutId, {
369
- email: 'customer@example.com',
370
- firstName: 'John',
371
- lastName: 'Doe',
372
- line1: '123 Main St',
373
- city: 'New York',
374
- region: 'NY', // ⚠️ Use 'region', NOT 'state'!
375
- postalCode: '10001',
376
- country: 'US',
377
- });
378
-
379
- // Show available shipping rates
380
- rates.forEach((rate) => {
381
- console.log(`${rate.name}: $${rate.price}`);
382
- });
383
- ```
384
-
385
- ### Step 3: Select Shipping Method
386
-
387
- ```typescript
388
- await client.selectShippingMethod(checkoutId, selectedRateId);
389
- ```
390
-
391
- ### Step 4: Payment (Multi-Provider)
392
-
393
- ```typescript
394
- // 1. Check if payment is configured
395
- const { hasPayments, providers } = await client.getPaymentProviders();
396
- if (!hasPayments) {
397
- return <div>Payment not configured for this store</div>;
398
- }
399
-
400
- // 2. Create payment intent — returns provider type!
401
- const intent = await client.createPaymentIntent(checkoutId, {
402
- successUrl: `${window.location.origin}/checkout/success?checkout_id=${checkoutId}`,
403
- cancelUrl: `${window.location.origin}/checkout?error=cancelled`,
404
- });
405
-
406
- // 3. Branch by provider
407
- if (intent.provider === 'grow') {
408
- // Grow: clientSecret is a payment URL show in iframe
409
- // <iframe src={intent.clientSecret} style={{ width: '100%', minHeight: '600px', border: 'none' }} allow="payment" />
410
- // Supports credit cards, Bit, Apple Pay, Google Pay, bank transfers
411
- // Add fallback: <a href={intent.clientSecret} target="_blank">Open payment in new tab</a>
412
- // Order created automatically via webhook!
413
- } else {
414
- // Stripe: install @stripe/stripe-js @stripe/react-stripe-js
415
- import { loadStripe } from '@stripe/stripe-js';
416
- const stripeProvider = providers.find(p => p.provider === 'stripe');
417
- const stripe = await loadStripe(stripeProvider.publicKey, {
418
- stripeAccount: stripeProvider.stripeAccountId,
419
- });
420
-
421
- // Confirm payment (in your payment form)
422
- const { error } = await stripe.confirmPayment({
423
- elements,
424
- confirmParams: {
425
- return_url: `${window.location.origin}/checkout/success?checkout_id=${checkoutId}`,
426
- },
427
- });
428
-
429
- if (error) {
430
- setError(error.message);
431
- }
432
- // If no error, Stripe redirects to success page
433
- }
434
- ```
435
-
436
- ### Step 5: Success Page (Complete Order & Clear Cart!)
437
-
438
- ```typescript
439
- // /checkout/success/page.tsx
440
- 'use client';
441
- import { useEffect, useState } from 'react';
442
- import { client } from '@/lib/brainerce';
443
-
444
- export default function CheckoutSuccessPage() {
445
- const [orderNumber, setOrderNumber] = useState<string>();
446
- const [loading, setLoading] = useState(true);
447
-
448
- useEffect(() => {
449
- // Break out of iframe if redirected here from Grow payment page
450
- if (window.top !== window.self) {
451
- window.top!.location.href = window.location.href;
452
- return;
453
- }
454
-
455
- const checkoutId = new URLSearchParams(window.location.search).get('checkout_id');
456
-
457
- if (checkoutId) {
458
- // ⚠️ CRITICAL: Complete the order on the server AND clear the cart!
459
- // Do NOT use handlePaymentSuccess() - it only clears localStorage!
460
- client.completeGuestCheckout(checkoutId).then(result => {
461
- setOrderNumber(result.orderNumber);
462
- setLoading(false);
463
- }).catch(() => {
464
- // Order may already be completed (e.g., page refresh) - check status
465
- client.getPaymentStatus(checkoutId).then(status => {
466
- if (status.orderNumber) {
467
- setOrderNumber(status.orderNumber);
468
- }
469
- setLoading(false);
470
- });
471
- });
472
- }
473
- }, []);
474
-
475
- return (
476
- <div className="text-center py-12">
477
- <h1 className="text-2xl font-bold text-green-600">Thank you for your order!</h1>
478
- {loading && <p className="mt-2">Processing your order...</p>}
479
- {orderNumber && <p className="mt-2">Order #{orderNumber}</p>}
480
- <p className="mt-4">A confirmation email will be sent shortly.</p>
481
- </div>
482
- );
483
- }
484
- ```
485
-
486
- ---
487
-
488
- ## Partial Checkout (AliExpress Style)
489
-
490
- Allow customers to buy only some items from their cart:
491
-
492
- ```typescript
493
- // Start checkout with only selected items (by index)
494
- const result = await client.startGuestCheckout({
495
- selectedIndices: [0, 2], // Buy items at index 0 and 2 only
496
- });
497
-
498
- // After payment, completeGuestCheckout() sends the order AND removes only those items!
499
- // Other items stay in cart.
500
- ```
501
-
502
- ---
503
-
504
- ## Products API
505
-
506
- ```typescript
507
- // List products with pagination
508
- const { data: products, meta } = await client.getProducts({
509
- page: 1,
510
- limit: 20,
511
- search: 'blue shirt', // Searches name, description, SKU, categories, tags
512
- });
513
- // meta = { page: 1, limit: 20, total: 150, totalPages: 8 }
514
-
515
- // Get single product by slug (for product detail page)
516
- const product = await client.getProductBySlug('blue-cotton-shirt');
517
-
518
- // Search suggestions (for autocomplete)
519
- const suggestions = await client.getSearchSuggestions('blue', 5);
520
- // { products: [...], categories: [...] }
521
- ```
522
-
523
- ---
524
-
525
- ## Product Custom Fields (Metafields)
526
-
527
- Products may have custom fields defined by the store owner (e.g., "Material", "Care Instructions", "Warranty").
528
-
529
- ```typescript
530
- import { getProductMetafield, getProductMetafieldValue } from 'brainerce';
531
-
532
- // Access metafields on a product
533
- const product = await client.getProductBySlug('blue-shirt');
534
-
535
- // Get all custom fields
536
- product.metafields?.forEach((field) => {
537
- console.log(`${field.definitionName}: ${field.value}`);
538
- });
539
-
540
- // Get specific field by key
541
- const material = getProductMetafieldValue(product, 'material');
542
- const careInstructions = getProductMetafield(product, 'care_instructions');
543
-
544
- // Get available metafield definitions (schema)
545
- const { definitions } = await client.getPublicMetafieldDefinitions();
546
- // Use definitions to build dynamic UI (filters, forms, etc.)
547
- ```
548
-
549
- > **Tip:** `metafields` may be empty if the store hasn't defined custom fields. Always use optional chaining.
550
-
551
- ---
552
-
553
- ## Customer Authentication
554
-
555
- ```typescript
556
- // Register
557
- const auth = await client.registerCustomer({
558
- email: 'john@example.com',
559
- password: 'securepass123',
560
- firstName: 'John',
561
- lastName: 'Doe',
562
- });
563
-
564
- if (auth.requiresVerification) {
565
- // Store token for verification step
566
- localStorage.setItem('verificationToken', auth.token);
567
- localStorage.setItem('verificationEmail', 'john@example.com');
568
- window.location.href = '/verify-email';
569
- } else {
570
- client.setCustomerToken(auth.token);
571
- localStorage.setItem('customerToken', auth.token);
572
- }
573
-
574
- // Login
575
- const auth = await client.loginCustomer('john@example.com', 'password');
576
-
577
- if (auth.requiresVerification) {
578
- localStorage.setItem('verificationToken', auth.token);
579
- localStorage.setItem('verificationEmail', 'john@example.com');
580
- window.location.href = '/verify-email';
581
- } else {
582
- client.setCustomerToken(auth.token);
583
- localStorage.setItem('customerToken', auth.token);
584
- }
585
-
586
- // Verify email (on /verify-email page)
587
- const result = await client.verifyEmail(code, token);
588
- if (result.verified) {
589
- client.setCustomerToken(token);
590
- localStorage.setItem('customerToken', token);
591
- localStorage.removeItem('verificationToken');
592
- localStorage.removeItem('verificationEmail');
593
- window.location.href = '/account';
594
- }
595
-
596
- // Resend verification code
597
- await client.resendVerificationEmail(token);
598
-
599
- // Logout
600
- client.setCustomerToken(null);
601
- localStorage.removeItem('customerToken');
602
-
603
- // Get profile (requires token)
604
- const profile = await client.getMyProfile();
605
-
606
- // Get order history
607
- const { data: orders, meta } = await client.getMyOrders({ page: 1, limit: 10 });
608
- ```
609
-
610
- ---
611
-
612
- ## OAuth / Social Login
613
-
614
- ```typescript
615
- // Get available providers for this store
616
- const { providers } = await client.getAvailableOAuthProviders();
617
- // providers = ['GOOGLE', 'FACEBOOK', 'GITHUB']
618
-
619
- // Redirect to OAuth provider
620
- const { authorizationUrl } = await client.getOAuthAuthorizeUrl('GOOGLE', {
621
- redirectUrl: `${window.location.origin}/auth/callback`,
622
- });
623
- window.location.href = authorizationUrl;
624
-
625
- // Handle callback (on /auth/callback page)
626
- const code = new URLSearchParams(window.location.search).get('code');
627
- const state = new URLSearchParams(window.location.search).get('state');
628
- const { customer, token } = await client.handleOAuthCallback('GOOGLE', code!, state!);
629
- setCustomerToken(token);
630
- ```
631
-
632
- ---
633
-
634
- ## Required Pages Checklist
635
-
636
- - [ ] **Home** (`/`) - Featured products grid
637
- - [ ] **Products** (`/products`) - Product list with infinite scroll
638
- - [ ] **Product Detail** (`/products/[slug]`) - Use `getProductBySlug(slug)`
639
- - [ ] **Cart** (`/cart`) - Show items, quantities, totals, **coupon code input**, discount display
640
- - [ ] **Checkout** (`/checkout`) - Address Shipping Payment. **Show discount in order summary!**
641
- - [ ] **Success** (`/checkout/success`) - **Must call `completeGuestCheckout()`!**
642
- - [ ] **Login** (`/login`) - Email/password + social buttons, handle `requiresVerification`
643
- - [ ] **Register** (`/register`) - Registration form, handle `requiresVerification`
644
- - [ ] **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
645
- - [ ] **OAuth Callback** (`/auth/callback`) - Handle OAuth redirect with token from URL params
646
- - [ ] **Account** (`/account`) - Profile + order history
647
- - [ ] **Header** - Logo, nav, cart icon with count, search
648
-
649
- ### ALWAYS Build These (Even If Currently Disabled)
650
-
651
- 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:
652
-
653
- - **Email Verification** → `/verify-email` page. `requiresVerification` is checked in login/register flows.
654
- - **OAuth Buttons** → Social login buttons on Login & Register + `/auth/callback` page. `getAvailableOAuthProviders()` returns `[]` when none configured — buttons just don't render.
655
- - **Discount Banners** → `getDiscountBanners()` returns `[]` when no rulescomponent renders nothing.
656
- - **Product Discount Badges** → `getProductDiscountBadge(id)` returns `null` — renders nothing.
657
- - **Cart Nudges** → `cart.nudges` is `[]` — renders nothing.
658
- - **Coupon Input** → Always show in cart. Works even with no coupons configured.
659
-
660
- ---
661
-
662
- ## Common Type Gotchas
663
-
664
- ```typescript
665
- // ❌ WRONG // ✅ CORRECT
666
- address.state address.region
667
- cart.total getCartTotals(cart).total
668
- cart.discount cart.discountAmount
669
- item.name (in cart) item.product.name
670
- response.url (OAuth) response.authorizationUrl
671
- providers.forEach (OAuth) response.providers.forEach
672
- status === 'completed' status === 'succeeded'
673
- product.metafields.name product.metafields[0].definitionName
674
- product.metafields.key product.metafields[0].definitionKey
675
- orderItem.unitPrice orderItem.price (OrderItem is FLAT, not nested!)
676
- cartItem.price cartItem.unitPrice (Cart/Checkout items use unitPrice)
677
- waitResult.orderNumber waitResult.status.orderNumber (nested in PaymentStatus)
678
- variant.attributes.map(...) Object.entries(variant.attributes || {}) (it's an object!)
679
- categorySuggestion.slug // doesn't exist! Only: id, name, productCount
680
- order.status === 'COMPLETED' order.status === 'delivered' (OrderStatus is lowercase!)
681
- getCartTotals(localCart) // LocalCart has no subtotal! Calculate manually
682
- result.checkoutId (guest checkout) // ⚠️ Check result.tracked first! It's a union type
683
- ```
684
-
685
- **Key distinctions:**
686
-
687
- - **OrderItem** (from orders): Flat structure — `item.price`, `item.name`, `item.image`
688
- - **CartItem / CheckoutLineItem**: Nested structure — `item.unitPrice`, `item.product.name`, `item.product.images`
689
- - **`getCartTotals()`** only works with **server Cart** (has `subtotal`/`discountAmount`). For **LocalCart**, calculate manually:
690
- ```typescript
691
- const subtotal = cart.items.reduce(
692
- (sum, item) => sum + parseFloat(item.price || '0') * item.quantity,
693
- 0
694
- );
695
- ```
696
- - **`GuestCheckoutStartResponse`** is a union type — always check `result.tracked` before accessing `result.checkoutId`
697
- - **`WaitForOrderResult`** has `result.status.orderNumber`, NOT `result.orderNumber`. But `completeGuestCheckout()` returns `GuestOrderResponse` which DOES have `result.orderNumber` directly.
698
- - **⚠️ HYDRATION: Never use `useState(client.getLocalCart())`** in Next.js — causes hydration mismatch! Server has no localStorage (empty cart) but client does (real cart). Always start with empty state and load in `useEffect`:
699
-
700
- ```typescript
701
- // ❌ WRONG — hydration mismatch!
702
- const [cart, setCart] = useState(client.getLocalCart());
703
-
704
- // ✅ CORRECT — load after hydration
705
- const [cart, setCart] = useState<LocalCart>({ items: [], updatedAt: '' });
706
- useEffect(() => {
707
- setCart(client.getLocalCart());
708
- }, []);
709
- ```
710
-
711
- ---
712
-
713
- ## Full SDK Documentation
714
-
715
- For complete API reference and working code examples:
716
- **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
+ ## Complete Checkout Flow
324
+
325
+ ### Step 1: Start Checkout (Different for Guest vs Logged-in!)
326
+
327
+ ```typescript
328
+ async function startCheckout() {
329
+ const cart = await client.smartGetCart();
330
+
331
+ if (cart.items.length === 0) {
332
+ alert('Cart is empty');
333
+ return;
334
+ }
335
+
336
+ let checkoutId: string;
337
+
338
+ if (client.isCustomerLoggedIn()) {
339
+ // Logged-in: create checkout from server cart
340
+ const checkout = await client.createCheckout({ cartId: cart.id });
341
+ checkoutId = checkout.id;
342
+ } else {
343
+ // Guest: use startGuestCheckout (syncs local cart to server)
344
+ const result = await client.startGuestCheckout();
345
+ if (!result.tracked || !result.checkoutId) {
346
+ throw new Error('Failed to create checkout');
347
+ }
348
+ checkoutId = result.checkoutId;
349
+ }
350
+
351
+ // Save for payment page
352
+ localStorage.setItem('checkoutId', checkoutId);
353
+
354
+ // Navigate to checkout
355
+ window.location.href = '/checkout';
356
+ }
357
+ ```
358
+
359
+ ### Step 2: Shipping Address
360
+
361
+ ```typescript
362
+ const checkoutId = localStorage.getItem('checkoutId')!;
363
+
364
+ // Set shipping address (email is required!)
365
+ const { checkout, rates } = await client.setShippingAddress(checkoutId, {
366
+ email: 'customer@example.com',
367
+ firstName: 'John',
368
+ lastName: 'Doe',
369
+ line1: '123 Main St',
370
+ city: 'New York',
371
+ region: 'NY', // ⚠️ Use 'region', NOT 'state'!
372
+ postalCode: '10001',
373
+ country: 'US',
374
+ });
375
+
376
+ // Show available shipping rates
377
+ rates.forEach((rate) => {
378
+ console.log(`${rate.name}: $${rate.price}`);
379
+ });
380
+ ```
381
+
382
+ ### Step 3: Select Shipping Method
383
+
384
+ ```typescript
385
+ await client.selectShippingMethod(checkoutId, selectedRateId);
386
+ ```
387
+
388
+ ### Step 4: Payment (Multi-Provider)
389
+
390
+ ```typescript
391
+ // 1. Check if payment is configured
392
+ const { hasPayments, providers } = await client.getPaymentProviders();
393
+ if (!hasPayments) {
394
+ return <div>Payment not configured for this store</div>;
395
+ }
396
+
397
+ // 2. Create payment intent returns provider type!
398
+ const intent = await client.createPaymentIntent(checkoutId, {
399
+ successUrl: `${window.location.origin}/checkout/success?checkout_id=${checkoutId}`,
400
+ cancelUrl: `${window.location.origin}/checkout?error=cancelled`,
401
+ });
402
+
403
+ // 3. Branch by provider
404
+ if (intent.provider === 'grow') {
405
+ // Grow: clientSecret is a payment URL — show in iframe
406
+ // <iframe src={intent.clientSecret} style={{ width: '100%', minHeight: '600px', border: 'none' }} allow="payment" />
407
+ // Supports credit cards, Bit, Apple Pay, Google Pay, bank transfers
408
+ // Add fallback: <a href={intent.clientSecret} target="_blank">Open payment in new tab</a>
409
+ // Order created automatically via webhook!
410
+ } else {
411
+ // Stripe: install @stripe/stripe-js @stripe/react-stripe-js
412
+ import { loadStripe } from '@stripe/stripe-js';
413
+ const stripeProvider = providers.find(p => p.provider === 'stripe');
414
+ const stripe = await loadStripe(stripeProvider.publicKey, {
415
+ stripeAccount: stripeProvider.stripeAccountId,
416
+ });
417
+
418
+ // Confirm payment (in your payment form)
419
+ const { error } = await stripe.confirmPayment({
420
+ elements,
421
+ confirmParams: {
422
+ return_url: `${window.location.origin}/checkout/success?checkout_id=${checkoutId}`,
423
+ },
424
+ });
425
+
426
+ if (error) {
427
+ setError(error.message);
428
+ }
429
+ // If no error, Stripe redirects to success page
430
+ }
431
+ ```
432
+
433
+ ### Step 5: Success Page (Complete Order & Clear Cart!)
434
+
435
+ ```typescript
436
+ // /checkout/success/page.tsx
437
+ 'use client';
438
+ import { useEffect, useState } from 'react';
439
+ import { client } from '@/lib/brainerce';
440
+
441
+ export default function CheckoutSuccessPage() {
442
+ const [orderNumber, setOrderNumber] = useState<string>();
443
+ const [loading, setLoading] = useState(true);
444
+
445
+ useEffect(() => {
446
+ // Break out of iframe if redirected here from Grow payment page
447
+ if (window.top !== window.self) {
448
+ window.top!.location.href = window.location.href;
449
+ return;
450
+ }
451
+
452
+ const checkoutId = new URLSearchParams(window.location.search).get('checkout_id');
453
+
454
+ if (checkoutId) {
455
+ // ⚠️ CRITICAL: Complete the order on the server AND clear the cart!
456
+ // Do NOT use handlePaymentSuccess() - it only clears localStorage!
457
+ client.completeGuestCheckout(checkoutId).then(result => {
458
+ setOrderNumber(result.orderNumber);
459
+ setLoading(false);
460
+ }).catch(() => {
461
+ // Order may already be completed (e.g., page refresh) - check status
462
+ client.getPaymentStatus(checkoutId).then(status => {
463
+ if (status.orderNumber) {
464
+ setOrderNumber(status.orderNumber);
465
+ }
466
+ setLoading(false);
467
+ });
468
+ });
469
+ }
470
+ }, []);
471
+
472
+ return (
473
+ <div className="text-center py-12">
474
+ <h1 className="text-2xl font-bold text-green-600">Thank you for your order!</h1>
475
+ {loading && <p className="mt-2">Processing your order...</p>}
476
+ {orderNumber && <p className="mt-2">Order #{orderNumber}</p>}
477
+ <p className="mt-4">A confirmation email will be sent shortly.</p>
478
+ </div>
479
+ );
480
+ }
481
+ ```
482
+
483
+ ---
484
+
485
+ ## Partial Checkout (AliExpress Style)
486
+
487
+ Allow customers to buy only some items from their cart:
488
+
489
+ ```typescript
490
+ // Start checkout with only selected items (by index)
491
+ const result = await client.startGuestCheckout({
492
+ selectedIndices: [0, 2], // Buy items at index 0 and 2 only
493
+ });
494
+
495
+ // After payment, completeGuestCheckout() sends the order AND removes only those items!
496
+ // Other items stay in cart.
497
+ ```
498
+
499
+ ---
500
+
501
+ ## Products API
502
+
503
+ ```typescript
504
+ // List products with pagination
505
+ const { data: products, meta } = await client.getProducts({
506
+ page: 1,
507
+ limit: 20,
508
+ search: 'blue shirt', // Searches name, description, SKU, categories, tags
509
+ });
510
+ // meta = { page: 1, limit: 20, total: 150, totalPages: 8 }
511
+
512
+ // Get single product by slug (for product detail page)
513
+ const product = await client.getProductBySlug('blue-cotton-shirt');
514
+
515
+ // Search suggestions (for autocomplete)
516
+ const suggestions = await client.getSearchSuggestions('blue', 5);
517
+ // { products: [...], categories: [...] }
518
+ ```
519
+
520
+ ---
521
+
522
+ ## Product Custom Fields (Metafields)
523
+
524
+ Products may have custom fields defined by the store owner (e.g., "Material", "Care Instructions", "Warranty").
525
+
526
+ ```typescript
527
+ import { getProductMetafield, getProductMetafieldValue } from 'brainerce';
528
+
529
+ // Access metafields on a product
530
+ const product = await client.getProductBySlug('blue-shirt');
531
+
532
+ // Get all custom fields
533
+ product.metafields?.forEach((field) => {
534
+ console.log(`${field.definitionName}: ${field.value}`);
535
+ });
536
+
537
+ // Get specific field by key
538
+ const material = getProductMetafieldValue(product, 'material');
539
+ const careInstructions = getProductMetafield(product, 'care_instructions');
540
+
541
+ // Get available metafield definitions (schema)
542
+ const { definitions } = await client.getPublicMetafieldDefinitions();
543
+ // Use definitions to build dynamic UI (filters, forms, etc.)
544
+ ```
545
+
546
+ > **Tip:** `metafields` may be empty if the store hasn't defined custom fields. Always use optional chaining.
547
+
548
+ ---
549
+
550
+ ## Customer Authentication
551
+
552
+ ```typescript
553
+ // Register
554
+ const auth = await client.registerCustomer({
555
+ email: 'john@example.com',
556
+ password: 'securepass123',
557
+ firstName: 'John',
558
+ lastName: 'Doe',
559
+ });
560
+
561
+ if (auth.requiresVerification) {
562
+ // Store token for verification step
563
+ localStorage.setItem('verificationToken', auth.token);
564
+ localStorage.setItem('verificationEmail', 'john@example.com');
565
+ window.location.href = '/verify-email';
566
+ } else {
567
+ client.setCustomerToken(auth.token);
568
+ localStorage.setItem('customerToken', auth.token);
569
+ }
570
+
571
+ // Login
572
+ const auth = await client.loginCustomer('john@example.com', 'password');
573
+
574
+ if (auth.requiresVerification) {
575
+ localStorage.setItem('verificationToken', auth.token);
576
+ localStorage.setItem('verificationEmail', 'john@example.com');
577
+ window.location.href = '/verify-email';
578
+ } else {
579
+ client.setCustomerToken(auth.token);
580
+ localStorage.setItem('customerToken', auth.token);
581
+ }
582
+
583
+ // Verify email (on /verify-email page)
584
+ const result = await client.verifyEmail(code, token);
585
+ if (result.verified) {
586
+ client.setCustomerToken(token);
587
+ localStorage.setItem('customerToken', token);
588
+ localStorage.removeItem('verificationToken');
589
+ localStorage.removeItem('verificationEmail');
590
+ window.location.href = '/account';
591
+ }
592
+
593
+ // Resend verification code
594
+ await client.resendVerificationEmail(token);
595
+
596
+ // Logout
597
+ client.setCustomerToken(null);
598
+ localStorage.removeItem('customerToken');
599
+
600
+ // Get profile (requires token)
601
+ const profile = await client.getMyProfile();
602
+
603
+ // Get order history
604
+ const { data: orders, meta } = await client.getMyOrders({ page: 1, limit: 10 });
605
+ ```
606
+
607
+ ---
608
+
609
+ ## OAuth / Social Login
610
+
611
+ ```typescript
612
+ // Get available providers for this store
613
+ const { providers } = await client.getAvailableOAuthProviders();
614
+ // providers = ['GOOGLE', 'FACEBOOK', 'GITHUB']
615
+
616
+ // Redirect to OAuth provider
617
+ const { authorizationUrl } = await client.getOAuthAuthorizeUrl('GOOGLE', {
618
+ redirectUrl: `${window.location.origin}/auth/callback`,
619
+ });
620
+ window.location.href = authorizationUrl;
621
+
622
+ // Handle callback (on /auth/callback page — backend redirects here with params)
623
+ const params = new URLSearchParams(window.location.search);
624
+ if (params.get('oauth_success') === 'true') {
625
+ const token = params.get('token');
626
+ client.setCustomerToken(token!);
627
+ // Also available: customer_id, customer_email, is_new
628
+ } else if (params.get('oauth_error')) {
629
+ // Show error to user
630
+ }
631
+ ```
632
+
633
+ ---
634
+
635
+ ## Required Pages Checklist
636
+
637
+ - [ ] **Home** (`/`) - Featured products grid
638
+ - [ ] **Products** (`/products`) - Product list with infinite scroll
639
+ - [ ] **Product Detail** (`/products/[slug]`) - Use `getProductBySlug(slug)`
640
+ - [ ] **Cart** (`/cart`) - Show items, quantities, totals, **coupon code input**, discount display
641
+ - [ ] **Checkout** (`/checkout`) - Address → Shipping → Payment. **Show discount in order summary!**
642
+ - [ ] **Success** (`/checkout/success`) - **Must call `completeGuestCheckout()`!**
643
+ - [ ] **Login** (`/login`) - Email/password + social buttons, handle `requiresVerification`
644
+ - [ ] **Register** (`/register`) - Registration form, handle `requiresVerification`
645
+ - [ ] **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
646
+ - [ ] **OAuth Callback** (`/auth/callback`) - Handle OAuth redirect with token from URL params
647
+ - [ ] **Account** (`/account`) - Profile + order history
648
+ - [ ] **Header** - Logo, nav, cart icon with count, search
649
+
650
+ ### ALWAYS Build These (Even If Currently Disabled)
651
+
652
+ 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:
653
+
654
+ - **Email Verification** → `/verify-email` page. `requiresVerification` is checked in login/register flows.
655
+ - **OAuth Buttons** → Social login buttons on Login & Register + `/auth/callback` page. `getAvailableOAuthProviders()` returns `[]` when none configuredbuttons just don't render.
656
+ - **Discount Banners** → `getDiscountBanners()` returns `[]` when no rules component renders nothing.
657
+ - **Product Discount Badges** → `getProductDiscountBadge(id)` returns `null` — renders nothing.
658
+ - **Cart Nudges** → `cart.nudges` is `[]` renders nothing.
659
+ - **Coupon Input** → Always show in cart. Works even with no coupons configured.
660
+
661
+ ---
662
+
663
+ ## Common Type Gotchas
664
+
665
+ ```typescript
666
+ // ❌ WRONG // ✅ CORRECT
667
+ address.state address.region
668
+ cart.total getCartTotals(cart).total
669
+ cart.discount cart.discountAmount
670
+ item.name (in cart) item.product.name
671
+ response.url (OAuth) response.authorizationUrl
672
+ providers.forEach (OAuth) response.providers.forEach
673
+ status === 'completed' status === 'succeeded'
674
+ product.metafields.name product.metafields[0].definitionName
675
+ product.metafields.key product.metafields[0].definitionKey
676
+ orderItem.unitPrice orderItem.price (OrderItem is FLAT, not nested!)
677
+ cartItem.price cartItem.unitPrice (Cart/Checkout items use unitPrice)
678
+ waitResult.orderNumber waitResult.status.orderNumber (nested in PaymentStatus)
679
+ variant.attributes.map(...) Object.entries(variant.attributes || {}) (it's an object!)
680
+ categorySuggestion.slug // doesn't exist! Only: id, name, productCount
681
+ order.status === 'COMPLETED' order.status === 'delivered' (OrderStatus is lowercase!)
682
+ getCartTotals(cart) // Works all carts are server carts now
683
+ result.checkoutId (guest checkout) // ⚠️ Check result.tracked first! It's a union type
684
+ ```
685
+
686
+ **Key distinctions:**
687
+
688
+ - **OrderItem** (from orders): Flat structure — `item.price`, `item.name`, `item.image`
689
+ - **CartItem / CheckoutLineItem**: Nested structure `item.unitPrice`, `item.product.name`, `item.product.images`
690
+ - **`getCartTotals()`** works on all carts — guests now use server-side session carts with full `subtotal`/`discountAmount` fields.
691
+ - **`GuestCheckoutStartResponse`** is a union type — always check `result.tracked` before accessing `result.checkoutId`
692
+ - **`WaitForOrderResult`** has `result.status.orderNumber`, NOT `result.orderNumber`. But `completeGuestCheckout()` returns `GuestOrderResponse` which DOES have `result.orderNumber` directly.
693
+ - **Cart state**: Use `useState<Cart | null>(null)` and load with `smartGetCart()` in `useEffect` — all carts are server-side now, no hydration mismatch issues.
694
+
695
+ ---
696
+
697
+ ## Full SDK Documentation
698
+
699
+ For complete API reference and working code examples:
700
+ **https://brainerce.com/docs/sdk**