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