omni-sync-sdk 0.22.1 → 0.22.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,548 +1,563 @@
1
- # OmniSync 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 omni.smartGetCart(); // Guest cart has id: "__local__"
14
- await omni.createCheckout({ cartId: cart.id }); // 💥 "__local__" doesn't exist on server!
15
-
16
- // ✅ CORRECT - Check user type first!
17
- if (omni.isCustomerLoggedIn()) {
18
- // Logged-in user → server cart exists
19
- const checkout = await omni.createCheckout({ cartId: cart.id });
20
- const checkoutId = checkout.id;
21
- } else {
22
- // Guest user → use startGuestCheckout()
23
- const result = await omni.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
- omni.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 omni.getProducts();
64
- ```
65
-
66
- ---
67
-
68
- ## Quick Setup
69
-
70
- ```bash
71
- npm install omni-sync-sdk
72
- ```
73
-
74
- ```typescript
75
- // lib/omni-sync.ts
76
- import { OmniSyncClient } from 'omni-sync-sdk';
77
-
78
- export const omni = new OmniSyncClient({
79
- connectionId: '{connection_id}',
80
- baseUrl: '{api_url}',
81
- });
82
-
83
- // Restore customer session on page load
84
- export function initOmniSync() {
85
- if (typeof window === 'undefined') return;
86
- const token = localStorage.getItem('customerToken');
87
- if (token) omni.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
- omni.setCustomerToken(token);
95
- } else {
96
- localStorage.removeItem('customerToken');
97
- omni.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 omni.smartGetCart();
109
-
110
- // Add to cart
111
- await omni.smartAddToCart({
112
- productId: 'prod_xxx',
113
- variantId: 'var_xxx', // optional, for products with variants
114
- quantity: 1,
115
- });
116
-
117
- // Update quantity (by productId, not itemId!)
118
- await omni.smartUpdateCartItem('prod_xxx', 2); // productId, quantity
119
- await omni.smartUpdateCartItem('prod_xxx', 3, 'var_xxx'); // with variant
120
-
121
- // Remove item (by productId, not itemId!)
122
- await omni.smartRemoveFromCart('prod_xxx');
123
- await omni.smartRemoveFromCart('prod_xxx', 'var_xxx'); // with variant
124
-
125
- // Get cart totals (cart doesn't have .total field!)
126
- import { getCartTotals } from 'omni-sync-sdk';
127
- const totals = getCartTotals(cart);
128
- // { subtotal: 59.98, discount: 10, shipping: 0, total: 49.98 }
129
-
130
- // ⚠️ LocalCart vs Cart - KEY DIFFERENCES:
131
- // Server Cart has: id, itemCount, subtotal, discountAmount
132
- // Guest LocalCart has NONE of these! Only: items, couponCode, customer
133
- // To check type: if ('id' in cart) { /* server Cart */ } else { /* LocalCart */ }
134
- // Item count for both: cart.items.length
135
- ```
136
-
137
- ---
138
-
139
- ## 🛒 Partial Checkout (AliExpress Style) - REQUIRED!
140
-
141
- Cart page MUST have checkboxes so users can select which items to buy:
142
-
143
- ```typescript
144
- // Cart page - track selected items
145
- const [selectedIndices, setSelectedIndices] = useState<number[]>(
146
- cart.items.map((_, i) => i) // All selected by default
147
- );
148
-
149
- const toggleItem = (index: number) => {
150
- setSelectedIndices(prev =>
151
- prev.includes(index)
152
- ? prev.filter(i => i !== index)
153
- : [...prev, index]
154
- );
155
- };
156
-
157
- const toggleAll = () => {
158
- if (selectedIndices.length === cart.items.length) {
159
- setSelectedIndices([]); // Deselect all
160
- } else {
161
- setSelectedIndices(cart.items.map((_, i) => i)); // Select all
162
- }
163
- };
164
-
165
- // In your cart UI:
166
- <div>
167
- <label>
168
- <input
169
- type="checkbox"
170
- checked={selectedIndices.length === cart.items.length}
171
- onChange={toggleAll}
172
- />
173
- Select All
174
- </label>
175
- </div>
176
-
177
- {cart.items.map((item, index) => (
178
- <div key={index}>
179
- <input
180
- type="checkbox"
181
- checked={selectedIndices.includes(index)}
182
- onChange={() => toggleItem(index)}
183
- />
184
- {/* ... item details ... */}
185
- </div>
186
- ))}
187
-
188
- // On checkout button - pass selected items!
189
- const handleCheckout = async () => {
190
- if (selectedIndices.length === 0) {
191
- alert('Please select items to checkout');
192
- return;
193
- }
194
-
195
- const result = await omni.startGuestCheckout({ selectedIndices });
196
- // Only selected items go to checkout, others stay in cart!
197
- };
198
- ```
199
-
200
- **Why this matters:**
201
-
202
- - Users can buy some items now, leave others for later
203
- - After payment, `completeGuestCheckout()` sends the order and only removes purchased items
204
- - Remaining items stay in cart for future purchase
205
-
206
- **⚠️ Order Summary on Checkout Page - Use checkout.lineItems!**
207
-
208
- ```typescript
209
- // ❌ WRONG - Shows ALL cart items (even unselected ones!)
210
- <div className="order-summary">
211
- {cart.items.map(item => (
212
- <div>{item.product.name} - ${item.price}</div>
213
- ))}
214
- </div>
215
-
216
- // ✅ CORRECT - Shows only items being purchased in this checkout
217
- <div className="order-summary">
218
- {checkout.lineItems.map(item => (
219
- <div>{item.product.name} - ${item.price}</div>
220
- ))}
221
- </div>
222
- ```
223
-
224
- The `checkout` object's `lineItems` array contains ONLY the items selected for this checkout!
225
-
226
- ---
227
-
228
- ## Complete Checkout Flow
229
-
230
- ### Step 1: Start Checkout (Different for Guest vs Logged-in!)
231
-
232
- ```typescript
233
- async function startCheckout() {
234
- const cart = await omni.smartGetCart();
235
-
236
- if (cart.items.length === 0) {
237
- alert('Cart is empty');
238
- return;
239
- }
240
-
241
- let checkoutId: string;
242
-
243
- if (omni.isCustomerLoggedIn()) {
244
- // Logged-in: create checkout from server cart
245
- const checkout = await omni.createCheckout({ cartId: cart.id });
246
- checkoutId = checkout.id;
247
- } else {
248
- // Guest: use startGuestCheckout (syncs local cart to server)
249
- const result = await omni.startGuestCheckout();
250
- if (!result.tracked || !result.checkoutId) {
251
- throw new Error('Failed to create checkout');
252
- }
253
- checkoutId = result.checkoutId;
254
- }
255
-
256
- // Save for payment page
257
- localStorage.setItem('checkoutId', checkoutId);
258
-
259
- // Navigate to checkout
260
- window.location.href = '/checkout';
261
- }
262
- ```
263
-
264
- ### Step 2: Shipping Address
265
-
266
- ```typescript
267
- const checkoutId = localStorage.getItem('checkoutId')!;
268
-
269
- // Set shipping address (email is required!)
270
- const { checkout, rates } = await omni.setShippingAddress(checkoutId, {
271
- email: 'customer@example.com',
272
- firstName: 'John',
273
- lastName: 'Doe',
274
- line1: '123 Main St',
275
- city: 'New York',
276
- region: 'NY', // ⚠️ Use 'region', NOT 'state'!
277
- postalCode: '10001',
278
- country: 'US',
279
- });
280
-
281
- // Show available shipping rates
282
- rates.forEach((rate) => {
283
- console.log(`${rate.name}: $${rate.price}`);
284
- });
285
- ```
286
-
287
- ### Step 3: Select Shipping Method
288
-
289
- ```typescript
290
- await omni.selectShippingMethod(checkoutId, selectedRateId);
291
- ```
292
-
293
- ### Step 4: Payment with Stripe
294
-
295
- ```typescript
296
- // Install: npm install @stripe/stripe-js @stripe/react-stripe-js
297
-
298
- // 1. Check if payment is configured
299
- const { hasPayments, providers } = await omni.getPaymentProviders();
300
- if (!hasPayments) {
301
- return <div>Payment not configured for this store</div>;
302
- }
303
-
304
- // 2. Get Stripe config
305
- const stripeProvider = providers.find(p => p.provider === 'stripe');
306
- if (!stripeProvider) {
307
- return <div>Stripe not configured</div>;
308
- }
309
-
310
- // 3. Create payment intent
311
- const intent = await omni.createPaymentIntent(checkoutId);
312
-
313
- // 4. Initialize Stripe
314
- import { loadStripe } from '@stripe/stripe-js';
315
- const stripe = await loadStripe(stripeProvider.publicKey, {
316
- stripeAccount: stripeProvider.stripeAccountId,
317
- });
318
-
319
- // 5. Confirm payment (in your payment form)
320
- const { error } = await stripe.confirmPayment({
321
- elements,
322
- confirmParams: {
323
- return_url: `${window.location.origin}/checkout/success?checkout_id=${checkoutId}`,
324
- },
325
- });
326
-
327
- if (error) {
328
- setError(error.message);
329
- }
330
- // If no error, Stripe redirects to success page
331
- ```
332
-
333
- ### Step 5: Success Page (Complete Order & Clear Cart!)
334
-
335
- ```typescript
336
- // /checkout/success/page.tsx
337
- 'use client';
338
- import { useEffect, useState } from 'react';
339
- import { omni } from '@/lib/omni-sync';
340
-
341
- export default function CheckoutSuccessPage() {
342
- const [orderNumber, setOrderNumber] = useState<string>();
343
- const [loading, setLoading] = useState(true);
344
-
345
- useEffect(() => {
346
- const checkoutId = new URLSearchParams(window.location.search).get('checkout_id');
347
-
348
- if (checkoutId) {
349
- // ⚠️ CRITICAL: Complete the order on the server AND clear the cart!
350
- // Do NOT use handlePaymentSuccess() - it only clears localStorage!
351
- omni.completeGuestCheckout(checkoutId).then(result => {
352
- setOrderNumber(result.orderNumber);
353
- setLoading(false);
354
- }).catch(() => {
355
- // Order may already be completed (e.g., page refresh) - check status
356
- omni.getPaymentStatus(checkoutId).then(status => {
357
- if (status.orderNumber) {
358
- setOrderNumber(status.orderNumber);
359
- }
360
- setLoading(false);
361
- });
362
- });
363
- }
364
- }, []);
365
-
366
- return (
367
- <div className="text-center py-12">
368
- <h1 className="text-2xl font-bold text-green-600">Thank you for your order!</h1>
369
- {loading && <p className="mt-2">Processing your order...</p>}
370
- {orderNumber && <p className="mt-2">Order #{orderNumber}</p>}
371
- <p className="mt-4">A confirmation email will be sent shortly.</p>
372
- </div>
373
- );
374
- }
375
- ```
376
-
377
- ---
378
-
379
- ## Partial Checkout (AliExpress Style)
380
-
381
- Allow customers to buy only some items from their cart:
382
-
383
- ```typescript
384
- // Start checkout with only selected items (by index)
385
- const result = await omni.startGuestCheckout({
386
- selectedIndices: [0, 2], // Buy items at index 0 and 2 only
387
- });
388
-
389
- // After payment, completeGuestCheckout() sends the order AND removes only those items!
390
- // Other items stay in cart.
391
- ```
392
-
393
- ---
394
-
395
- ## Products API
396
-
397
- ```typescript
398
- // List products with pagination
399
- const { data: products, meta } = await omni.getProducts({
400
- page: 1,
401
- limit: 20,
402
- search: 'blue shirt', // Searches name, description, SKU, categories, tags
403
- });
404
- // meta = { page: 1, limit: 20, total: 150, totalPages: 8 }
405
-
406
- // Get single product by slug (for product detail page)
407
- const product = await omni.getProductBySlug('blue-cotton-shirt');
408
-
409
- // Search suggestions (for autocomplete)
410
- const suggestions = await omni.getSearchSuggestions('blue', 5);
411
- // { products: [...], categories: [...] }
412
- ```
413
-
414
- ---
415
-
416
- ## Product Custom Fields (Metafields)
417
-
418
- Products may have custom fields defined by the store owner (e.g., "Material", "Care Instructions", "Warranty").
419
-
420
- ```typescript
421
- import { getProductMetafield, getProductMetafieldValue } from 'omni-sync-sdk';
422
-
423
- // Access metafields on a product
424
- const product = await omni.getProductBySlug('blue-shirt');
425
-
426
- // Get all custom fields
427
- product.metafields?.forEach((field) => {
428
- console.log(`${field.definitionName}: ${field.value}`);
429
- });
430
-
431
- // Get specific field by key
432
- const material = getProductMetafieldValue(product, 'material');
433
- const careInstructions = getProductMetafield(product, 'care_instructions');
434
-
435
- // Get available metafield definitions (schema)
436
- const { definitions } = await omni.getPublicMetafieldDefinitions();
437
- // Use definitions to build dynamic UI (filters, forms, etc.)
438
- ```
439
-
440
- > **Tip:** `metafields` may be empty if the store hasn't defined custom fields. Always use optional chaining.
441
-
442
- ---
443
-
444
- ## Customer Authentication
445
-
446
- ```typescript
447
- // Register
448
- const { customer, token } = await omni.registerCustomer({
449
- email: 'john@example.com',
450
- password: 'securepass123',
451
- firstName: 'John',
452
- lastName: 'Doe',
453
- });
454
- setCustomerToken(token);
455
-
456
- // Login
457
- const { customer, token } = await omni.loginCustomer('john@example.com', 'password');
458
- setCustomerToken(token);
459
-
460
- // Logout
461
- setCustomerToken(null);
462
-
463
- // Get profile (requires token)
464
- const profile = await omni.getMyProfile();
465
-
466
- // Get order history
467
- const { data: orders, meta } = await omni.getMyOrders({ page: 1, limit: 10 });
468
- ```
469
-
470
- ---
471
-
472
- ## OAuth / Social Login
473
-
474
- ```typescript
475
- // Get available providers for this store
476
- const { providers } = await omni.getAvailableOAuthProviders();
477
- // providers = ['GOOGLE', 'FACEBOOK', 'GITHUB']
478
-
479
- // Redirect to OAuth provider
480
- const { authorizationUrl } = await omni.getOAuthAuthorizeUrl('GOOGLE', {
481
- redirectUrl: `${window.location.origin}/auth/callback`,
482
- });
483
- window.location.href = authorizationUrl;
484
-
485
- // Handle callback (on /auth/callback page)
486
- const code = new URLSearchParams(window.location.search).get('code');
487
- const state = new URLSearchParams(window.location.search).get('state');
488
- const { customer, token } = await omni.handleOAuthCallback('GOOGLE', code!, state!);
489
- setCustomerToken(token);
490
- ```
491
-
492
- ---
493
-
494
- ## Required Pages Checklist
495
-
496
- - [ ] **Home** (`/`) - Featured products grid
497
- - [ ] **Products** (`/products`) - Product list with infinite scroll
498
- - [ ] **Product Detail** (`/products/[slug]`) - Use `getProductBySlug(slug)`
499
- - [ ] **Cart** (`/cart`) - Show items, quantities, totals
500
- - [ ] **Checkout** (`/checkout`) - Address → Shipping → Payment
501
- - [ ] **Success** (`/checkout/success`) - **Must call `completeGuestCheckout()`!**
502
- - [ ] **Login** (`/login`) - Email/password + social buttons
503
- - [ ] **Register** (`/register`) - Registration form
504
- - [ ] **Account** (`/account`) - Profile + order history
505
- - [ ] **Header** - Logo, nav, cart icon with count, search
506
-
507
- ---
508
-
509
- ## Common Type Gotchas
510
-
511
- ```typescript
512
- // ❌ WRONG // ✅ CORRECT
513
- address.state address.region
514
- cart.total getCartTotals(cart).total
515
- cart.discount cart.discountAmount
516
- item.name (in cart) item.product.name
517
- response.url (OAuth) response.authorizationUrl
518
- providers.forEach (OAuth) response.providers.forEach
519
- status === 'completed' status === 'succeeded'
520
- product.metafields.name product.metafields[0].definitionName
521
- product.metafields.key product.metafields[0].definitionKey
522
- orderItem.unitPrice orderItem.price (OrderItem is FLAT, not nested!)
523
- cartItem.price cartItem.unitPrice (Cart/Checkout items use unitPrice)
524
- waitResult.orderNumber waitResult.status.orderNumber (nested in PaymentStatus)
525
- variant.attributes.map(...) Object.entries(variant.attributes || {}) (it's an object!)
526
- categorySuggestion.slug // ❌ doesn't exist! Only: id, name, productCount
527
- order.status === 'COMPLETED' order.status === 'delivered' (OrderStatus is lowercase!)
528
- getCartTotals(localCart) // ❌ LocalCart has no subtotal! Calculate manually
529
- result.checkoutId (guest checkout) // ⚠️ Check result.tracked first! It's a union type
530
- ```
531
-
532
- **Key distinctions:**
533
-
534
- - **OrderItem** (from orders): Flat structure — `item.price`, `item.name`, `item.image`
535
- - **CartItem / CheckoutLineItem**: Nested structure — `item.unitPrice`, `item.product.name`, `item.product.images`
536
- - **`getCartTotals()`** only works with **server Cart** (has `subtotal`/`discountAmount`). For **LocalCart**, calculate manually:
537
- ```typescript
538
- const subtotal = cart.items.reduce((sum, item) => sum + parseFloat(item.price || '0') * item.quantity, 0);
539
- ```
540
- - **`GuestCheckoutStartResponse`** is a union type — always check `result.tracked` before accessing `result.checkoutId`
541
- - **`WaitForOrderResult`** has `result.status.orderNumber`, NOT `result.orderNumber`. But `completeGuestCheckout()` returns `GuestOrderResponse` which DOES have `result.orderNumber` directly.
542
-
543
- ---
544
-
545
- ## Full SDK Documentation
546
-
547
- For complete API reference and working code examples:
548
- **https://brainerce.com/docs/sdk**
1
+ # OmniSync 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 omni.smartGetCart(); // Guest cart has id: "__local__"
14
+ await omni.createCheckout({ cartId: cart.id }); // 💥 "__local__" doesn't exist on server!
15
+
16
+ // ✅ CORRECT - Check user type first!
17
+ if (omni.isCustomerLoggedIn()) {
18
+ // Logged-in user → server cart exists
19
+ const checkout = await omni.createCheckout({ cartId: cart.id });
20
+ const checkoutId = checkout.id;
21
+ } else {
22
+ // Guest user → use startGuestCheckout()
23
+ const result = await omni.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
+ omni.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 omni.getProducts();
64
+ ```
65
+
66
+ ---
67
+
68
+ ## Quick Setup
69
+
70
+ ```bash
71
+ npm install omni-sync-sdk
72
+ ```
73
+
74
+ ```typescript
75
+ // lib/omni-sync.ts
76
+ import { OmniSyncClient } from 'omni-sync-sdk';
77
+
78
+ export const omni = new OmniSyncClient({
79
+ connectionId: '{connection_id}',
80
+ baseUrl: '{api_url}',
81
+ });
82
+
83
+ // Restore customer session on page load
84
+ export function initOmniSync() {
85
+ if (typeof window === 'undefined') return;
86
+ const token = localStorage.getItem('customerToken');
87
+ if (token) omni.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
+ omni.setCustomerToken(token);
95
+ } else {
96
+ localStorage.removeItem('customerToken');
97
+ omni.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 omni.smartGetCart();
109
+
110
+ // Add to cart
111
+ await omni.smartAddToCart({
112
+ productId: 'prod_xxx',
113
+ variantId: 'var_xxx', // optional, for products with variants
114
+ quantity: 1,
115
+ });
116
+
117
+ // Update quantity (by productId, not itemId!)
118
+ await omni.smartUpdateCartItem('prod_xxx', 2); // productId, quantity
119
+ await omni.smartUpdateCartItem('prod_xxx', 3, 'var_xxx'); // with variant
120
+
121
+ // Remove item (by productId, not itemId!)
122
+ await omni.smartRemoveFromCart('prod_xxx');
123
+ await omni.smartRemoveFromCart('prod_xxx', 'var_xxx'); // with variant
124
+
125
+ // Get cart totals (cart doesn't have .total field!)
126
+ import { getCartTotals } from 'omni-sync-sdk';
127
+ const totals = getCartTotals(cart);
128
+ // { subtotal: 59.98, discount: 10, shipping: 0, total: 49.98 }
129
+
130
+ // ⚠️ LocalCart vs Cart - KEY DIFFERENCES:
131
+ // Server Cart has: id, itemCount, subtotal, discountAmount
132
+ // Guest LocalCart has NONE of these! Only: items, couponCode, customer
133
+ // To check type: if ('id' in cart) { /* server Cart */ } else { /* LocalCart */ }
134
+ // Item count for both: cart.items.length
135
+ ```
136
+
137
+ ---
138
+
139
+ ## 🛒 Partial Checkout (AliExpress Style) - REQUIRED!
140
+
141
+ Cart page MUST have checkboxes so users can select which items to buy:
142
+
143
+ ```typescript
144
+ // Cart page - track selected items
145
+ const [selectedIndices, setSelectedIndices] = useState<number[]>(
146
+ cart.items.map((_, i) => i) // All selected by default
147
+ );
148
+
149
+ const toggleItem = (index: number) => {
150
+ setSelectedIndices(prev =>
151
+ prev.includes(index)
152
+ ? prev.filter(i => i !== index)
153
+ : [...prev, index]
154
+ );
155
+ };
156
+
157
+ const toggleAll = () => {
158
+ if (selectedIndices.length === cart.items.length) {
159
+ setSelectedIndices([]); // Deselect all
160
+ } else {
161
+ setSelectedIndices(cart.items.map((_, i) => i)); // Select all
162
+ }
163
+ };
164
+
165
+ // In your cart UI:
166
+ <div>
167
+ <label>
168
+ <input
169
+ type="checkbox"
170
+ checked={selectedIndices.length === cart.items.length}
171
+ onChange={toggleAll}
172
+ />
173
+ Select All
174
+ </label>
175
+ </div>
176
+
177
+ {cart.items.map((item, index) => (
178
+ <div key={index}>
179
+ <input
180
+ type="checkbox"
181
+ checked={selectedIndices.includes(index)}
182
+ onChange={() => toggleItem(index)}
183
+ />
184
+ {/* ... item details ... */}
185
+ </div>
186
+ ))}
187
+
188
+ // On checkout button - pass selected items!
189
+ const handleCheckout = async () => {
190
+ if (selectedIndices.length === 0) {
191
+ alert('Please select items to checkout');
192
+ return;
193
+ }
194
+
195
+ const result = await omni.startGuestCheckout({ selectedIndices });
196
+ // Only selected items go to checkout, others stay in cart!
197
+ };
198
+ ```
199
+
200
+ **Why this matters:**
201
+
202
+ - Users can buy some items now, leave others for later
203
+ - After payment, `completeGuestCheckout()` sends the order and only removes purchased items
204
+ - Remaining items stay in cart for future purchase
205
+
206
+ **⚠️ Order Summary on Checkout Page - Use checkout.lineItems!**
207
+
208
+ ```typescript
209
+ // ❌ WRONG - Shows ALL cart items (even unselected ones!)
210
+ <div className="order-summary">
211
+ {cart.items.map(item => (
212
+ <div>{item.product.name} - ${item.price}</div>
213
+ ))}
214
+ </div>
215
+
216
+ // ✅ CORRECT - Shows only items being purchased in this checkout
217
+ <div className="order-summary">
218
+ {checkout.lineItems.map(item => (
219
+ <div>{item.product.name} - ${item.price}</div>
220
+ ))}
221
+ </div>
222
+ ```
223
+
224
+ The `checkout` object's `lineItems` array contains ONLY the items selected for this checkout!
225
+
226
+ ---
227
+
228
+ ## Complete Checkout Flow
229
+
230
+ ### Step 1: Start Checkout (Different for Guest vs Logged-in!)
231
+
232
+ ```typescript
233
+ async function startCheckout() {
234
+ const cart = await omni.smartGetCart();
235
+
236
+ if (cart.items.length === 0) {
237
+ alert('Cart is empty');
238
+ return;
239
+ }
240
+
241
+ let checkoutId: string;
242
+
243
+ if (omni.isCustomerLoggedIn()) {
244
+ // Logged-in: create checkout from server cart
245
+ const checkout = await omni.createCheckout({ cartId: cart.id });
246
+ checkoutId = checkout.id;
247
+ } else {
248
+ // Guest: use startGuestCheckout (syncs local cart to server)
249
+ const result = await omni.startGuestCheckout();
250
+ if (!result.tracked || !result.checkoutId) {
251
+ throw new Error('Failed to create checkout');
252
+ }
253
+ checkoutId = result.checkoutId;
254
+ }
255
+
256
+ // Save for payment page
257
+ localStorage.setItem('checkoutId', checkoutId);
258
+
259
+ // Navigate to checkout
260
+ window.location.href = '/checkout';
261
+ }
262
+ ```
263
+
264
+ ### Step 2: Shipping Address
265
+
266
+ ```typescript
267
+ const checkoutId = localStorage.getItem('checkoutId')!;
268
+
269
+ // Set shipping address (email is required!)
270
+ const { checkout, rates } = await omni.setShippingAddress(checkoutId, {
271
+ email: 'customer@example.com',
272
+ firstName: 'John',
273
+ lastName: 'Doe',
274
+ line1: '123 Main St',
275
+ city: 'New York',
276
+ region: 'NY', // ⚠️ Use 'region', NOT 'state'!
277
+ postalCode: '10001',
278
+ country: 'US',
279
+ });
280
+
281
+ // Show available shipping rates
282
+ rates.forEach((rate) => {
283
+ console.log(`${rate.name}: $${rate.price}`);
284
+ });
285
+ ```
286
+
287
+ ### Step 3: Select Shipping Method
288
+
289
+ ```typescript
290
+ await omni.selectShippingMethod(checkoutId, selectedRateId);
291
+ ```
292
+
293
+ ### Step 4: Payment with Stripe
294
+
295
+ ```typescript
296
+ // Install: npm install @stripe/stripe-js @stripe/react-stripe-js
297
+
298
+ // 1. Check if payment is configured
299
+ const { hasPayments, providers } = await omni.getPaymentProviders();
300
+ if (!hasPayments) {
301
+ return <div>Payment not configured for this store</div>;
302
+ }
303
+
304
+ // 2. Get Stripe config
305
+ const stripeProvider = providers.find(p => p.provider === 'stripe');
306
+ if (!stripeProvider) {
307
+ return <div>Stripe not configured</div>;
308
+ }
309
+
310
+ // 3. Create payment intent
311
+ const intent = await omni.createPaymentIntent(checkoutId);
312
+
313
+ // 4. Initialize Stripe
314
+ import { loadStripe } from '@stripe/stripe-js';
315
+ const stripe = await loadStripe(stripeProvider.publicKey, {
316
+ stripeAccount: stripeProvider.stripeAccountId,
317
+ });
318
+
319
+ // 5. Confirm payment (in your payment form)
320
+ const { error } = await stripe.confirmPayment({
321
+ elements,
322
+ confirmParams: {
323
+ return_url: `${window.location.origin}/checkout/success?checkout_id=${checkoutId}`,
324
+ },
325
+ });
326
+
327
+ if (error) {
328
+ setError(error.message);
329
+ }
330
+ // If no error, Stripe redirects to success page
331
+ ```
332
+
333
+ ### Step 5: Success Page (Complete Order & Clear Cart!)
334
+
335
+ ```typescript
336
+ // /checkout/success/page.tsx
337
+ 'use client';
338
+ import { useEffect, useState } from 'react';
339
+ import { omni } from '@/lib/omni-sync';
340
+
341
+ export default function CheckoutSuccessPage() {
342
+ const [orderNumber, setOrderNumber] = useState<string>();
343
+ const [loading, setLoading] = useState(true);
344
+
345
+ useEffect(() => {
346
+ const checkoutId = new URLSearchParams(window.location.search).get('checkout_id');
347
+
348
+ if (checkoutId) {
349
+ // ⚠️ CRITICAL: Complete the order on the server AND clear the cart!
350
+ // Do NOT use handlePaymentSuccess() - it only clears localStorage!
351
+ omni.completeGuestCheckout(checkoutId).then(result => {
352
+ setOrderNumber(result.orderNumber);
353
+ setLoading(false);
354
+ }).catch(() => {
355
+ // Order may already be completed (e.g., page refresh) - check status
356
+ omni.getPaymentStatus(checkoutId).then(status => {
357
+ if (status.orderNumber) {
358
+ setOrderNumber(status.orderNumber);
359
+ }
360
+ setLoading(false);
361
+ });
362
+ });
363
+ }
364
+ }, []);
365
+
366
+ return (
367
+ <div className="text-center py-12">
368
+ <h1 className="text-2xl font-bold text-green-600">Thank you for your order!</h1>
369
+ {loading && <p className="mt-2">Processing your order...</p>}
370
+ {orderNumber && <p className="mt-2">Order #{orderNumber}</p>}
371
+ <p className="mt-4">A confirmation email will be sent shortly.</p>
372
+ </div>
373
+ );
374
+ }
375
+ ```
376
+
377
+ ---
378
+
379
+ ## Partial Checkout (AliExpress Style)
380
+
381
+ Allow customers to buy only some items from their cart:
382
+
383
+ ```typescript
384
+ // Start checkout with only selected items (by index)
385
+ const result = await omni.startGuestCheckout({
386
+ selectedIndices: [0, 2], // Buy items at index 0 and 2 only
387
+ });
388
+
389
+ // After payment, completeGuestCheckout() sends the order AND removes only those items!
390
+ // Other items stay in cart.
391
+ ```
392
+
393
+ ---
394
+
395
+ ## Products API
396
+
397
+ ```typescript
398
+ // List products with pagination
399
+ const { data: products, meta } = await omni.getProducts({
400
+ page: 1,
401
+ limit: 20,
402
+ search: 'blue shirt', // Searches name, description, SKU, categories, tags
403
+ });
404
+ // meta = { page: 1, limit: 20, total: 150, totalPages: 8 }
405
+
406
+ // Get single product by slug (for product detail page)
407
+ const product = await omni.getProductBySlug('blue-cotton-shirt');
408
+
409
+ // Search suggestions (for autocomplete)
410
+ const suggestions = await omni.getSearchSuggestions('blue', 5);
411
+ // { products: [...], categories: [...] }
412
+ ```
413
+
414
+ ---
415
+
416
+ ## Product Custom Fields (Metafields)
417
+
418
+ Products may have custom fields defined by the store owner (e.g., "Material", "Care Instructions", "Warranty").
419
+
420
+ ```typescript
421
+ import { getProductMetafield, getProductMetafieldValue } from 'omni-sync-sdk';
422
+
423
+ // Access metafields on a product
424
+ const product = await omni.getProductBySlug('blue-shirt');
425
+
426
+ // Get all custom fields
427
+ product.metafields?.forEach((field) => {
428
+ console.log(`${field.definitionName}: ${field.value}`);
429
+ });
430
+
431
+ // Get specific field by key
432
+ const material = getProductMetafieldValue(product, 'material');
433
+ const careInstructions = getProductMetafield(product, 'care_instructions');
434
+
435
+ // Get available metafield definitions (schema)
436
+ const { definitions } = await omni.getPublicMetafieldDefinitions();
437
+ // Use definitions to build dynamic UI (filters, forms, etc.)
438
+ ```
439
+
440
+ > **Tip:** `metafields` may be empty if the store hasn't defined custom fields. Always use optional chaining.
441
+
442
+ ---
443
+
444
+ ## Customer Authentication
445
+
446
+ ```typescript
447
+ // Register
448
+ const { customer, token } = await omni.registerCustomer({
449
+ email: 'john@example.com',
450
+ password: 'securepass123',
451
+ firstName: 'John',
452
+ lastName: 'Doe',
453
+ });
454
+ setCustomerToken(token);
455
+
456
+ // Login
457
+ const { customer, token } = await omni.loginCustomer('john@example.com', 'password');
458
+ setCustomerToken(token);
459
+
460
+ // Logout
461
+ setCustomerToken(null);
462
+
463
+ // Get profile (requires token)
464
+ const profile = await omni.getMyProfile();
465
+
466
+ // Get order history
467
+ const { data: orders, meta } = await omni.getMyOrders({ page: 1, limit: 10 });
468
+ ```
469
+
470
+ ---
471
+
472
+ ## OAuth / Social Login
473
+
474
+ ```typescript
475
+ // Get available providers for this store
476
+ const { providers } = await omni.getAvailableOAuthProviders();
477
+ // providers = ['GOOGLE', 'FACEBOOK', 'GITHUB']
478
+
479
+ // Redirect to OAuth provider
480
+ const { authorizationUrl } = await omni.getOAuthAuthorizeUrl('GOOGLE', {
481
+ redirectUrl: `${window.location.origin}/auth/callback`,
482
+ });
483
+ window.location.href = authorizationUrl;
484
+
485
+ // Handle callback (on /auth/callback page)
486
+ const code = new URLSearchParams(window.location.search).get('code');
487
+ const state = new URLSearchParams(window.location.search).get('state');
488
+ const { customer, token } = await omni.handleOAuthCallback('GOOGLE', code!, state!);
489
+ setCustomerToken(token);
490
+ ```
491
+
492
+ ---
493
+
494
+ ## Required Pages Checklist
495
+
496
+ - [ ] **Home** (`/`) - Featured products grid
497
+ - [ ] **Products** (`/products`) - Product list with infinite scroll
498
+ - [ ] **Product Detail** (`/products/[slug]`) - Use `getProductBySlug(slug)`
499
+ - [ ] **Cart** (`/cart`) - Show items, quantities, totals
500
+ - [ ] **Checkout** (`/checkout`) - Address → Shipping → Payment
501
+ - [ ] **Success** (`/checkout/success`) - **Must call `completeGuestCheckout()`!**
502
+ - [ ] **Login** (`/login`) - Email/password + social buttons
503
+ - [ ] **Register** (`/register`) - Registration form
504
+ - [ ] **Account** (`/account`) - Profile + order history
505
+ - [ ] **Header** - Logo, nav, cart icon with count, search
506
+
507
+ ---
508
+
509
+ ## Common Type Gotchas
510
+
511
+ ```typescript
512
+ // ❌ WRONG // ✅ CORRECT
513
+ address.state address.region
514
+ cart.total getCartTotals(cart).total
515
+ cart.discount cart.discountAmount
516
+ item.name (in cart) item.product.name
517
+ response.url (OAuth) response.authorizationUrl
518
+ providers.forEach (OAuth) response.providers.forEach
519
+ status === 'completed' status === 'succeeded'
520
+ product.metafields.name product.metafields[0].definitionName
521
+ product.metafields.key product.metafields[0].definitionKey
522
+ orderItem.unitPrice orderItem.price (OrderItem is FLAT, not nested!)
523
+ cartItem.price cartItem.unitPrice (Cart/Checkout items use unitPrice)
524
+ waitResult.orderNumber waitResult.status.orderNumber (nested in PaymentStatus)
525
+ variant.attributes.map(...) Object.entries(variant.attributes || {}) (it's an object!)
526
+ categorySuggestion.slug // ❌ doesn't exist! Only: id, name, productCount
527
+ order.status === 'COMPLETED' order.status === 'delivered' (OrderStatus is lowercase!)
528
+ getCartTotals(localCart) // ❌ LocalCart has no subtotal! Calculate manually
529
+ result.checkoutId (guest checkout) // ⚠️ Check result.tracked first! It's a union type
530
+ ```
531
+
532
+ **Key distinctions:**
533
+
534
+ - **OrderItem** (from orders): Flat structure — `item.price`, `item.name`, `item.image`
535
+ - **CartItem / CheckoutLineItem**: Nested structure — `item.unitPrice`, `item.product.name`, `item.product.images`
536
+ - **`getCartTotals()`** only works with **server Cart** (has `subtotal`/`discountAmount`). For **LocalCart**, calculate manually:
537
+ ```typescript
538
+ const subtotal = cart.items.reduce(
539
+ (sum, item) => sum + parseFloat(item.price || '0') * item.quantity,
540
+ 0
541
+ );
542
+ ```
543
+ - **`GuestCheckoutStartResponse`** is a union type — always check `result.tracked` before accessing `result.checkoutId`
544
+ - **`WaitForOrderResult`** has `result.status.orderNumber`, NOT `result.orderNumber`. But `completeGuestCheckout()` returns `GuestOrderResponse` which DOES have `result.orderNumber` directly.
545
+ - **⚠️ HYDRATION: Never use `useState(omni.getLocalCart())`** in Next.js — causes hydration mismatch! Server has no localStorage (empty cart) but client does (real cart). Always start with empty state and load in `useEffect`:
546
+
547
+ ```typescript
548
+ // ❌ WRONG — hydration mismatch!
549
+ const [cart, setCart] = useState(omni.getLocalCart());
550
+
551
+ // ✅ CORRECT — load after hydration
552
+ const [cart, setCart] = useState<LocalCart>({ items: [], updatedAt: '' });
553
+ useEffect(() => {
554
+ setCart(omni.getLocalCart());
555
+ }, []);
556
+ ```
557
+
558
+ ---
559
+
560
+ ## Full SDK Documentation
561
+
562
+ For complete API reference and working code examples:
563
+ **https://brainerce.com/docs/sdk**