omni-sync-sdk 0.4.0 → 0.6.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/README.md CHANGED
@@ -31,14 +31,73 @@ const omni = new OmniSyncClient({
31
31
  // Fetch products
32
32
  const { data: products } = await omni.getProducts();
33
33
 
34
- // Create a cart
35
- const cart = await omni.createCart();
34
+ // ===== GUEST CHECKOUT (Recommended for most sites) =====
35
+ // Cart stored locally - NO API calls until checkout!
36
36
 
37
- // Add item to cart
38
- await omni.addToCart(cart.id, {
37
+ // Add to local cart (stored in localStorage)
38
+ omni.addToLocalCart({
39
39
  productId: products[0].id,
40
40
  quantity: 1,
41
+ name: products[0].name,
42
+ price: String(products[0].basePrice),
43
+ });
44
+
45
+ // Set customer info
46
+ omni.setLocalCartCustomer({ email: 'customer@example.com' });
47
+ omni.setLocalCartShippingAddress({
48
+ firstName: 'John',
49
+ lastName: 'Doe',
50
+ line1: '123 Main St',
51
+ city: 'New York',
52
+ postalCode: '10001',
53
+ country: 'US',
41
54
  });
55
+
56
+ // Submit order (single API call!)
57
+ const order = await omni.submitGuestOrder();
58
+ console.log('Order created:', order.orderId);
59
+ ```
60
+
61
+ ---
62
+
63
+ ## Two Ways to Handle Cart
64
+
65
+ ### Option 1: Local Cart (Guest Users) - RECOMMENDED
66
+
67
+ For guest users, the cart is stored in **localStorage** - exactly like Amazon, Shopify, and other major platforms do. This means:
68
+ - ✅ No API calls when browsing/adding to cart
69
+ - ✅ Cart persists across page refreshes
70
+ - ✅ Single API call at checkout
71
+ - ✅ No server load for window shoppers
72
+
73
+ ```typescript
74
+ // Add product to local cart
75
+ omni.addToLocalCart({ productId: 'prod_123', quantity: 2 });
76
+
77
+ // View cart
78
+ const cart = omni.getLocalCart();
79
+ console.log('Items:', cart.items.length);
80
+
81
+ // Update quantity
82
+ omni.updateLocalCartItem('prod_123', 5);
83
+
84
+ // Remove item
85
+ omni.removeFromLocalCart('prod_123');
86
+
87
+ // At checkout - submit everything in ONE API call
88
+ const order = await omni.submitGuestOrder();
89
+ ```
90
+
91
+ ### Option 2: Server Cart (Registered Users)
92
+
93
+ For logged-in customers, use server-side cart:
94
+ - ✅ Cart syncs across devices
95
+ - ✅ Abandoned cart recovery
96
+ - ✅ Customer history tracking
97
+
98
+ ```typescript
99
+ const cart = await omni.createCart();
100
+ await omni.addToCart(cart.id, { productId: 'prod_123', quantity: 2 });
42
101
  ```
43
102
 
44
103
  ---
@@ -56,18 +115,28 @@ export const omni = new OmniSyncClient({
56
115
  connectionId: 'vc_YOUR_CONNECTION_ID', // Your Connection ID from OmniSync
57
116
  });
58
117
 
59
- // ----- Cart Helpers -----
118
+ // ----- Guest Cart Helpers (localStorage) -----
119
+
120
+ export function getCartItemCount(): number {
121
+ return omni.getLocalCartItemCount();
122
+ }
123
+
124
+ export function getCart() {
125
+ return omni.getLocalCart();
126
+ }
127
+
128
+ // ----- For Registered Users (server cart) -----
60
129
 
61
- export function getCartId(): string | null {
130
+ export function getServerCartId(): string | null {
62
131
  if (typeof window === 'undefined') return null;
63
132
  return localStorage.getItem('cartId');
64
133
  }
65
134
 
66
- export function setCartId(id: string): void {
135
+ export function setServerCartId(id: string): void {
67
136
  localStorage.setItem('cartId', id);
68
137
  }
69
138
 
70
- export function clearCartId(): void {
139
+ export function clearServerCartId(): void {
71
140
  localStorage.removeItem('cartId');
72
141
  }
73
142
 
@@ -109,15 +178,15 @@ import type { Product, PaginatedResponse } from 'omni-sync-sdk';
109
178
  const response: PaginatedResponse<Product> = await omni.getProducts({
110
179
  page: 1,
111
180
  limit: 12,
112
- search: 'shirt', // Optional: search by name
113
- status: 'active', // Optional: 'active' | 'draft' | 'archived'
114
- type: 'SIMPLE', // Optional: 'SIMPLE' | 'VARIABLE'
115
- sortBy: 'createdAt', // Optional: 'name' | 'createdAt' | 'updatedAt' | 'basePrice'
116
- sortOrder: 'desc', // Optional: 'asc' | 'desc'
181
+ search: 'shirt', // Optional: search by name
182
+ status: 'active', // Optional: 'active' | 'draft' | 'archived'
183
+ type: 'SIMPLE', // Optional: 'SIMPLE' | 'VARIABLE'
184
+ sortBy: 'createdAt', // Optional: 'name' | 'createdAt' | 'updatedAt' | 'basePrice'
185
+ sortOrder: 'desc', // Optional: 'asc' | 'desc'
117
186
  });
118
187
 
119
- console.log(response.data); // Product[]
120
- console.log(response.meta.total); // Total number of products
188
+ console.log(response.data); // Product[]
189
+ console.log(response.meta.total); // Total number of products
121
190
  console.log(response.meta.totalPages); // Total pages
122
191
  ```
123
192
 
@@ -128,10 +197,10 @@ const product: Product = await omni.getProduct('product_id');
128
197
 
129
198
  console.log(product.name);
130
199
  console.log(product.basePrice);
131
- console.log(product.salePrice); // null if no sale
132
- console.log(product.images); // ProductImage[]
133
- console.log(product.variants); // ProductVariant[] (for VARIABLE products)
134
- console.log(product.inventory); // { total, reserved, available }
200
+ console.log(product.salePrice); // null if no sale
201
+ console.log(product.images); // ProductImage[]
202
+ console.log(product.variants); // ProductVariant[] (for VARIABLE products)
203
+ console.log(product.inventory); // { total, reserved, available }
135
204
  ```
136
205
 
137
206
  #### Product Type Definition
@@ -180,13 +249,234 @@ interface InventoryInfo {
180
249
 
181
250
  ---
182
251
 
183
- ### Cart
252
+ ### Local Cart (Guest Users) - RECOMMENDED
253
+
254
+ The local cart stores everything in **localStorage** until checkout. This is the recommended approach for most storefronts.
255
+
256
+ #### Add to Local Cart
257
+
258
+ ```typescript
259
+ // Add item with product info (for display)
260
+ omni.addToLocalCart({
261
+ productId: 'prod_123',
262
+ variantId: 'var_456', // Optional: for products with variants
263
+ quantity: 2,
264
+ name: 'Cool T-Shirt', // Optional: for cart display
265
+ price: '29.99', // Optional: for cart display
266
+ image: 'https://...', // Optional: for cart display
267
+ });
268
+ ```
269
+
270
+ #### Get Local Cart
271
+
272
+ ```typescript
273
+ const cart = omni.getLocalCart();
274
+
275
+ console.log(cart.items); // Array of cart items
276
+ console.log(cart.customer); // Customer info (if set)
277
+ console.log(cart.shippingAddress); // Shipping address (if set)
278
+ console.log(cart.couponCode); // Applied coupon (if any)
279
+ ```
280
+
281
+ #### Update Item Quantity
282
+
283
+ ```typescript
284
+ // Set quantity to 5
285
+ omni.updateLocalCartItem('prod_123', 5);
286
+
287
+ // For variant products
288
+ omni.updateLocalCartItem('prod_123', 3, 'var_456');
289
+
290
+ // Set to 0 to remove
291
+ omni.updateLocalCartItem('prod_123', 0);
292
+ ```
293
+
294
+ #### Remove Item
295
+
296
+ ```typescript
297
+ omni.removeFromLocalCart('prod_123');
298
+ omni.removeFromLocalCart('prod_123', 'var_456'); // With variant
299
+ ```
300
+
301
+ #### Clear Cart
302
+
303
+ ```typescript
304
+ omni.clearLocalCart();
305
+ ```
306
+
307
+ #### Set Customer Info
308
+
309
+ ```typescript
310
+ omni.setLocalCartCustomer({
311
+ email: 'customer@example.com', // Required
312
+ firstName: 'John', // Optional
313
+ lastName: 'Doe', // Optional
314
+ phone: '+1234567890', // Optional
315
+ });
316
+ ```
317
+
318
+ #### Set Shipping Address
319
+
320
+ ```typescript
321
+ omni.setLocalCartShippingAddress({
322
+ firstName: 'John',
323
+ lastName: 'Doe',
324
+ line1: '123 Main St',
325
+ line2: 'Apt 4B', // Optional
326
+ city: 'New York',
327
+ region: 'NY', // Optional: State/Province
328
+ postalCode: '10001',
329
+ country: 'US',
330
+ phone: '+1234567890', // Optional
331
+ });
332
+ ```
333
+
334
+ #### Set Billing Address (Optional)
335
+
336
+ ```typescript
337
+ omni.setLocalCartBillingAddress({
338
+ firstName: 'John',
339
+ lastName: 'Doe',
340
+ line1: '456 Business Ave',
341
+ city: 'New York',
342
+ postalCode: '10002',
343
+ country: 'US',
344
+ });
345
+ ```
346
+
347
+ #### Apply Coupon
348
+
349
+ ```typescript
350
+ omni.setLocalCartCoupon('SAVE20');
351
+
352
+ // Remove coupon
353
+ omni.setLocalCartCoupon(undefined);
354
+ ```
355
+
356
+ #### Get Cart Item Count
357
+
358
+ ```typescript
359
+ const count = omni.getLocalCartItemCount();
360
+ console.log(`${count} items in cart`);
361
+ ```
362
+
363
+ #### Local Cart Type Definition
364
+
365
+ ```typescript
366
+ interface LocalCart {
367
+ items: LocalCartItem[];
368
+ couponCode?: string;
369
+ customer?: {
370
+ email: string;
371
+ firstName?: string;
372
+ lastName?: string;
373
+ phone?: string;
374
+ };
375
+ shippingAddress?: {
376
+ firstName: string;
377
+ lastName: string;
378
+ line1: string;
379
+ line2?: string;
380
+ city: string;
381
+ region?: string;
382
+ postalCode: string;
383
+ country: string;
384
+ phone?: string;
385
+ };
386
+ billingAddress?: { /* same as shipping */ };
387
+ notes?: string;
388
+ updatedAt: string;
389
+ }
390
+
391
+ interface LocalCartItem {
392
+ productId: string;
393
+ variantId?: string;
394
+ quantity: number;
395
+ name?: string;
396
+ sku?: string;
397
+ price?: string;
398
+ image?: string;
399
+ addedAt: string;
400
+ }
401
+ ```
402
+
403
+ ---
404
+
405
+ ### Guest Checkout (Submit Order)
406
+
407
+ Submit the local cart as an order with a **single API call**:
408
+
409
+ ```typescript
410
+ // Make sure cart has items, customer email, and shipping address
411
+ const order = await omni.submitGuestOrder();
412
+
413
+ console.log(order.orderId); // 'order_abc123...'
414
+ console.log(order.orderNumber); // 'ORD-12345'
415
+ console.log(order.status); // 'pending'
416
+ console.log(order.total); // 59.98
417
+ console.log(order.message); // 'Order created successfully'
418
+
419
+ // Cart is automatically cleared after successful order
420
+ ```
421
+
422
+ #### Keep Cart After Order
423
+
424
+ ```typescript
425
+ // If you want to keep the cart data (e.g., for order review page)
426
+ const order = await omni.submitGuestOrder({ clearCartOnSuccess: false });
427
+ ```
428
+
429
+ #### Create Order with Custom Data
430
+
431
+ If you manage cart state yourself instead of using local cart:
432
+
433
+ ```typescript
434
+ const order = await omni.createGuestOrder({
435
+ items: [
436
+ { productId: 'prod_123', quantity: 2 },
437
+ { productId: 'prod_456', variantId: 'var_789', quantity: 1 },
438
+ ],
439
+ customer: {
440
+ email: 'customer@example.com',
441
+ firstName: 'John',
442
+ lastName: 'Doe',
443
+ },
444
+ shippingAddress: {
445
+ firstName: 'John',
446
+ lastName: 'Doe',
447
+ line1: '123 Main St',
448
+ city: 'New York',
449
+ postalCode: '10001',
450
+ country: 'US',
451
+ },
452
+ couponCode: 'SAVE20', // Optional
453
+ notes: 'Please gift wrap', // Optional
454
+ });
455
+ ```
456
+
457
+ #### Guest Order Response Type
458
+
459
+ ```typescript
460
+ interface GuestOrderResponse {
461
+ orderId: string;
462
+ orderNumber: string;
463
+ status: string;
464
+ total: number;
465
+ message: string;
466
+ }
467
+ ```
468
+
469
+ ---
470
+
471
+ ### Server Cart (Registered Users)
472
+
473
+ For logged-in customers who want cart sync across devices.
184
474
 
185
475
  #### Create Cart
186
476
 
187
477
  ```typescript
188
478
  const cart = await omni.createCart();
189
- setCartId(cart.id); // Save to localStorage
479
+ setServerCartId(cart.id); // Save to localStorage
190
480
  ```
191
481
 
192
482
  #### Get Cart
@@ -195,9 +485,9 @@ setCartId(cart.id); // Save to localStorage
195
485
  const cartId = getCartId();
196
486
  if (cartId) {
197
487
  const cart = await omni.getCart(cartId);
198
- console.log(cart.items); // CartItem[]
199
- console.log(cart.itemCount); // Total items
200
- console.log(cart.subtotal); // Subtotal amount
488
+ console.log(cart.items); // CartItem[]
489
+ console.log(cart.itemCount); // Total items
490
+ console.log(cart.subtotal); // Subtotal amount
201
491
  }
202
492
  ```
203
493
 
@@ -206,7 +496,7 @@ if (cartId) {
206
496
  ```typescript
207
497
  const cart = await omni.addToCart(cartId, {
208
498
  productId: 'product_id',
209
- variantId: 'variant_id', // Optional: for VARIABLE products
499
+ variantId: 'variant_id', // Optional: for VARIABLE products
210
500
  quantity: 2,
211
501
  notes: 'Gift wrap please', // Optional
212
502
  });
@@ -231,7 +521,7 @@ const cart = await omni.removeCartItem(cartId, itemId);
231
521
  ```typescript
232
522
  const cart = await omni.applyCoupon(cartId, 'SAVE20');
233
523
  console.log(cart.discountAmount); // Discount applied
234
- console.log(cart.couponCode); // 'SAVE20'
524
+ console.log(cart.couponCode); // 'SAVE20'
235
525
  ```
236
526
 
237
527
  #### Remove Coupon
@@ -310,12 +600,12 @@ const { checkout, rates } = await omni.setShippingAddress(checkoutId, {
310
600
  firstName: 'John',
311
601
  lastName: 'Doe',
312
602
  line1: '123 Main St',
313
- line2: 'Apt 4B', // Optional
603
+ line2: 'Apt 4B', // Optional
314
604
  city: 'New York',
315
- region: 'NY', // State/Province
605
+ region: 'NY', // State/Province
316
606
  postalCode: '10001',
317
607
  country: 'US',
318
- phone: '+1234567890', // Optional
608
+ phone: '+1234567890', // Optional
319
609
  });
320
610
 
321
611
  // rates contains available shipping options
@@ -368,12 +658,7 @@ interface Checkout {
368
658
  availableShippingRates?: ShippingRate[];
369
659
  }
370
660
 
371
- type CheckoutStatus =
372
- | 'PENDING'
373
- | 'SHIPPING_SET'
374
- | 'PAYMENT_PENDING'
375
- | 'COMPLETED'
376
- | 'FAILED';
661
+ type CheckoutStatus = 'PENDING' | 'SHIPPING_SET' | 'PAYMENT_PENDING' | 'COMPLETED' | 'FAILED';
377
662
 
378
663
  interface ShippingRate {
379
664
  id: string;
@@ -498,7 +783,7 @@ await omni.deleteMyAddress(addressId);
498
783
  ```typescript
499
784
  const store = await omni.getStoreInfo();
500
785
 
501
- console.log(store.name); // Store name
786
+ console.log(store.name); // Store name
502
787
  console.log(store.currency); // 'USD', 'ILS', etc.
503
788
  console.log(store.language); // 'en', 'he', etc.
504
789
  ```
@@ -620,12 +905,12 @@ export default function ProductsPage() {
620
905
  }
621
906
  ```
622
907
 
623
- ### Product Detail with Add to Cart
908
+ ### Product Detail with Add to Cart (Local Cart)
624
909
 
625
910
  ```typescript
626
911
  'use client';
627
912
  import { useEffect, useState } from 'react';
628
- import { omni, getCartId, setCartId } from '@/lib/omni-sync';
913
+ import { omni } from '@/lib/omni-sync';
629
914
  import type { Product } from 'omni-sync-sdk';
630
915
 
631
916
  export default function ProductPage({ params }: { params: { id: string } }) {
@@ -633,7 +918,6 @@ export default function ProductPage({ params }: { params: { id: string } }) {
633
918
  const [selectedVariant, setSelectedVariant] = useState<string | null>(null);
634
919
  const [quantity, setQuantity] = useState(1);
635
920
  const [loading, setLoading] = useState(true);
636
- const [adding, setAdding] = useState(false);
637
921
 
638
922
  useEffect(() => {
639
923
  async function load() {
@@ -650,30 +934,25 @@ export default function ProductPage({ params }: { params: { id: string } }) {
650
934
  load();
651
935
  }, [params.id]);
652
936
 
653
- const handleAddToCart = async () => {
937
+ const handleAddToCart = () => {
654
938
  if (!product) return;
655
- setAdding(true);
656
- try {
657
- let cartId = getCartId();
658
939
 
659
- if (!cartId) {
660
- const cart = await omni.createCart();
661
- cartId = cart.id;
662
- setCartId(cartId);
663
- }
664
-
665
- await omni.addToCart(cartId, {
666
- productId: product.id,
667
- variantId: selectedVariant || undefined,
668
- quantity,
669
- });
670
-
671
- alert('Added to cart!');
672
- } catch (err) {
673
- alert('Failed to add to cart');
674
- } finally {
675
- setAdding(false);
676
- }
940
+ // Get variant if selected
941
+ const variant = selectedVariant
942
+ ? product.variants?.find(v => v.id === selectedVariant)
943
+ : null;
944
+
945
+ // Add to local cart (NO API call!)
946
+ omni.addToLocalCart({
947
+ productId: product.id,
948
+ variantId: selectedVariant || undefined,
949
+ quantity,
950
+ name: variant?.name || product.name,
951
+ price: String(variant?.price || product.salePrice || product.basePrice),
952
+ image: product.images?.[0]?.url,
953
+ });
954
+
955
+ alert('Added to cart!');
677
956
  };
678
957
 
679
958
  if (loading) return <div>Loading...</div>;
@@ -754,63 +1033,33 @@ export default function ProductPage({ params }: { params: { id: string } }) {
754
1033
  }
755
1034
  ```
756
1035
 
757
- ### Cart Page
1036
+ ### Cart Page (Local Cart)
758
1037
 
759
1038
  ```typescript
760
1039
  'use client';
761
- import { useEffect, useState } from 'react';
762
- import { omni, getCartId } from '@/lib/omni-sync';
763
- import type { Cart } from 'omni-sync-sdk';
1040
+ import { useState } from 'react';
1041
+ import { omni } from '@/lib/omni-sync';
1042
+ import type { LocalCart } from 'omni-sync-sdk';
764
1043
 
765
1044
  export default function CartPage() {
766
- const [cart, setCart] = useState<Cart | null>(null);
767
- const [loading, setLoading] = useState(true);
768
- const [updating, setUpdating] = useState<string | null>(null);
1045
+ const [cart, setCart] = useState<LocalCart>(omni.getLocalCart());
769
1046
 
770
- const loadCart = async () => {
771
- const cartId = getCartId();
772
- if (!cartId) {
773
- setLoading(false);
774
- return;
775
- }
776
- try {
777
- const c = await omni.getCart(cartId);
778
- setCart(c);
779
- } finally {
780
- setLoading(false);
781
- }
1047
+ const updateQuantity = (productId: string, quantity: number, variantId?: string) => {
1048
+ const updated = omni.updateLocalCartItem(productId, quantity, variantId);
1049
+ setCart(updated);
782
1050
  };
783
1051
 
784
- useEffect(() => { loadCart(); }, []);
785
-
786
- const updateQuantity = async (itemId: string, quantity: number) => {
787
- if (!cart) return;
788
- setUpdating(itemId);
789
- try {
790
- if (quantity <= 0) {
791
- await omni.removeCartItem(cart.id, itemId);
792
- } else {
793
- await omni.updateCartItem(cart.id, itemId, { quantity });
794
- }
795
- await loadCart();
796
- } finally {
797
- setUpdating(null);
798
- }
1052
+ const removeItem = (productId: string, variantId?: string) => {
1053
+ const updated = omni.removeFromLocalCart(productId, variantId);
1054
+ setCart(updated);
799
1055
  };
800
1056
 
801
- const removeItem = async (itemId: string) => {
802
- if (!cart) return;
803
- setUpdating(itemId);
804
- try {
805
- await omni.removeCartItem(cart.id, itemId);
806
- await loadCart();
807
- } finally {
808
- setUpdating(null);
809
- }
810
- };
1057
+ // Calculate subtotal from local cart
1058
+ const subtotal = cart.items.reduce((sum, item) => {
1059
+ return sum + (parseFloat(item.price || '0') * item.quantity);
1060
+ }, 0);
811
1061
 
812
- if (loading) return <div>Loading cart...</div>;
813
- if (!cart || cart.items.length === 0) {
1062
+ if (cart.items.length === 0) {
814
1063
  return (
815
1064
  <div className="text-center py-12">
816
1065
  <h1 className="text-2xl font-bold">Your cart is empty</h1>
@@ -824,42 +1073,38 @@ export default function CartPage() {
824
1073
  <h1 className="text-2xl font-bold mb-6">Shopping Cart</h1>
825
1074
 
826
1075
  {cart.items.map((item) => (
827
- <div key={item.id} className="flex items-center gap-4 py-4 border-b">
1076
+ <div key={`${item.productId}-${item.variantId || ''}`} className="flex items-center gap-4 py-4 border-b">
828
1077
  <img
829
- src={item.product.images?.[0]?.url || '/placeholder.jpg'}
830
- alt={item.product.name}
1078
+ src={item.image || '/placeholder.jpg'}
1079
+ alt={item.name || 'Product'}
831
1080
  className="w-20 h-20 object-cover"
832
1081
  />
833
1082
  <div className="flex-1">
834
- <h3 className="font-medium">{item.product.name}</h3>
835
- {item.variant && <p className="text-sm text-gray-500">{item.variant.name}</p>}
836
- <p className="font-bold">${item.unitPrice}</p>
1083
+ <h3 className="font-medium">{item.name || 'Product'}</h3>
1084
+ <p className="font-bold">${item.price}</p>
837
1085
  </div>
838
1086
  <div className="flex items-center gap-2">
839
1087
  <button
840
- onClick={() => updateQuantity(item.id, item.quantity - 1)}
841
- disabled={updating === item.id}
1088
+ onClick={() => updateQuantity(item.productId, item.quantity - 1, item.variantId)}
842
1089
  className="w-8 h-8 border rounded"
843
1090
  >-</button>
844
1091
  <span className="w-8 text-center">{item.quantity}</span>
845
1092
  <button
846
- onClick={() => updateQuantity(item.id, item.quantity + 1)}
847
- disabled={updating === item.id}
1093
+ onClick={() => updateQuantity(item.productId, item.quantity + 1, item.variantId)}
848
1094
  className="w-8 h-8 border rounded"
849
1095
  >+</button>
850
1096
  </div>
851
1097
  <button
852
- onClick={() => removeItem(item.id)}
853
- disabled={updating === item.id}
1098
+ onClick={() => removeItem(item.productId, item.variantId)}
854
1099
  className="text-red-600"
855
1100
  >Remove</button>
856
1101
  </div>
857
1102
  ))}
858
1103
 
859
1104
  <div className="mt-6 text-right">
860
- <p className="text-xl">Subtotal: <strong>${cart.subtotal}</strong></p>
861
- {cart.discountAmount && Number(cart.discountAmount) > 0 && (
862
- <p className="text-green-600">Discount: -${cart.discountAmount}</p>
1105
+ <p className="text-xl">Subtotal: <strong>${subtotal.toFixed(2)}</strong></p>
1106
+ {cart.couponCode && (
1107
+ <p className="text-green-600">Coupon applied: {cart.couponCode}</p>
863
1108
  )}
864
1109
  <a
865
1110
  href="/checkout"
@@ -873,12 +1118,182 @@ export default function CartPage() {
873
1118
  }
874
1119
  ```
875
1120
 
876
- ### Multi-Step Checkout
1121
+ ### Guest Checkout (Single API Call)
1122
+
1123
+ This is the recommended checkout for guest users. All cart data is in localStorage, and we submit it in one API call.
1124
+
1125
+ ```typescript
1126
+ 'use client';
1127
+ import { useState, useEffect } from 'react';
1128
+ import { omni } from '@/lib/omni-sync';
1129
+ import type { LocalCart, GuestOrderResponse } from 'omni-sync-sdk';
1130
+
1131
+ type Step = 'info' | 'review' | 'complete';
1132
+
1133
+ export default function CheckoutPage() {
1134
+ const [cart, setCart] = useState<LocalCart>(omni.getLocalCart());
1135
+ const [step, setStep] = useState<Step>('info');
1136
+ const [order, setOrder] = useState<GuestOrderResponse | null>(null);
1137
+ const [submitting, setSubmitting] = useState(false);
1138
+ const [error, setError] = useState('');
1139
+
1140
+ // Form state
1141
+ const [email, setEmail] = useState(cart.customer?.email || '');
1142
+ const [firstName, setFirstName] = useState(cart.customer?.firstName || '');
1143
+ const [lastName, setLastName] = useState(cart.customer?.lastName || '');
1144
+ const [address, setAddress] = useState(cart.shippingAddress?.line1 || '');
1145
+ const [city, setCity] = useState(cart.shippingAddress?.city || '');
1146
+ const [postalCode, setPostalCode] = useState(cart.shippingAddress?.postalCode || '');
1147
+ const [country, setCountry] = useState(cart.shippingAddress?.country || 'US');
1148
+
1149
+ // Calculate subtotal
1150
+ const subtotal = cart.items.reduce((sum, item) => {
1151
+ return sum + (parseFloat(item.price || '0') * item.quantity);
1152
+ }, 0);
1153
+
1154
+ // Redirect if cart is empty
1155
+ useEffect(() => {
1156
+ if (cart.items.length === 0 && step !== 'complete') {
1157
+ window.location.href = '/cart';
1158
+ }
1159
+ }, [cart.items.length, step]);
1160
+
1161
+ const handleInfoSubmit = (e: React.FormEvent) => {
1162
+ e.preventDefault();
1163
+
1164
+ // Save to local cart
1165
+ omni.setLocalCartCustomer({ email, firstName, lastName });
1166
+ omni.setLocalCartShippingAddress({
1167
+ firstName,
1168
+ lastName,
1169
+ line1: address,
1170
+ city,
1171
+ postalCode,
1172
+ country,
1173
+ });
1174
+
1175
+ setStep('review');
1176
+ };
1177
+
1178
+ const handlePlaceOrder = async () => {
1179
+ setSubmitting(true);
1180
+ setError('');
1181
+
1182
+ try {
1183
+ // Single API call to create order!
1184
+ const result = await omni.submitGuestOrder();
1185
+ setOrder(result);
1186
+ setStep('complete');
1187
+ } catch (err) {
1188
+ setError(err instanceof Error ? err.message : 'Failed to place order');
1189
+ } finally {
1190
+ setSubmitting(false);
1191
+ }
1192
+ };
1193
+
1194
+ if (step === 'complete' && order) {
1195
+ return (
1196
+ <div className="text-center py-12">
1197
+ <h1 className="text-3xl font-bold text-green-600">Order Complete!</h1>
1198
+ <p className="mt-4">Order Number: <strong>{order.orderNumber}</strong></p>
1199
+ <p className="mt-2">Total: <strong>${order.total.toFixed(2)}</strong></p>
1200
+ <p className="mt-4 text-gray-600">A confirmation email will be sent to {email}</p>
1201
+ <a href="/" className="mt-6 inline-block text-blue-600">Continue Shopping</a>
1202
+ </div>
1203
+ );
1204
+ }
1205
+
1206
+ return (
1207
+ <div className="max-w-2xl mx-auto">
1208
+ <h1 className="text-2xl font-bold mb-6">Checkout</h1>
1209
+
1210
+ {error && (
1211
+ <div className="bg-red-100 text-red-600 p-3 rounded mb-4">{error}</div>
1212
+ )}
1213
+
1214
+ {step === 'info' && (
1215
+ <form onSubmit={handleInfoSubmit} className="space-y-4">
1216
+ <h2 className="text-lg font-bold">Contact Information</h2>
1217
+ <input
1218
+ type="email"
1219
+ placeholder="Email"
1220
+ value={email}
1221
+ onChange={e => setEmail(e.target.value)}
1222
+ required
1223
+ className="w-full border p-2 rounded"
1224
+ />
1225
+
1226
+ <h2 className="text-lg font-bold mt-6">Shipping Address</h2>
1227
+ <div className="grid grid-cols-2 gap-4">
1228
+ <input placeholder="First Name" value={firstName} onChange={e => setFirstName(e.target.value)} required className="border p-2 rounded" />
1229
+ <input placeholder="Last Name" value={lastName} onChange={e => setLastName(e.target.value)} required className="border p-2 rounded" />
1230
+ </div>
1231
+ <input placeholder="Address" value={address} onChange={e => setAddress(e.target.value)} required className="w-full border p-2 rounded" />
1232
+ <div className="grid grid-cols-2 gap-4">
1233
+ <input placeholder="City" value={city} onChange={e => setCity(e.target.value)} required className="border p-2 rounded" />
1234
+ <input placeholder="Postal Code" value={postalCode} onChange={e => setPostalCode(e.target.value)} required className="border p-2 rounded" />
1235
+ </div>
1236
+ <select value={country} onChange={e => setCountry(e.target.value)} className="w-full border p-2 rounded">
1237
+ <option value="US">United States</option>
1238
+ <option value="IL">Israel</option>
1239
+ <option value="GB">United Kingdom</option>
1240
+ </select>
1241
+
1242
+ <button type="submit" className="w-full bg-black text-white py-3 rounded">
1243
+ Review Order
1244
+ </button>
1245
+ </form>
1246
+ )}
1247
+
1248
+ {step === 'review' && (
1249
+ <div className="space-y-6">
1250
+ {/* Order Summary */}
1251
+ <div className="border p-4 rounded">
1252
+ <h3 className="font-bold mb-4">Order Summary</h3>
1253
+ {cart.items.map((item) => (
1254
+ <div key={`${item.productId}-${item.variantId || ''}`} className="flex justify-between py-2">
1255
+ <span>{item.name} x {item.quantity}</span>
1256
+ <span>${(parseFloat(item.price || '0') * item.quantity).toFixed(2)}</span>
1257
+ </div>
1258
+ ))}
1259
+ <hr className="my-2" />
1260
+ <div className="flex justify-between font-bold">
1261
+ <span>Total</span>
1262
+ <span>${subtotal.toFixed(2)}</span>
1263
+ </div>
1264
+ </div>
1265
+
1266
+ {/* Shipping Info */}
1267
+ <div className="border p-4 rounded">
1268
+ <h3 className="font-bold mb-2">Shipping To</h3>
1269
+ <p>{firstName} {lastName}</p>
1270
+ <p>{address}</p>
1271
+ <p>{city}, {postalCode}, {country}</p>
1272
+ <button onClick={() => setStep('info')} className="text-blue-600 text-sm mt-2">Edit</button>
1273
+ </div>
1274
+
1275
+ <button
1276
+ onClick={handlePlaceOrder}
1277
+ disabled={submitting}
1278
+ className="w-full bg-green-600 text-white py-3 rounded text-lg"
1279
+ >
1280
+ {submitting ? 'Processing...' : 'Place Order'}
1281
+ </button>
1282
+ </div>
1283
+ )}
1284
+ </div>
1285
+ );
1286
+ }
1287
+ ```
1288
+
1289
+ ### Multi-Step Checkout (Server Cart - For Registered Users)
1290
+
1291
+ For logged-in users with server-side cart:
877
1292
 
878
1293
  ```typescript
879
1294
  'use client';
880
1295
  import { useEffect, useState } from 'react';
881
- import { omni, getCartId, clearCartId } from '@/lib/omni-sync';
1296
+ import { omni, getServerCartId } from '@/lib/omni-sync';
882
1297
  import type { Checkout, ShippingRate } from 'omni-sync-sdk';
883
1298
 
884
1299
  type Step = 'customer' | 'shipping' | 'payment' | 'complete';
@@ -901,7 +1316,7 @@ export default function CheckoutPage() {
901
1316
 
902
1317
  useEffect(() => {
903
1318
  async function initCheckout() {
904
- const cartId = getCartId();
1319
+ const cartId = getServerCartId();
905
1320
  if (!cartId) {
906
1321
  window.location.href = '/cart';
907
1322
  return;
@@ -953,7 +1368,6 @@ export default function CheckoutPage() {
953
1368
  setSubmitting(true);
954
1369
  try {
955
1370
  const { orderId } = await omni.completeCheckout(checkout.id);
956
- clearCartId();
957
1371
  setStep('complete');
958
1372
  } catch (err) {
959
1373
  alert('Failed to complete order');
@@ -1207,12 +1621,12 @@ export default function AccountPage() {
1207
1621
  }
1208
1622
  ```
1209
1623
 
1210
- ### Header Component with Cart Count
1624
+ ### Header Component with Cart Count (Local Cart)
1211
1625
 
1212
1626
  ```typescript
1213
1627
  'use client';
1214
- import { useEffect, useState } from 'react';
1215
- import { omni, getCartId, isLoggedIn } from '@/lib/omni-sync';
1628
+ import { useState, useEffect } from 'react';
1629
+ import { omni, isLoggedIn } from '@/lib/omni-sync';
1216
1630
 
1217
1631
  export function Header() {
1218
1632
  const [cartCount, setCartCount] = useState(0);
@@ -1220,17 +1634,8 @@ export function Header() {
1220
1634
 
1221
1635
  useEffect(() => {
1222
1636
  setLoggedIn(isLoggedIn());
1223
-
1224
- async function loadCart() {
1225
- const cartId = getCartId();
1226
- if (cartId) {
1227
- try {
1228
- const cart = await omni.getCart(cartId);
1229
- setCartCount(cart.itemCount);
1230
- } catch {}
1231
- }
1232
- }
1233
- loadCart();
1637
+ // Get cart count from local storage (NO API call!)
1638
+ setCartCount(omni.getLocalCartItemCount());
1234
1639
  }, []);
1235
1640
 
1236
1641
  return (
@@ -1318,15 +1723,15 @@ export async function POST(req: Request) {
1318
1723
 
1319
1724
  ### Webhook Events
1320
1725
 
1321
- | Event | Description |
1322
- |-------|-------------|
1323
- | `product.created` | New product created |
1324
- | `product.updated` | Product details changed |
1325
- | `product.deleted` | Product removed |
1326
- | `inventory.updated` | Stock levels changed |
1327
- | `order.created` | New order received |
1328
- | `order.updated` | Order status changed |
1329
- | `cart.abandoned` | Cart abandoned (no activity) |
1726
+ | Event | Description |
1727
+ | -------------------- | ------------------------------- |
1728
+ | `product.created` | New product created |
1729
+ | `product.updated` | Product details changed |
1730
+ | `product.deleted` | Product removed |
1731
+ | `inventory.updated` | Stock levels changed |
1732
+ | `order.created` | New order received |
1733
+ | `order.updated` | Order status changed |
1734
+ | `cart.abandoned` | Cart abandoned (no activity) |
1330
1735
  | `checkout.completed` | Checkout completed successfully |
1331
1736
 
1332
1737
  ---
@@ -1345,7 +1750,13 @@ import type {
1345
1750
  ProductQueryParams,
1346
1751
  PaginatedResponse,
1347
1752
 
1348
- // Cart
1753
+ // Local Cart (Guest Users)
1754
+ LocalCart,
1755
+ LocalCartItem,
1756
+ CreateGuestOrderDto,
1757
+ GuestOrderResponse,
1758
+
1759
+ // Server Cart (Registered Users)
1349
1760
  Cart,
1350
1761
  CartItem,
1351
1762
  AddToCartDto,
@@ -1411,6 +1822,7 @@ When building a store, implement these pages:
1411
1822
  ## Important Rules
1412
1823
 
1413
1824
  ### DO:
1825
+
1414
1826
  - Install `omni-sync-sdk` and use it for ALL data
1415
1827
  - Import types from the SDK
1416
1828
  - Handle loading states and errors
@@ -1418,10 +1830,12 @@ When building a store, implement these pages:
1418
1830
  - Persist customer token after login
1419
1831
 
1420
1832
  ### DON'T:
1833
+
1421
1834
  - Create mock/hardcoded product data
1422
1835
  - Use localStorage for products
1423
1836
  - Skip implementing required pages
1424
1837
  - Write `const products = [...]` - use the API!
1838
+ - Use `@apply group` in CSS - Tailwind doesn't allow 'group' in @apply. Use `className="group"` on the element instead
1425
1839
 
1426
1840
  ---
1427
1841