brainerce 1.0.1 → 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.
- package/AI_BUILDER_PROMPT.md +8 -28
- package/README.md +211 -608
- package/dist/index.d.mts +106 -98
- package/dist/index.d.ts +106 -98
- package/dist/index.js +384 -132
- package/dist/index.mjs +384 -132
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -123,15 +123,11 @@ const { data: products } = await client.getProducts();
|
|
|
123
123
|
|
|
124
124
|
> **AI Agents / Vibe-Coders:** Read this section carefully! These are common misunderstandings.
|
|
125
125
|
|
|
126
|
-
### 1. Guest Checkout -
|
|
126
|
+
### 1. Guest Checkout - Use `startGuestCheckout()` for Guests
|
|
127
127
|
|
|
128
|
-
**
|
|
128
|
+
**For guest users, use `startGuestCheckout()` which creates a checkout from the session cart:**
|
|
129
129
|
|
|
130
130
|
```typescript
|
|
131
|
-
// ❌ WRONG - Local cart ID "__local__" doesn't exist on server!
|
|
132
|
-
const cart = await client.smartGetCart(); // Returns { id: "__local__", ... }
|
|
133
|
-
const checkout = await client.createCheckout({ cartId: cart.id }); // 💥 ERROR: Cart not found
|
|
134
|
-
|
|
135
131
|
// ✅ CORRECT - Use startGuestCheckout() for guest users
|
|
136
132
|
const result = await client.startGuestCheckout();
|
|
137
133
|
if (result.tracked) {
|
|
@@ -145,7 +141,7 @@ const order = await client.submitGuestOrder();
|
|
|
145
141
|
|
|
146
142
|
**Rule of thumb:**
|
|
147
143
|
|
|
148
|
-
- Guest user +
|
|
144
|
+
- Guest user + Session cart → `startGuestCheckout()` or `submitGuestOrder()`
|
|
149
145
|
- Logged-in user + Server cart → `createCheckout({ cartId })`
|
|
150
146
|
|
|
151
147
|
### 2. ⛔ NEVER Create Local Interfaces - Use SDK Types!
|
|
@@ -381,13 +377,7 @@ const total = subtotal - discount;
|
|
|
381
377
|
- Cart field is `discountAmount`, NOT `discount`
|
|
382
378
|
- Cart has NO `total` field - use `getCartTotals()` or calculate
|
|
383
379
|
- Checkout DOES have a `total` field, but Cart does not
|
|
384
|
-
- `getCartTotals()`
|
|
385
|
-
```typescript
|
|
386
|
-
const subtotal = cart.items.reduce(
|
|
387
|
-
(sum, item) => sum + parseFloat(item.price || '0') * item.quantity,
|
|
388
|
-
0
|
|
389
|
-
);
|
|
390
|
-
```
|
|
380
|
+
- `getCartTotals()` works with all carts — guests now use server-side session carts with full pricing fields.
|
|
391
381
|
|
|
392
382
|
### 15. SearchSuggestions - Products Have `price`, Not `basePrice`
|
|
393
383
|
|
|
@@ -432,91 +422,61 @@ export default function SuccessPage() {
|
|
|
432
422
|
|
|
433
423
|
**Why is this needed?**
|
|
434
424
|
|
|
435
|
-
- `completeGuestCheckout()` sends the order to the server AND clears the
|
|
425
|
+
- `completeGuestCheckout()` sends the order to the server AND clears the session cart
|
|
436
426
|
- Without it, the order is never created on the server (payment goes through but no order!)
|
|
437
427
|
- For partial checkout (AliExpress-style), only the purchased items are removed
|
|
438
|
-
-
|
|
428
|
+
- After successful checkout, also call `client.onCheckoutComplete()` to clear the session cart reference
|
|
439
429
|
|
|
440
430
|
---
|
|
441
431
|
|
|
442
432
|
## Checkout: Guest vs Logged-In Customer
|
|
443
433
|
|
|
444
|
-
|
|
434
|
+
Both guests and logged-in customers now use the same `smart*` cart methods. The SDK handles server-side session carts for guests automatically.
|
|
445
435
|
|
|
446
|
-
| Customer Type | Cart
|
|
447
|
-
| ------------- |
|
|
448
|
-
| **Guest** |
|
|
449
|
-
| **Logged In** |
|
|
436
|
+
| Customer Type | Cart Method | Checkout |
|
|
437
|
+
| ------------- | ----------- | -------- |
|
|
438
|
+
| **Guest** | `smartAddToCart()` (session cart) | `startGuestCheckout()` → `createCheckout()` |
|
|
439
|
+
| **Logged In** | `smartAddToCart()` (server cart) | `createCheckout()` → `completeCheckout()` |
|
|
450
440
|
|
|
451
|
-
###
|
|
441
|
+
### Cart Usage (Same for Both)
|
|
452
442
|
|
|
453
443
|
```typescript
|
|
454
|
-
//
|
|
455
|
-
|
|
456
|
-
const checkout = await client.createCheckout({ cartId: cart.id }); // ERROR!
|
|
457
|
-
|
|
458
|
-
// The "__local__" ID is virtual - it doesn't exist on the server!
|
|
459
|
-
```
|
|
444
|
+
// Add to cart — works for both guests and logged-in users
|
|
445
|
+
await client.smartAddToCart({ productId: 'prod_123', quantity: 2 });
|
|
460
446
|
|
|
461
|
-
|
|
447
|
+
// Get cart
|
|
448
|
+
const cart = await client.smartGetCart();
|
|
462
449
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
const result = await client.startGuestCheckout();
|
|
450
|
+
// Update quantity
|
|
451
|
+
await client.smartUpdateCartItem('prod_123', 5);
|
|
466
452
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
const checkout = await client.getCheckout(result.checkoutId);
|
|
453
|
+
// Remove item
|
|
454
|
+
await client.smartRemoveFromCart('prod_123');
|
|
470
455
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
// ... Stripe payment ...
|
|
475
|
-
}
|
|
456
|
+
// Get totals (works for all carts)
|
|
457
|
+
import { getCartTotals } from 'brainerce';
|
|
458
|
+
const totals = getCartTotals(cart); // { subtotal, discount, shipping, total }
|
|
476
459
|
```
|
|
477
460
|
|
|
478
|
-
###
|
|
461
|
+
### On Login — Merge Guest Cart
|
|
479
462
|
|
|
480
463
|
```typescript
|
|
481
|
-
//
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
// Orders will be linked to their account
|
|
485
|
-
const order = await completeServerCheckout();
|
|
486
|
-
} else {
|
|
487
|
-
// ✅ Guest → Local Cart + submitGuestOrder
|
|
488
|
-
// Orders are standalone (not linked to any account)
|
|
489
|
-
const order = await client.submitGuestOrder();
|
|
490
|
-
}
|
|
464
|
+
// After setting customer token
|
|
465
|
+
client.setCustomerToken(token);
|
|
466
|
+
await client.syncCartOnLogin(); // Merges session cart into customer cart
|
|
491
467
|
```
|
|
492
468
|
|
|
493
|
-
### Guest Checkout
|
|
469
|
+
### Guest Checkout
|
|
494
470
|
|
|
495
471
|
```typescript
|
|
496
|
-
//
|
|
497
|
-
|
|
498
|
-
// Add to local cart (stored in localStorage)
|
|
499
|
-
client.addToLocalCart({
|
|
500
|
-
productId: products[0].id,
|
|
501
|
-
quantity: 1,
|
|
502
|
-
name: products[0].name,
|
|
503
|
-
price: String(products[0].basePrice),
|
|
504
|
-
});
|
|
505
|
-
|
|
506
|
-
// Set customer info
|
|
507
|
-
client.setLocalCartCustomer({ email: 'customer@example.com' });
|
|
508
|
-
client.setLocalCartShippingAddress({
|
|
509
|
-
firstName: 'John',
|
|
510
|
-
lastName: 'Doe',
|
|
511
|
-
line1: '123 Main St',
|
|
512
|
-
city: 'New York',
|
|
513
|
-
postalCode: '10001',
|
|
514
|
-
country: 'US',
|
|
515
|
-
});
|
|
472
|
+
// Guest checkout creates a checkout from the session cart
|
|
473
|
+
const result = await client.startGuestCheckout();
|
|
516
474
|
|
|
517
|
-
|
|
518
|
-
const
|
|
519
|
-
|
|
475
|
+
if (result.tracked) {
|
|
476
|
+
const checkout = await client.getCheckout(result.checkoutId);
|
|
477
|
+
await client.setShippingAddress(result.checkoutId, shippingAddress);
|
|
478
|
+
// ... continue with shipping rates, payment, etc.
|
|
479
|
+
}
|
|
520
480
|
```
|
|
521
481
|
|
|
522
482
|
### Logged-In Customer Checkout (orders linked to account)
|
|
@@ -525,27 +485,24 @@ console.log('Order created:', order.orderId);
|
|
|
525
485
|
// 1. Make sure customer token is set (after login)
|
|
526
486
|
client.setCustomerToken(authResponse.token);
|
|
527
487
|
|
|
528
|
-
// 2.
|
|
529
|
-
|
|
530
|
-
localStorage.setItem('cartId', cart.id);
|
|
531
|
-
|
|
532
|
-
// 3. Add items to server cart
|
|
533
|
-
await client.addToCart(cart.id, {
|
|
488
|
+
// 2. Add items to cart (smart methods handle server cart automatically)
|
|
489
|
+
await client.smartAddToCart({
|
|
534
490
|
productId: products[0].id,
|
|
535
491
|
quantity: 1,
|
|
536
492
|
});
|
|
537
493
|
|
|
538
|
-
//
|
|
494
|
+
// 3. Get cart and create checkout
|
|
495
|
+
const cart = await client.smartGetCart();
|
|
539
496
|
const checkout = await client.createCheckout({ cartId: cart.id });
|
|
540
497
|
|
|
541
|
-
//
|
|
498
|
+
// 4. Set customer info (REQUIRED - email is needed for order!)
|
|
542
499
|
await client.setCheckoutCustomer(checkout.id, {
|
|
543
500
|
email: 'customer@example.com',
|
|
544
501
|
firstName: 'John',
|
|
545
502
|
lastName: 'Doe',
|
|
546
503
|
});
|
|
547
504
|
|
|
548
|
-
//
|
|
505
|
+
// 5. Set shipping address
|
|
549
506
|
await client.setShippingAddress(checkout.id, {
|
|
550
507
|
firstName: 'John',
|
|
551
508
|
lastName: 'Doe',
|
|
@@ -555,11 +512,11 @@ await client.setShippingAddress(checkout.id, {
|
|
|
555
512
|
country: 'US',
|
|
556
513
|
});
|
|
557
514
|
|
|
558
|
-
//
|
|
515
|
+
// 6. Get shipping rates and select one
|
|
559
516
|
const rates = await client.getShippingRates(checkout.id);
|
|
560
517
|
await client.selectShippingMethod(checkout.id, rates[0].id);
|
|
561
518
|
|
|
562
|
-
//
|
|
519
|
+
// 7. Complete checkout - order is linked to customer!
|
|
563
520
|
const { orderId } = await client.completeCheckout(checkout.id);
|
|
564
521
|
console.log('Order created:', orderId);
|
|
565
522
|
|
|
@@ -570,62 +527,53 @@ console.log('Order created:', orderId);
|
|
|
570
527
|
|
|
571
528
|
---
|
|
572
529
|
|
|
573
|
-
##
|
|
574
|
-
|
|
575
|
-
### Option 1: Local Cart (Guest Users)
|
|
530
|
+
## Cart (Unified for All Users)
|
|
576
531
|
|
|
577
|
-
|
|
532
|
+
The SDK uses **server-side carts for all users**. Guests get automatic session carts; logged-in customers get server carts linked to their account.
|
|
578
533
|
|
|
579
|
-
- ✅
|
|
580
|
-
- ✅
|
|
581
|
-
- ✅
|
|
582
|
-
- ✅
|
|
534
|
+
- ✅ Cart persists across page refreshes (via session token in localStorage)
|
|
535
|
+
- ✅ Server-side pricing, discounts, and totals
|
|
536
|
+
- ✅ Automatic migration from guest → customer cart on login
|
|
537
|
+
- ✅ Same API for both guests and logged-in users
|
|
583
538
|
|
|
584
539
|
```typescript
|
|
585
|
-
// Add
|
|
586
|
-
client.
|
|
540
|
+
// Add to cart (guest or logged-in — same code!)
|
|
541
|
+
await client.smartAddToCart({ productId: 'prod_123', quantity: 2 });
|
|
587
542
|
|
|
588
|
-
//
|
|
589
|
-
const cart = client.
|
|
543
|
+
// Get cart
|
|
544
|
+
const cart = await client.smartGetCart();
|
|
590
545
|
console.log('Items:', cart.items.length);
|
|
546
|
+
console.log('Total:', getCartTotals(cart).total);
|
|
591
547
|
|
|
592
548
|
// Update quantity
|
|
593
|
-
client.
|
|
549
|
+
await client.smartUpdateCartItem('prod_123', 5);
|
|
594
550
|
|
|
595
551
|
// Remove item
|
|
596
|
-
client.
|
|
597
|
-
|
|
598
|
-
// At checkout - submit everything in ONE API call
|
|
599
|
-
const order = await client.submitGuestOrder();
|
|
552
|
+
await client.smartRemoveFromCart('prod_123');
|
|
600
553
|
```
|
|
601
554
|
|
|
602
|
-
###
|
|
603
|
-
|
|
604
|
-
For logged-in customers, **you MUST use server-side cart** to link orders to their account:
|
|
605
|
-
|
|
606
|
-
- ✅ Cart syncs across devices
|
|
607
|
-
- ✅ Abandoned cart recovery
|
|
608
|
-
- ✅ Orders linked to customer account
|
|
609
|
-
- ✅ Customer can see orders in "My Orders"
|
|
555
|
+
### After Login — Sync Cart
|
|
610
556
|
|
|
611
557
|
```typescript
|
|
612
|
-
// 1. Set customer token (after login)
|
|
613
558
|
client.setCustomerToken(token);
|
|
559
|
+
const mergedCart = await client.syncCartOnLogin();
|
|
560
|
+
// Guest session cart items are merged into the customer's server cart
|
|
561
|
+
```
|
|
614
562
|
|
|
615
|
-
|
|
616
|
-
const cart = await client.createCart();
|
|
617
|
-
localStorage.setItem('cartId', cart.id);
|
|
618
|
-
|
|
619
|
-
// 3. Add items
|
|
620
|
-
await client.addToCart(cart.id, { productId: 'prod_123', quantity: 2 });
|
|
563
|
+
### After Checkout — Clear Cart
|
|
621
564
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
//
|
|
625
|
-
const { orderId } = await client.completeCheckout(checkout.id);
|
|
565
|
+
```typescript
|
|
566
|
+
client.onCheckoutComplete();
|
|
567
|
+
// Clears session cart reference so next visit starts fresh
|
|
626
568
|
```
|
|
627
569
|
|
|
628
|
-
|
|
570
|
+
### After Logout — Preserve Guest Cart
|
|
571
|
+
|
|
572
|
+
```typescript
|
|
573
|
+
client.clearCustomerToken();
|
|
574
|
+
client.onLogout();
|
|
575
|
+
// Session cart is preserved — guest can continue browsing
|
|
576
|
+
```
|
|
629
577
|
|
|
630
578
|
---
|
|
631
579
|
|
|
@@ -642,29 +590,14 @@ export const client = new BrainerceClient({
|
|
|
642
590
|
connectionId: 'vc_YOUR_CONNECTION_ID', // Your Connection ID from Brainerce
|
|
643
591
|
});
|
|
644
592
|
|
|
645
|
-
// -----
|
|
646
|
-
|
|
647
|
-
export function getCartItemCount(): number {
|
|
648
|
-
return client.getLocalCartItemCount();
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
export function getCart() {
|
|
652
|
-
return client.getLocalCart();
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
// ----- For Registered Users (server cart) -----
|
|
656
|
-
|
|
657
|
-
export function getServerCartId(): string | null {
|
|
658
|
-
if (typeof window === 'undefined') return null;
|
|
659
|
-
return localStorage.getItem('cartId');
|
|
660
|
-
}
|
|
593
|
+
// ----- Cart Helpers -----
|
|
661
594
|
|
|
662
|
-
export function
|
|
663
|
-
|
|
595
|
+
export async function getCart() {
|
|
596
|
+
return client.smartGetCart();
|
|
664
597
|
}
|
|
665
598
|
|
|
666
|
-
export function
|
|
667
|
-
|
|
599
|
+
export function getCartItemCount(): number {
|
|
600
|
+
return client.getSmartCartItemCount();
|
|
668
601
|
}
|
|
669
602
|
|
|
670
603
|
// ----- Customer Token Helpers -----
|
|
@@ -1096,164 +1029,86 @@ function ProductDescription({ product }: { product: Product }) {
|
|
|
1096
1029
|
|
|
1097
1030
|
---
|
|
1098
1031
|
|
|
1099
|
-
###
|
|
1032
|
+
### Cart Operations (All Users)
|
|
1100
1033
|
|
|
1101
|
-
The
|
|
1034
|
+
The `smart*` methods work for both guests and logged-in users. Guests use server-side session carts; logged-in users use server carts linked to their account.
|
|
1102
1035
|
|
|
1103
|
-
#### Add to
|
|
1036
|
+
#### Add to Cart
|
|
1104
1037
|
|
|
1105
1038
|
```typescript
|
|
1106
|
-
|
|
1107
|
-
client.addToLocalCart({
|
|
1039
|
+
await client.smartAddToCart({
|
|
1108
1040
|
productId: 'prod_123',
|
|
1109
1041
|
variantId: 'var_456', // Optional: for products with variants
|
|
1110
1042
|
quantity: 2,
|
|
1111
|
-
name: 'Cool T-Shirt', // Optional: for cart display
|
|
1112
|
-
price: '29.99', // Optional: for cart display
|
|
1113
|
-
image: 'https://...', // Optional: for cart display
|
|
1114
1043
|
});
|
|
1115
1044
|
```
|
|
1116
1045
|
|
|
1117
|
-
#### Get
|
|
1046
|
+
#### Get Cart
|
|
1118
1047
|
|
|
1119
1048
|
```typescript
|
|
1120
|
-
const cart = client.
|
|
1049
|
+
const cart = await client.smartGetCart();
|
|
1121
1050
|
|
|
1122
|
-
console.log(cart.items);
|
|
1123
|
-
console.log(cart.
|
|
1124
|
-
console.log(cart.
|
|
1125
|
-
console.log(cart.couponCode); // Applied coupon (if any)
|
|
1051
|
+
console.log(cart.items); // Array of CartItem
|
|
1052
|
+
console.log(cart.itemCount); // Total item count
|
|
1053
|
+
console.log(cart.couponCode); // Applied coupon (if any)
|
|
1126
1054
|
```
|
|
1127
1055
|
|
|
1128
1056
|
#### Update Item Quantity
|
|
1129
1057
|
|
|
1130
1058
|
```typescript
|
|
1131
1059
|
// Set quantity to 5
|
|
1132
|
-
client.
|
|
1060
|
+
await client.smartUpdateCartItem('prod_123', 5);
|
|
1133
1061
|
|
|
1134
1062
|
// For variant products
|
|
1135
|
-
client.
|
|
1063
|
+
await client.smartUpdateCartItem('prod_123', 3, 'var_456');
|
|
1136
1064
|
|
|
1137
1065
|
// Set to 0 to remove
|
|
1138
|
-
client.
|
|
1066
|
+
await client.smartUpdateCartItem('prod_123', 0);
|
|
1139
1067
|
```
|
|
1140
1068
|
|
|
1141
1069
|
#### Remove Item
|
|
1142
1070
|
|
|
1143
1071
|
```typescript
|
|
1144
|
-
client.
|
|
1145
|
-
client.
|
|
1146
|
-
```
|
|
1147
|
-
|
|
1148
|
-
#### Clear Cart
|
|
1149
|
-
|
|
1150
|
-
```typescript
|
|
1151
|
-
client.clearLocalCart();
|
|
1072
|
+
await client.smartRemoveFromCart('prod_123');
|
|
1073
|
+
await client.smartRemoveFromCart('prod_123', 'var_456'); // With variant
|
|
1152
1074
|
```
|
|
1153
1075
|
|
|
1154
|
-
####
|
|
1076
|
+
#### Get Cart Item Count (No API Call)
|
|
1155
1077
|
|
|
1156
1078
|
```typescript
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
phone: '+1234567890', // Optional
|
|
1162
|
-
});
|
|
1163
|
-
```
|
|
1164
|
-
|
|
1165
|
-
#### Set Shipping Address
|
|
1166
|
-
|
|
1167
|
-
```typescript
|
|
1168
|
-
client.setLocalCartShippingAddress({
|
|
1169
|
-
firstName: 'John',
|
|
1170
|
-
lastName: 'Doe',
|
|
1171
|
-
line1: '123 Main St',
|
|
1172
|
-
line2: 'Apt 4B', // Optional
|
|
1173
|
-
city: 'New York',
|
|
1174
|
-
region: 'NY', // Optional: State/Province
|
|
1175
|
-
postalCode: '10001',
|
|
1176
|
-
country: 'US',
|
|
1177
|
-
phone: '+1234567890', // Optional
|
|
1178
|
-
});
|
|
1179
|
-
```
|
|
1180
|
-
|
|
1181
|
-
#### Set Billing Address (Optional)
|
|
1182
|
-
|
|
1183
|
-
```typescript
|
|
1184
|
-
client.setLocalCartBillingAddress({
|
|
1185
|
-
firstName: 'John',
|
|
1186
|
-
lastName: 'Doe',
|
|
1187
|
-
line1: '456 Business Ave',
|
|
1188
|
-
city: 'New York',
|
|
1189
|
-
postalCode: '10002',
|
|
1190
|
-
country: 'US',
|
|
1191
|
-
});
|
|
1079
|
+
// Returns cached count from session reference — instant, no API call
|
|
1080
|
+
const count = client.getSmartCartItemCount();
|
|
1081
|
+
console.log(`${count} items in cart`);
|
|
1082
|
+
// For accurate count, use: (await client.smartGetCart()).itemCount
|
|
1192
1083
|
```
|
|
1193
1084
|
|
|
1194
1085
|
#### Apply Coupon
|
|
1195
1086
|
|
|
1196
1087
|
```typescript
|
|
1197
|
-
client.
|
|
1088
|
+
const cart = await client.smartGetCart();
|
|
1089
|
+
const updated = await client.applyCoupon(cart.id, 'SAVE20');
|
|
1090
|
+
console.log(updated.discountAmount); // "10.00"
|
|
1091
|
+
console.log(updated.couponCode); // "SAVE20"
|
|
1198
1092
|
|
|
1199
1093
|
// Remove coupon
|
|
1200
|
-
client.
|
|
1201
|
-
```
|
|
1202
|
-
|
|
1203
|
-
#### Get Cart Item Count
|
|
1204
|
-
|
|
1205
|
-
```typescript
|
|
1206
|
-
const count = client.getLocalCartItemCount();
|
|
1207
|
-
console.log(`${count} items in cart`);
|
|
1094
|
+
await client.removeCoupon(cart.id);
|
|
1208
1095
|
```
|
|
1209
1096
|
|
|
1210
|
-
####
|
|
1097
|
+
#### Cart Totals
|
|
1211
1098
|
|
|
1212
1099
|
```typescript
|
|
1213
|
-
|
|
1214
|
-
items: LocalCartItem[];
|
|
1215
|
-
couponCode?: string;
|
|
1216
|
-
customer?: {
|
|
1217
|
-
email: string;
|
|
1218
|
-
firstName?: string;
|
|
1219
|
-
lastName?: string;
|
|
1220
|
-
phone?: string;
|
|
1221
|
-
};
|
|
1222
|
-
shippingAddress?: {
|
|
1223
|
-
firstName: string;
|
|
1224
|
-
lastName: string;
|
|
1225
|
-
line1: string;
|
|
1226
|
-
line2?: string;
|
|
1227
|
-
city: string;
|
|
1228
|
-
region?: string;
|
|
1229
|
-
postalCode: string;
|
|
1230
|
-
country: string;
|
|
1231
|
-
phone?: string;
|
|
1232
|
-
};
|
|
1233
|
-
billingAddress?: {
|
|
1234
|
-
/* same as shipping */
|
|
1235
|
-
};
|
|
1236
|
-
notes?: string;
|
|
1237
|
-
updatedAt: string;
|
|
1238
|
-
}
|
|
1100
|
+
import { getCartTotals } from 'brainerce';
|
|
1239
1101
|
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
quantity: number;
|
|
1244
|
-
name?: string;
|
|
1245
|
-
sku?: string;
|
|
1246
|
-
price?: string;
|
|
1247
|
-
image?: string;
|
|
1248
|
-
addedAt: string;
|
|
1249
|
-
}
|
|
1102
|
+
const cart = await client.smartGetCart();
|
|
1103
|
+
const totals = getCartTotals(cart);
|
|
1104
|
+
// { subtotal: 59.98, discount: 10, shipping: 5.99, total: 55.97 }
|
|
1250
1105
|
```
|
|
1251
1106
|
|
|
1252
1107
|
---
|
|
1253
1108
|
|
|
1254
1109
|
### Guest Checkout (Submit Order)
|
|
1255
1110
|
|
|
1256
|
-
|
|
1111
|
+
> **Note:** `startGuestCheckout()` is the preferred method for guest checkout — it creates a full checkout session from the session cart. `submitGuestOrder()` still works as a simpler alternative for basic orders.
|
|
1257
1112
|
|
|
1258
1113
|
```typescript
|
|
1259
1114
|
// Make sure cart has items, customer email, and shipping address
|
|
@@ -1279,7 +1134,7 @@ const order = await client.submitGuestOrder({ clearCartOnSuccess: false });
|
|
|
1279
1134
|
|
|
1280
1135
|
#### Create Order with Custom Data
|
|
1281
1136
|
|
|
1282
|
-
If you manage cart state yourself instead of using
|
|
1137
|
+
If you manage cart state yourself instead of using the smart cart methods:
|
|
1283
1138
|
|
|
1284
1139
|
```typescript
|
|
1285
1140
|
const order = await client.createGuestOrder({
|
|
@@ -1384,15 +1239,14 @@ type GuestCheckoutStartResponse =
|
|
|
1384
1239
|
|
|
1385
1240
|
---
|
|
1386
1241
|
|
|
1387
|
-
### Server Cart (
|
|
1242
|
+
### Server Cart (Low-Level API)
|
|
1388
1243
|
|
|
1389
|
-
|
|
1244
|
+
These low-level methods are available for advanced use cases. For most storefronts, use the `smart*` methods above instead.
|
|
1390
1245
|
|
|
1391
1246
|
#### Create Cart
|
|
1392
1247
|
|
|
1393
1248
|
```typescript
|
|
1394
1249
|
const cart = await client.createCart();
|
|
1395
|
-
setServerCartId(cart.id); // Save to localStorage
|
|
1396
1250
|
```
|
|
1397
1251
|
|
|
1398
1252
|
#### Get Cart
|
|
@@ -1715,12 +1569,13 @@ For vibe-coded sites, the SDK provides payment integration with Stripe and PayPa
|
|
|
1715
1569
|
Before creating a payment intent, you need a checkout ID. How you get it depends on the customer type:
|
|
1716
1570
|
|
|
1717
1571
|
```typescript
|
|
1718
|
-
// For GUEST users (
|
|
1572
|
+
// For GUEST users (session cart):
|
|
1719
1573
|
const result = await client.startGuestCheckout();
|
|
1720
1574
|
const checkoutId = result.checkoutId;
|
|
1721
1575
|
|
|
1722
1576
|
// For LOGGED-IN users (server cart):
|
|
1723
|
-
const
|
|
1577
|
+
const cart = await client.smartGetCart();
|
|
1578
|
+
const checkout = await client.createCheckout({ cartId: cart.id });
|
|
1724
1579
|
const checkoutId = checkout.id;
|
|
1725
1580
|
|
|
1726
1581
|
// Then continue with shipping and payment...
|
|
@@ -1941,11 +1796,12 @@ if (status.status === 'succeeded' && status.orderId) {
|
|
|
1941
1796
|
**How to get a checkout_id:**
|
|
1942
1797
|
|
|
1943
1798
|
```typescript
|
|
1944
|
-
// For GUEST users (
|
|
1799
|
+
// For GUEST users (session cart):
|
|
1945
1800
|
const result = await client.startGuestCheckout();
|
|
1946
1801
|
const checkoutId = result.checkoutId; // Use this!
|
|
1947
1802
|
|
|
1948
1803
|
// For LOGGED-IN users (server cart):
|
|
1804
|
+
const cart = await client.smartGetCart();
|
|
1949
1805
|
const checkout = await client.createCheckout({ cartId: cart.id });
|
|
1950
1806
|
const checkoutId = checkout.id; // Use this!
|
|
1951
1807
|
```
|
|
@@ -2058,7 +1914,7 @@ function PaymentForm({ checkoutId }: { checkoutId: string }) {
|
|
|
2058
1914
|
|
|
2059
1915
|
**CRITICAL:** After payment succeeds, you MUST call `completeGuestCheckout()` to create the order on the server and clear the cart.
|
|
2060
1916
|
|
|
2061
|
-
> **WARNING:** Do NOT use `handlePaymentSuccess()` - it only clears
|
|
1917
|
+
> **WARNING:** Do NOT use `handlePaymentSuccess()` - it only clears cart state locally
|
|
2062
1918
|
> and does NOT send the order to the server. Your customer will pay but no order will be created!
|
|
2063
1919
|
|
|
2064
1920
|
```typescript
|
|
@@ -2092,8 +1948,8 @@ export default function CheckoutSuccessPage() {
|
|
|
2092
1948
|
**How it works:**
|
|
2093
1949
|
| User Type | Cart Type | Behavior |
|
|
2094
1950
|
|-----------|-----------|----------|
|
|
2095
|
-
| Guest (partial checkout) |
|
|
2096
|
-
| Guest (full checkout) |
|
|
1951
|
+
| Guest (partial checkout) | Session cart | Creates order + removes only purchased items |
|
|
1952
|
+
| Guest (full checkout) | Session cart | Creates order + clears entire cart |
|
|
2097
1953
|
| Logged-in | Server cart | Creates order + clears cart via SDK state |
|
|
2098
1954
|
|
|
2099
1955
|
**Why is this needed?**
|
|
@@ -2452,10 +2308,7 @@ export default function LoginPage() {
|
|
|
2452
2308
|
// Social login handler
|
|
2453
2309
|
const handleSocialLogin = async (provider: string) => {
|
|
2454
2310
|
try {
|
|
2455
|
-
//
|
|
2456
|
-
const cartId = localStorage.getItem('cartId');
|
|
2457
|
-
if (cartId) sessionStorage.setItem('pendingCartId', cartId);
|
|
2458
|
-
|
|
2311
|
+
// Session cart is preserved automatically via localStorage session token
|
|
2459
2312
|
const { authorizationUrl, state } = await client.getOAuthAuthorizeUrl(
|
|
2460
2313
|
provider as 'GOOGLE' | 'FACEBOOK' | 'GITHUB',
|
|
2461
2314
|
{ redirectUrl: window.location.origin + '/auth/callback' }
|
|
@@ -2979,7 +2832,7 @@ export default function ProductsPage() {
|
|
|
2979
2832
|
}
|
|
2980
2833
|
```
|
|
2981
2834
|
|
|
2982
|
-
### Product Detail with Add to Cart
|
|
2835
|
+
### Product Detail with Add to Cart
|
|
2983
2836
|
|
|
2984
2837
|
```typescript
|
|
2985
2838
|
'use client';
|
|
@@ -3036,19 +2889,14 @@ export default function ProductPage({ params }: { params: { slug: string } }) {
|
|
|
3036
2889
|
return product.salePrice || product.basePrice;
|
|
3037
2890
|
};
|
|
3038
2891
|
|
|
3039
|
-
const handleAddToCart = () => {
|
|
2892
|
+
const handleAddToCart = async () => {
|
|
3040
2893
|
if (!product) return;
|
|
3041
2894
|
|
|
3042
|
-
// Add to
|
|
3043
|
-
client.
|
|
2895
|
+
// Add to cart (works for both guests and logged-in users)
|
|
2896
|
+
await client.smartAddToCart({
|
|
3044
2897
|
productId: product.id,
|
|
3045
2898
|
variantId: selectedVariant?.id,
|
|
3046
2899
|
quantity,
|
|
3047
|
-
name: selectedVariant?.name
|
|
3048
|
-
? `${product.name} - ${selectedVariant.name}`
|
|
3049
|
-
: product.name,
|
|
3050
|
-
price: getDisplayPrice(),
|
|
3051
|
-
image: getDisplayImage(),
|
|
3052
2900
|
});
|
|
3053
2901
|
|
|
3054
2902
|
alert('Added to cart!');
|
|
@@ -3170,39 +3018,35 @@ export default function ProductPage({ params }: { params: { slug: string } }) {
|
|
|
3170
3018
|
}
|
|
3171
3019
|
```
|
|
3172
3020
|
|
|
3173
|
-
### Cart Page
|
|
3021
|
+
### Cart Page
|
|
3174
3022
|
|
|
3175
3023
|
```typescript
|
|
3176
3024
|
'use client';
|
|
3177
3025
|
import { useState, useEffect } from 'react';
|
|
3178
3026
|
import { client } from '@/lib/brainerce';
|
|
3179
|
-
import
|
|
3027
|
+
import { getCartTotals, formatPrice } from 'brainerce';
|
|
3028
|
+
import type { Cart } from 'brainerce';
|
|
3180
3029
|
|
|
3181
3030
|
export default function CartPage() {
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
const [cart, setCart] = useState<LocalCart>({ items: [], updatedAt: '' });
|
|
3031
|
+
const [cart, setCart] = useState<Cart | null>(null);
|
|
3032
|
+
const [loading, setLoading] = useState(true);
|
|
3185
3033
|
|
|
3186
3034
|
useEffect(() => {
|
|
3187
|
-
|
|
3035
|
+
client.smartGetCart().then(setCart).finally(() => setLoading(false));
|
|
3188
3036
|
}, []);
|
|
3189
3037
|
|
|
3190
|
-
const updateQuantity = (productId: string, quantity: number, variantId?: string) => {
|
|
3191
|
-
const updated = client.
|
|
3038
|
+
const updateQuantity = async (productId: string, quantity: number, variantId?: string) => {
|
|
3039
|
+
const updated = await client.smartUpdateCartItem(productId, quantity, variantId);
|
|
3192
3040
|
setCart(updated);
|
|
3193
3041
|
};
|
|
3194
3042
|
|
|
3195
|
-
const removeItem = (productId: string, variantId?: string) => {
|
|
3196
|
-
const updated = client.
|
|
3043
|
+
const removeItem = async (productId: string, variantId?: string) => {
|
|
3044
|
+
const updated = await client.smartRemoveFromCart(productId, variantId);
|
|
3197
3045
|
setCart(updated);
|
|
3198
3046
|
};
|
|
3199
3047
|
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
return sum + (parseFloat(item.price || '0') * item.quantity);
|
|
3203
|
-
}, 0);
|
|
3204
|
-
|
|
3205
|
-
if (cart.items.length === 0) {
|
|
3048
|
+
if (loading) return <div>Loading cart...</div>;
|
|
3049
|
+
if (!cart || cart.items.length === 0) {
|
|
3206
3050
|
return (
|
|
3207
3051
|
<div className="text-center py-12">
|
|
3208
3052
|
<h1 className="text-2xl font-bold">Your cart is empty</h1>
|
|
@@ -3211,43 +3055,46 @@ export default function CartPage() {
|
|
|
3211
3055
|
);
|
|
3212
3056
|
}
|
|
3213
3057
|
|
|
3058
|
+
const totals = getCartTotals(cart);
|
|
3059
|
+
|
|
3214
3060
|
return (
|
|
3215
3061
|
<div>
|
|
3216
3062
|
<h1 className="text-2xl font-bold mb-6">Shopping Cart</h1>
|
|
3217
3063
|
|
|
3218
3064
|
{cart.items.map((item) => (
|
|
3219
|
-
<div key={
|
|
3065
|
+
<div key={item.id} className="flex items-center gap-4 py-4 border-b">
|
|
3220
3066
|
<img
|
|
3221
|
-
src={item.
|
|
3222
|
-
alt={item.name
|
|
3067
|
+
src={item.product.images?.[0] || '/placeholder.jpg'}
|
|
3068
|
+
alt={item.product.name}
|
|
3223
3069
|
className="w-20 h-20 object-cover"
|
|
3224
3070
|
/>
|
|
3225
3071
|
<div className="flex-1">
|
|
3226
|
-
<h3 className="font-medium">{item.name
|
|
3227
|
-
<p className="
|
|
3072
|
+
<h3 className="font-medium">{item.product.name}</h3>
|
|
3073
|
+
{item.variant && <p className="text-sm text-gray-500">{item.variant.name}</p>}
|
|
3074
|
+
<p className="font-bold">${item.unitPrice}</p>
|
|
3228
3075
|
</div>
|
|
3229
3076
|
<div className="flex items-center gap-2">
|
|
3230
3077
|
<button
|
|
3231
|
-
onClick={() => updateQuantity(item.productId, item.quantity - 1, item.variantId)}
|
|
3078
|
+
onClick={() => updateQuantity(item.productId, item.quantity - 1, item.variantId ?? undefined)}
|
|
3232
3079
|
className="w-8 h-8 border rounded"
|
|
3233
3080
|
>-</button>
|
|
3234
3081
|
<span className="w-8 text-center">{item.quantity}</span>
|
|
3235
3082
|
<button
|
|
3236
|
-
onClick={() => updateQuantity(item.productId, item.quantity + 1, item.variantId)}
|
|
3083
|
+
onClick={() => updateQuantity(item.productId, item.quantity + 1, item.variantId ?? undefined)}
|
|
3237
3084
|
className="w-8 h-8 border rounded"
|
|
3238
3085
|
>+</button>
|
|
3239
3086
|
</div>
|
|
3240
3087
|
<button
|
|
3241
|
-
onClick={() => removeItem(item.productId, item.variantId)}
|
|
3088
|
+
onClick={() => removeItem(item.productId, item.variantId ?? undefined)}
|
|
3242
3089
|
className="text-red-600"
|
|
3243
3090
|
>Remove</button>
|
|
3244
3091
|
</div>
|
|
3245
3092
|
))}
|
|
3246
3093
|
|
|
3247
3094
|
<div className="mt-6 text-right">
|
|
3248
|
-
<p className="text-xl">Subtotal: <strong>${subtotal.toFixed(2)}</strong></p>
|
|
3249
|
-
{
|
|
3250
|
-
<p className="text-green-600">
|
|
3095
|
+
<p className="text-xl">Subtotal: <strong>${totals.subtotal.toFixed(2)}</strong></p>
|
|
3096
|
+
{totals.discount > 0 && (
|
|
3097
|
+
<p className="text-green-600">Discount: -${totals.discount.toFixed(2)}</p>
|
|
3251
3098
|
)}
|
|
3252
3099
|
<a
|
|
3253
3100
|
href="/checkout"
|
|
@@ -3261,19 +3108,23 @@ export default function CartPage() {
|
|
|
3261
3108
|
}
|
|
3262
3109
|
```
|
|
3263
3110
|
|
|
3264
|
-
###
|
|
3111
|
+
### Checkout Page
|
|
3265
3112
|
|
|
3266
|
-
> **RECOMMENDED:** Use this pattern
|
|
3113
|
+
> **RECOMMENDED:** Use this unified pattern — the `smart*` methods handle both guest and logged-in users.
|
|
3267
3114
|
|
|
3268
3115
|
```typescript
|
|
3269
3116
|
'use client';
|
|
3270
3117
|
import { useState, useEffect } from 'react';
|
|
3271
|
-
import { client, isLoggedIn,
|
|
3118
|
+
import { client, isLoggedIn, restoreCustomerToken } from '@/lib/brainerce';
|
|
3119
|
+
import type { Checkout, ShippingRate } from 'brainerce';
|
|
3272
3120
|
|
|
3273
3121
|
export default function CheckoutPage() {
|
|
3274
3122
|
const [loading, setLoading] = useState(true);
|
|
3275
3123
|
const [submitting, setSubmitting] = useState(false);
|
|
3276
|
-
const [
|
|
3124
|
+
const [checkout, setCheckout] = useState<Checkout | null>(null);
|
|
3125
|
+
const [shippingRates, setShippingRates] = useState<ShippingRate[]>([]);
|
|
3126
|
+
const [selectedRate, setSelectedRate] = useState<string | null>(null);
|
|
3127
|
+
const customerLoggedIn = isLoggedIn();
|
|
3277
3128
|
|
|
3278
3129
|
// Form state
|
|
3279
3130
|
const [email, setEmail] = useState('');
|
|
@@ -3281,49 +3132,26 @@ export default function CheckoutPage() {
|
|
|
3281
3132
|
firstName: '', lastName: '', line1: '', city: '', postalCode: '', country: 'US'
|
|
3282
3133
|
});
|
|
3283
3134
|
|
|
3284
|
-
// Server checkout state (for logged-in customers)
|
|
3285
|
-
const [checkoutId, setCheckoutId] = useState<string | null>(null);
|
|
3286
|
-
const [shippingRates, setShippingRates] = useState<any[]>([]);
|
|
3287
|
-
const [selectedRate, setSelectedRate] = useState<string | null>(null);
|
|
3288
|
-
|
|
3289
3135
|
useEffect(() => {
|
|
3290
3136
|
restoreCustomerToken();
|
|
3291
|
-
const loggedIn = isLoggedIn();
|
|
3292
|
-
setCustomerLoggedIn(loggedIn);
|
|
3293
3137
|
|
|
3294
3138
|
async function initCheckout() {
|
|
3295
|
-
|
|
3296
|
-
//
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
for (const item of localCart.items) {
|
|
3308
|
-
await client.addToCart(cartId, {
|
|
3309
|
-
productId: item.productId,
|
|
3310
|
-
variantId: item.variantId,
|
|
3311
|
-
quantity: item.quantity,
|
|
3312
|
-
});
|
|
3139
|
+
try {
|
|
3140
|
+
// startGuestCheckout() creates checkout from session cart (for guests)
|
|
3141
|
+
// For logged-in users, create checkout from their server cart
|
|
3142
|
+
if (customerLoggedIn) {
|
|
3143
|
+
const cart = await client.smartGetCart();
|
|
3144
|
+
const co = await client.createCheckout({ cartId: cart.id });
|
|
3145
|
+
setCheckout(co);
|
|
3146
|
+
} else {
|
|
3147
|
+
const result = await client.startGuestCheckout();
|
|
3148
|
+
if (result.tracked) {
|
|
3149
|
+
const co = await client.getCheckout(result.checkoutId);
|
|
3150
|
+
setCheckout(co);
|
|
3313
3151
|
}
|
|
3314
|
-
client.clearLocalCart(); // Clear local cart after migration
|
|
3315
|
-
}
|
|
3316
|
-
|
|
3317
|
-
// Create checkout from server cart
|
|
3318
|
-
const checkout = await client.createCheckout({ cartId });
|
|
3319
|
-
setCheckoutId(checkout.id);
|
|
3320
|
-
|
|
3321
|
-
// Pre-fill from customer profile if available
|
|
3322
|
-
try {
|
|
3323
|
-
const profile = await client.getMyOrders({ limit: 1 }); // Just to check auth works
|
|
3324
|
-
} catch (e) {
|
|
3325
|
-
console.log('Could not fetch profile');
|
|
3326
3152
|
}
|
|
3153
|
+
} catch (err) {
|
|
3154
|
+
console.error('Failed to initialize checkout:', err);
|
|
3327
3155
|
}
|
|
3328
3156
|
setLoading(false);
|
|
3329
3157
|
}
|
|
@@ -3333,50 +3161,33 @@ export default function CheckoutPage() {
|
|
|
3333
3161
|
|
|
3334
3162
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
3335
3163
|
e.preventDefault();
|
|
3164
|
+
if (!checkout) return;
|
|
3336
3165
|
setSubmitting(true);
|
|
3337
3166
|
|
|
3338
3167
|
try {
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
firstName: shippingAddress.firstName,
|
|
3346
|
-
lastName: shippingAddress.lastName,
|
|
3347
|
-
});
|
|
3348
|
-
|
|
3349
|
-
// 2. Set shipping address
|
|
3350
|
-
await client.setShippingAddress(checkoutId, shippingAddress);
|
|
3351
|
-
|
|
3352
|
-
// 3. Get and select shipping rate
|
|
3353
|
-
const rates = await client.getShippingRates(checkoutId);
|
|
3354
|
-
if (rates.length > 0) {
|
|
3355
|
-
await client.selectShippingMethod(checkoutId, selectedRate || rates[0].id);
|
|
3356
|
-
}
|
|
3357
|
-
|
|
3358
|
-
// 4. Complete checkout - ORDER IS LINKED TO CUSTOMER!
|
|
3359
|
-
const { orderId } = await client.completeCheckout(checkoutId);
|
|
3360
|
-
|
|
3361
|
-
// Clear cart ID
|
|
3362
|
-
localStorage.removeItem('cartId');
|
|
3168
|
+
// 1. Set customer info
|
|
3169
|
+
await client.setCheckoutCustomer(checkout.id, {
|
|
3170
|
+
email,
|
|
3171
|
+
firstName: shippingAddress.firstName,
|
|
3172
|
+
lastName: shippingAddress.lastName,
|
|
3173
|
+
});
|
|
3363
3174
|
|
|
3364
|
-
|
|
3365
|
-
|
|
3175
|
+
// 2. Set shipping address
|
|
3176
|
+
await client.setShippingAddress(checkout.id, shippingAddress);
|
|
3366
3177
|
|
|
3367
|
-
|
|
3368
|
-
|
|
3178
|
+
// 3. Get and select shipping rate
|
|
3179
|
+
const rates = await client.getShippingRates(checkout.id);
|
|
3180
|
+
if (rates.length > 0) {
|
|
3181
|
+
await client.selectShippingMethod(checkout.id, selectedRate || rates[0].id);
|
|
3182
|
+
}
|
|
3369
3183
|
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
client.setLocalCartShippingAddress(shippingAddress);
|
|
3184
|
+
// 4. Complete checkout
|
|
3185
|
+
const { orderId } = await client.completeCheckout(checkout.id);
|
|
3373
3186
|
|
|
3374
|
-
|
|
3375
|
-
|
|
3187
|
+
// 5. Clean up
|
|
3188
|
+
client.onCheckoutComplete();
|
|
3376
3189
|
|
|
3377
|
-
|
|
3378
|
-
window.location.href = `/order-success?orderId=${order.orderId}`;
|
|
3379
|
-
}
|
|
3190
|
+
window.location.href = `/order-success?orderId=${orderId}`;
|
|
3380
3191
|
} catch (error) {
|
|
3381
3192
|
console.error('Checkout failed:', error);
|
|
3382
3193
|
alert('Checkout failed. Please try again.');
|
|
@@ -3389,31 +3200,19 @@ export default function CheckoutPage() {
|
|
|
3389
3200
|
|
|
3390
3201
|
return (
|
|
3391
3202
|
<form onSubmit={handleSubmit}>
|
|
3392
|
-
{/* Show email field only for guests */}
|
|
3393
3203
|
{!customerLoggedIn && (
|
|
3394
|
-
<input
|
|
3395
|
-
type="email"
|
|
3396
|
-
value={email}
|
|
3397
|
-
onChange={(e) => setEmail(e.target.value)}
|
|
3398
|
-
placeholder="Email"
|
|
3399
|
-
required
|
|
3400
|
-
/>
|
|
3204
|
+
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" required />
|
|
3401
3205
|
)}
|
|
3402
|
-
|
|
3403
|
-
{/* Shipping address fields */}
|
|
3404
3206
|
<input value={shippingAddress.firstName} onChange={(e) => setShippingAddress({...shippingAddress, firstName: e.target.value})} placeholder="First Name" required />
|
|
3405
3207
|
<input value={shippingAddress.lastName} onChange={(e) => setShippingAddress({...shippingAddress, lastName: e.target.value})} placeholder="Last Name" required />
|
|
3406
3208
|
<input value={shippingAddress.line1} onChange={(e) => setShippingAddress({...shippingAddress, line1: e.target.value})} placeholder="Address" required />
|
|
3407
3209
|
<input value={shippingAddress.city} onChange={(e) => setShippingAddress({...shippingAddress, city: e.target.value})} placeholder="City" required />
|
|
3408
3210
|
<input value={shippingAddress.postalCode} onChange={(e) => setShippingAddress({...shippingAddress, postalCode: e.target.value})} placeholder="Postal Code" required />
|
|
3409
3211
|
|
|
3410
|
-
{
|
|
3411
|
-
{customerLoggedIn && shippingRates.length > 0 && (
|
|
3212
|
+
{shippingRates.length > 0 && (
|
|
3412
3213
|
<select value={selectedRate || ''} onChange={(e) => setSelectedRate(e.target.value)}>
|
|
3413
3214
|
{shippingRates.map((rate) => (
|
|
3414
|
-
<option key={rate.id} value={rate.id}>
|
|
3415
|
-
{rate.name} - ${rate.price}
|
|
3416
|
-
</option>
|
|
3215
|
+
<option key={rate.id} value={rate.id}>{rate.name} - ${rate.price}</option>
|
|
3417
3216
|
))}
|
|
3418
3217
|
</select>
|
|
3419
3218
|
)}
|
|
@@ -3421,12 +3220,6 @@ export default function CheckoutPage() {
|
|
|
3421
3220
|
<button type="submit" disabled={submitting}>
|
|
3422
3221
|
{submitting ? 'Processing...' : 'Place Order'}
|
|
3423
3222
|
</button>
|
|
3424
|
-
|
|
3425
|
-
{customerLoggedIn && (
|
|
3426
|
-
<p className="text-sm text-green-600">
|
|
3427
|
-
✓ Logged in - Order will be saved to your account
|
|
3428
|
-
</p>
|
|
3429
|
-
)}
|
|
3430
3223
|
</form>
|
|
3431
3224
|
);
|
|
3432
3225
|
}
|
|
@@ -3434,207 +3227,21 @@ export default function CheckoutPage() {
|
|
|
3434
3227
|
|
|
3435
3228
|
> **Key Points:**
|
|
3436
3229
|
>
|
|
3437
|
-
> - `
|
|
3438
|
-
> -
|
|
3439
|
-
> -
|
|
3440
|
-
> -
|
|
3441
|
-
|
|
3442
|
-
---
|
|
3443
|
-
|
|
3444
|
-
### Guest Checkout (Single API Call)
|
|
3445
|
-
|
|
3446
|
-
This checkout is for **guest users only**. All cart data is in localStorage, and we submit it in one API call.
|
|
3447
|
-
|
|
3448
|
-
> **⚠️ WARNING:** Do NOT use this for logged-in customers! Use the Universal Checkout pattern above instead.
|
|
3449
|
-
|
|
3450
|
-
```typescript
|
|
3451
|
-
'use client';
|
|
3452
|
-
import { useState, useEffect } from 'react';
|
|
3453
|
-
import { client } from '@/lib/brainerce';
|
|
3454
|
-
import type { LocalCart, GuestOrderResponse } from 'brainerce';
|
|
3455
|
-
|
|
3456
|
-
type Step = 'info' | 'review' | 'complete';
|
|
3457
|
-
|
|
3458
|
-
export default function CheckoutPage() {
|
|
3459
|
-
// ⚠️ Do NOT use useState(client.getLocalCart()) — causes hydration mismatch!
|
|
3460
|
-
const [cart, setCart] = useState<LocalCart>({ items: [], updatedAt: '' });
|
|
3461
|
-
const [step, setStep] = useState<Step>('info');
|
|
3462
|
-
const [order, setOrder] = useState<GuestOrderResponse | null>(null);
|
|
3463
|
-
const [submitting, setSubmitting] = useState(false);
|
|
3464
|
-
const [error, setError] = useState('');
|
|
3465
|
-
|
|
3466
|
-
// Form state
|
|
3467
|
-
const [email, setEmail] = useState('');
|
|
3468
|
-
const [firstName, setFirstName] = useState('');
|
|
3469
|
-
const [lastName, setLastName] = useState('');
|
|
3470
|
-
const [address, setAddress] = useState('');
|
|
3471
|
-
const [city, setCity] = useState('');
|
|
3472
|
-
const [postalCode, setPostalCode] = useState('');
|
|
3473
|
-
const [country, setCountry] = useState('US');
|
|
3474
|
-
|
|
3475
|
-
// Load cart from localStorage after hydration
|
|
3476
|
-
useEffect(() => {
|
|
3477
|
-
const localCart = client.getLocalCart();
|
|
3478
|
-
setCart(localCart);
|
|
3479
|
-
if (localCart.customer?.email) setEmail(localCart.customer.email);
|
|
3480
|
-
if (localCart.customer?.firstName) setFirstName(localCart.customer.firstName);
|
|
3481
|
-
if (localCart.customer?.lastName) setLastName(localCart.customer.lastName);
|
|
3482
|
-
if (localCart.shippingAddress?.line1) setAddress(localCart.shippingAddress.line1);
|
|
3483
|
-
if (localCart.shippingAddress?.city) setCity(localCart.shippingAddress.city);
|
|
3484
|
-
if (localCart.shippingAddress?.postalCode) setPostalCode(localCart.shippingAddress.postalCode);
|
|
3485
|
-
if (localCart.shippingAddress?.country) setCountry(localCart.shippingAddress.country);
|
|
3486
|
-
}, []);
|
|
3487
|
-
|
|
3488
|
-
// Calculate subtotal
|
|
3489
|
-
const subtotal = cart.items.reduce((sum, item) => {
|
|
3490
|
-
return sum + (parseFloat(item.price || '0') * item.quantity);
|
|
3491
|
-
}, 0);
|
|
3492
|
-
|
|
3493
|
-
// Redirect if cart is empty
|
|
3494
|
-
useEffect(() => {
|
|
3495
|
-
if (cart.items.length === 0 && step !== 'complete') {
|
|
3496
|
-
window.location.href = '/cart';
|
|
3497
|
-
}
|
|
3498
|
-
}, [cart.items.length, step]);
|
|
3499
|
-
|
|
3500
|
-
const handleInfoSubmit = (e: React.FormEvent) => {
|
|
3501
|
-
e.preventDefault();
|
|
3502
|
-
|
|
3503
|
-
// Save to local cart
|
|
3504
|
-
client.setLocalCartCustomer({ email, firstName, lastName });
|
|
3505
|
-
client.setLocalCartShippingAddress({
|
|
3506
|
-
firstName,
|
|
3507
|
-
lastName,
|
|
3508
|
-
line1: address,
|
|
3509
|
-
city,
|
|
3510
|
-
postalCode,
|
|
3511
|
-
country,
|
|
3512
|
-
});
|
|
3513
|
-
|
|
3514
|
-
setStep('review');
|
|
3515
|
-
};
|
|
3516
|
-
|
|
3517
|
-
const handlePlaceOrder = async () => {
|
|
3518
|
-
setSubmitting(true);
|
|
3519
|
-
setError('');
|
|
3520
|
-
|
|
3521
|
-
try {
|
|
3522
|
-
// Single API call to create order!
|
|
3523
|
-
const result = await client.submitGuestOrder();
|
|
3524
|
-
setOrder(result);
|
|
3525
|
-
setStep('complete');
|
|
3526
|
-
} catch (err) {
|
|
3527
|
-
setError(err instanceof Error ? err.message : 'Failed to place order');
|
|
3528
|
-
} finally {
|
|
3529
|
-
setSubmitting(false);
|
|
3530
|
-
}
|
|
3531
|
-
};
|
|
3532
|
-
|
|
3533
|
-
if (step === 'complete' && order) {
|
|
3534
|
-
return (
|
|
3535
|
-
<div className="text-center py-12">
|
|
3536
|
-
<h1 className="text-3xl font-bold text-green-600">Order Complete!</h1>
|
|
3537
|
-
<p className="mt-4">Order Number: <strong>{order.orderNumber}</strong></p>
|
|
3538
|
-
<p className="mt-2">Total: <strong>${order.total.toFixed(2)}</strong></p>
|
|
3539
|
-
<p className="mt-4 text-gray-600">A confirmation email will be sent to {email}</p>
|
|
3540
|
-
<a href="/" className="mt-6 inline-block text-blue-600">Continue Shopping</a>
|
|
3541
|
-
</div>
|
|
3542
|
-
);
|
|
3543
|
-
}
|
|
3544
|
-
|
|
3545
|
-
return (
|
|
3546
|
-
<div className="max-w-2xl mx-auto">
|
|
3547
|
-
<h1 className="text-2xl font-bold mb-6">Checkout</h1>
|
|
3548
|
-
|
|
3549
|
-
{error && (
|
|
3550
|
-
<div className="bg-red-100 text-red-600 p-3 rounded mb-4">{error}</div>
|
|
3551
|
-
)}
|
|
3552
|
-
|
|
3553
|
-
{step === 'info' && (
|
|
3554
|
-
<form onSubmit={handleInfoSubmit} className="space-y-4">
|
|
3555
|
-
<h2 className="text-lg font-bold">Contact Information</h2>
|
|
3556
|
-
<input
|
|
3557
|
-
type="email"
|
|
3558
|
-
placeholder="Email"
|
|
3559
|
-
value={email}
|
|
3560
|
-
onChange={e => setEmail(e.target.value)}
|
|
3561
|
-
required
|
|
3562
|
-
className="w-full border p-2 rounded"
|
|
3563
|
-
/>
|
|
3564
|
-
|
|
3565
|
-
<h2 className="text-lg font-bold mt-6">Shipping Address</h2>
|
|
3566
|
-
<div className="grid grid-cols-2 gap-4">
|
|
3567
|
-
<input placeholder="First Name" value={firstName} onChange={e => setFirstName(e.target.value)} required className="border p-2 rounded" />
|
|
3568
|
-
<input placeholder="Last Name" value={lastName} onChange={e => setLastName(e.target.value)} required className="border p-2 rounded" />
|
|
3569
|
-
</div>
|
|
3570
|
-
<input placeholder="Address" value={address} onChange={e => setAddress(e.target.value)} required className="w-full border p-2 rounded" />
|
|
3571
|
-
<div className="grid grid-cols-2 gap-4">
|
|
3572
|
-
<input placeholder="City" value={city} onChange={e => setCity(e.target.value)} required className="border p-2 rounded" />
|
|
3573
|
-
<input placeholder="Postal Code" value={postalCode} onChange={e => setPostalCode(e.target.value)} required className="border p-2 rounded" />
|
|
3574
|
-
</div>
|
|
3575
|
-
<select value={country} onChange={e => setCountry(e.target.value)} className="w-full border p-2 rounded">
|
|
3576
|
-
<option value="US">United States</option>
|
|
3577
|
-
<option value="IL">Israel</option>
|
|
3578
|
-
<option value="GB">United Kingdom</option>
|
|
3579
|
-
</select>
|
|
3580
|
-
|
|
3581
|
-
<button type="submit" className="w-full bg-black text-white py-3 rounded">
|
|
3582
|
-
Review Order
|
|
3583
|
-
</button>
|
|
3584
|
-
</form>
|
|
3585
|
-
)}
|
|
3586
|
-
|
|
3587
|
-
{step === 'review' && (
|
|
3588
|
-
<div className="space-y-6">
|
|
3589
|
-
{/* Order Summary */}
|
|
3590
|
-
<div className="border p-4 rounded">
|
|
3591
|
-
<h3 className="font-bold mb-4">Order Summary</h3>
|
|
3592
|
-
{cart.items.map((item) => (
|
|
3593
|
-
<div key={`${item.productId}-${item.variantId || ''}`} className="flex justify-between py-2">
|
|
3594
|
-
<span>{item.name} x {item.quantity}</span>
|
|
3595
|
-
<span>${(parseFloat(item.price || '0') * item.quantity).toFixed(2)}</span>
|
|
3596
|
-
</div>
|
|
3597
|
-
))}
|
|
3598
|
-
<hr className="my-2" />
|
|
3599
|
-
<div className="flex justify-between font-bold">
|
|
3600
|
-
<span>Total</span>
|
|
3601
|
-
<span>${subtotal.toFixed(2)}</span>
|
|
3602
|
-
</div>
|
|
3603
|
-
</div>
|
|
3604
|
-
|
|
3605
|
-
{/* Shipping Info */}
|
|
3606
|
-
<div className="border p-4 rounded">
|
|
3607
|
-
<h3 className="font-bold mb-2">Shipping To</h3>
|
|
3608
|
-
<p>{firstName} {lastName}</p>
|
|
3609
|
-
<p>{address}</p>
|
|
3610
|
-
<p>{city}, {postalCode}, {country}</p>
|
|
3611
|
-
<button onClick={() => setStep('info')} className="text-blue-600 text-sm mt-2">Edit</button>
|
|
3612
|
-
</div>
|
|
3613
|
-
|
|
3614
|
-
<button
|
|
3615
|
-
onClick={handlePlaceOrder}
|
|
3616
|
-
disabled={submitting}
|
|
3617
|
-
className="w-full bg-green-600 text-white py-3 rounded text-lg"
|
|
3618
|
-
>
|
|
3619
|
-
{submitting ? 'Processing...' : 'Place Order'}
|
|
3620
|
-
</button>
|
|
3621
|
-
</div>
|
|
3622
|
-
)}
|
|
3623
|
-
</div>
|
|
3624
|
-
);
|
|
3625
|
-
}
|
|
3626
|
-
```
|
|
3230
|
+
> - Both guests and logged-in users go through `createCheckout()` → `completeCheckout()`
|
|
3231
|
+
> - Guest session cart is created automatically by `smart*` methods
|
|
3232
|
+
> - Call `client.onCheckoutComplete()` after successful payment to clear the session cart
|
|
3233
|
+
> - Call `client.syncCartOnLogin()` when a user logs in to merge their guest cart
|
|
3627
3234
|
|
|
3628
3235
|
### Multi-Step Checkout (Server Cart - For Logged-In Customers Only)
|
|
3629
3236
|
|
|
3630
|
-
> **IMPORTANT:** This checkout pattern is ONLY for logged-in customers. For a checkout page that handles both guests and logged-in customers, see the "
|
|
3237
|
+
> **IMPORTANT:** This checkout pattern is ONLY for logged-in customers. For a checkout page that handles both guests and logged-in customers, see the "Checkout Page" example above.
|
|
3631
3238
|
|
|
3632
3239
|
For logged-in users with server-side cart - orders will be linked to their account:
|
|
3633
3240
|
|
|
3634
3241
|
```typescript
|
|
3635
3242
|
'use client';
|
|
3636
3243
|
import { useEffect, useState } from 'react';
|
|
3637
|
-
import { client
|
|
3244
|
+
import { client } from '@/lib/brainerce';
|
|
3638
3245
|
import type { Checkout, ShippingRate } from 'brainerce';
|
|
3639
3246
|
|
|
3640
3247
|
type Step = 'customer' | 'shipping' | 'payment' | 'complete';
|
|
@@ -3657,13 +3264,13 @@ export default function CheckoutPage() {
|
|
|
3657
3264
|
|
|
3658
3265
|
useEffect(() => {
|
|
3659
3266
|
async function initCheckout() {
|
|
3660
|
-
const cartId = getServerCartId();
|
|
3661
|
-
if (!cartId) {
|
|
3662
|
-
window.location.href = '/cart';
|
|
3663
|
-
return;
|
|
3664
|
-
}
|
|
3665
3267
|
try {
|
|
3666
|
-
const
|
|
3268
|
+
const cart = await client.smartGetCart();
|
|
3269
|
+
if (!cart || cart.items.length === 0) {
|
|
3270
|
+
window.location.href = '/cart';
|
|
3271
|
+
return;
|
|
3272
|
+
}
|
|
3273
|
+
const c = await client.createCheckout({ cartId: cart.id });
|
|
3667
3274
|
setCheckout(c);
|
|
3668
3275
|
} finally {
|
|
3669
3276
|
setLoading(false);
|
|
@@ -3995,7 +3602,7 @@ export default function AccountPage() {
|
|
|
3995
3602
|
}
|
|
3996
3603
|
```
|
|
3997
3604
|
|
|
3998
|
-
### Header Component with Cart Count
|
|
3605
|
+
### Header Component with Cart Count
|
|
3999
3606
|
|
|
4000
3607
|
```typescript
|
|
4001
3608
|
'use client';
|
|
@@ -4008,8 +3615,8 @@ export function Header() {
|
|
|
4008
3615
|
|
|
4009
3616
|
useEffect(() => {
|
|
4010
3617
|
setLoggedIn(isLoggedIn());
|
|
4011
|
-
// Get cart count from
|
|
4012
|
-
setCartCount(client.
|
|
3618
|
+
// Get cart count from session reference (no API call!)
|
|
3619
|
+
setCartCount(client.getSmartCartItemCount());
|
|
4013
3620
|
}, []);
|
|
4014
3621
|
|
|
4015
3622
|
return (
|
|
@@ -4124,16 +3731,12 @@ import type {
|
|
|
4124
3731
|
ProductQueryParams,
|
|
4125
3732
|
PaginatedResponse,
|
|
4126
3733
|
|
|
4127
|
-
//
|
|
4128
|
-
LocalCart,
|
|
4129
|
-
LocalCartItem,
|
|
4130
|
-
CreateGuestOrderDto,
|
|
4131
|
-
GuestOrderResponse,
|
|
4132
|
-
|
|
4133
|
-
// Server Cart (Registered Users)
|
|
3734
|
+
// Cart (All Users)
|
|
4134
3735
|
Cart,
|
|
4135
3736
|
CartItem,
|
|
4136
3737
|
AddToCartDto,
|
|
3738
|
+
CreateGuestOrderDto,
|
|
3739
|
+
GuestOrderResponse,
|
|
4137
3740
|
|
|
4138
3741
|
// Checkout
|
|
4139
3742
|
Checkout,
|
|
@@ -4246,7 +3849,7 @@ import { BrainerceError } from 'brainerce';
|
|
|
4246
3849
|
// Add to cart with toast feedback
|
|
4247
3850
|
const handleAddToCart = async (productId: string, quantity: number) => {
|
|
4248
3851
|
try {
|
|
4249
|
-
client.
|
|
3852
|
+
await client.smartAddToCart({ productId, quantity });
|
|
4250
3853
|
toast.success('Added to cart!');
|
|
4251
3854
|
} catch (error) {
|
|
4252
3855
|
if (error instanceof BrainerceError) {
|