omni-sync-sdk 0.3.0 → 0.4.1

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.
Files changed (2) hide show
  1. package/README.md +1322 -78
  2. package/package.json +24 -3
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # omni-sync-sdk
2
2
 
3
- SDK for integrating vibe coding stores with Omni-Sync Platform.
3
+ Official SDK for building e-commerce storefronts with **OmniSync Platform**.
4
+
5
+ This SDK provides a complete solution for vibe-coded sites, AI-built stores (Cursor, Lovable, v0), and custom storefronts to connect to OmniSync's unified commerce API.
4
6
 
5
7
  ## Installation
6
8
 
@@ -12,99 +14,1269 @@ pnpm add omni-sync-sdk
12
14
  yarn add omni-sync-sdk
13
15
  ```
14
16
 
17
+ ---
18
+
15
19
  ## Quick Start
16
20
 
21
+ ### For Vibe-Coded Sites (Recommended)
22
+
17
23
  ```typescript
18
24
  import { OmniSyncClient } from 'omni-sync-sdk';
19
25
 
26
+ // Initialize with your Connection ID
20
27
  const omni = new OmniSyncClient({
21
- apiKey: process.env.OMNI_SYNC_API_KEY!,
28
+ connectionId: 'vc_YOUR_CONNECTION_ID',
22
29
  });
23
30
 
24
- // Get products
31
+ // Fetch products
25
32
  const { data: products } = await omni.getProducts();
26
33
 
27
- // Create an order
28
- const order = await omni.createOrder({
29
- items: [{ productId: products[0].id, quantity: 2, price: 99.99 }],
30
- customer: { email: 'customer@example.com', name: 'John Doe' },
31
- totalAmount: 199.98,
34
+ // Create a cart
35
+ const cart = await omni.createCart();
36
+
37
+ // Add item to cart
38
+ await omni.addToCart(cart.id, {
39
+ productId: products[0].id,
40
+ quantity: 1,
32
41
  });
33
42
  ```
34
43
 
35
- ## Products
44
+ ---
45
+
46
+ ## Complete Store Setup
47
+
48
+ ### Step 1: Create the OmniSync Client
49
+
50
+ Create a file `lib/omni-sync.ts`:
51
+
52
+ ```typescript
53
+ import { OmniSyncClient } from 'omni-sync-sdk';
54
+
55
+ export const omni = new OmniSyncClient({
56
+ connectionId: 'vc_YOUR_CONNECTION_ID', // Your Connection ID from OmniSync
57
+ });
58
+
59
+ // ----- Cart Helpers -----
60
+
61
+ export function getCartId(): string | null {
62
+ if (typeof window === 'undefined') return null;
63
+ return localStorage.getItem('cartId');
64
+ }
65
+
66
+ export function setCartId(id: string): void {
67
+ localStorage.setItem('cartId', id);
68
+ }
69
+
70
+ export function clearCartId(): void {
71
+ localStorage.removeItem('cartId');
72
+ }
73
+
74
+ // ----- Customer Token Helpers -----
75
+
76
+ export function setCustomerToken(token: string | null): void {
77
+ if (token) {
78
+ localStorage.setItem('customerToken', token);
79
+ omni.setCustomerToken(token);
80
+ } else {
81
+ localStorage.removeItem('customerToken');
82
+ omni.clearCustomerToken();
83
+ }
84
+ }
85
+
86
+ export function restoreCustomerToken(): string | null {
87
+ const token = localStorage.getItem('customerToken');
88
+ if (token) omni.setCustomerToken(token);
89
+ return token;
90
+ }
91
+
92
+ export function isLoggedIn(): boolean {
93
+ return !!localStorage.getItem('customerToken');
94
+ }
95
+ ```
96
+
97
+ ---
98
+
99
+ ## API Reference
100
+
101
+ ### Products
102
+
103
+ #### Get Products (with pagination)
36
104
 
37
105
  ```typescript
38
- // List products with pagination
39
- const { data, meta } = await omni.getProducts({
106
+ import { omni } from '@/lib/omni-sync';
107
+ import type { Product, PaginatedResponse } from 'omni-sync-sdk';
108
+
109
+ const response: PaginatedResponse<Product> = await omni.getProducts({
40
110
  page: 1,
41
- limit: 20,
42
- status: 'active',
43
- search: 'shirt',
111
+ 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'
44
117
  });
45
118
 
46
- // Get single product
47
- const product = await omni.getProduct('prod_123');
119
+ console.log(response.data); // Product[]
120
+ console.log(response.meta.total); // Total number of products
121
+ console.log(response.meta.totalPages); // Total pages
122
+ ```
123
+
124
+ #### Get Single Product
125
+
126
+ ```typescript
127
+ const product: Product = await omni.getProduct('product_id');
128
+
129
+ console.log(product.name);
130
+ 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 }
135
+ ```
136
+
137
+ #### Product Type Definition
48
138
 
49
- // Create product
50
- const newProduct = await omni.createProduct({
51
- name: 'Blue T-Shirt',
52
- sku: 'TSHIRT-BLUE-M',
53
- basePrice: 29.99,
54
- status: 'active',
139
+ ```typescript
140
+ interface Product {
141
+ id: string;
142
+ name: string;
143
+ description?: string | null;
144
+ sku: string;
145
+ basePrice: number;
146
+ salePrice?: number | null;
147
+ status: 'active' | 'draft' | 'archived';
148
+ type: 'SIMPLE' | 'VARIABLE';
149
+ images?: ProductImage[];
150
+ inventory?: InventoryInfo | null;
151
+ variants?: ProductVariant[];
152
+ categories?: string[];
153
+ tags?: string[];
154
+ createdAt: string;
155
+ updatedAt: string;
156
+ }
157
+
158
+ interface ProductImage {
159
+ url: string;
160
+ position?: number;
161
+ isMain?: boolean;
162
+ }
163
+
164
+ interface ProductVariant {
165
+ id: string;
166
+ sku?: string | null;
167
+ name?: string | null;
168
+ price?: number | null;
169
+ salePrice?: number | null;
170
+ attributes?: Record<string, string>;
171
+ inventory?: InventoryInfo | null;
172
+ }
173
+
174
+ interface InventoryInfo {
175
+ total: number;
176
+ reserved: number;
177
+ available: number;
178
+ }
179
+ ```
180
+
181
+ ---
182
+
183
+ ### Cart
184
+
185
+ #### Create Cart
186
+
187
+ ```typescript
188
+ const cart = await omni.createCart();
189
+ setCartId(cart.id); // Save to localStorage
190
+ ```
191
+
192
+ #### Get Cart
193
+
194
+ ```typescript
195
+ const cartId = getCartId();
196
+ if (cartId) {
197
+ 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
201
+ }
202
+ ```
203
+
204
+ #### Add to Cart
205
+
206
+ ```typescript
207
+ const cart = await omni.addToCart(cartId, {
208
+ productId: 'product_id',
209
+ variantId: 'variant_id', // Optional: for VARIABLE products
210
+ quantity: 2,
211
+ notes: 'Gift wrap please', // Optional
55
212
  });
213
+ ```
214
+
215
+ #### Update Cart Item
56
216
 
57
- // Update product
58
- const updated = await omni.updateProduct('prod_123', {
59
- salePrice: 24.99,
217
+ ```typescript
218
+ const cart = await omni.updateCartItem(cartId, itemId, {
219
+ quantity: 3,
60
220
  });
221
+ ```
222
+
223
+ #### Remove Cart Item
224
+
225
+ ```typescript
226
+ const cart = await omni.removeCartItem(cartId, itemId);
227
+ ```
228
+
229
+ #### Apply Coupon
230
+
231
+ ```typescript
232
+ const cart = await omni.applyCoupon(cartId, 'SAVE20');
233
+ console.log(cart.discountAmount); // Discount applied
234
+ console.log(cart.couponCode); // 'SAVE20'
235
+ ```
236
+
237
+ #### Remove Coupon
61
238
 
62
- // Delete product
63
- await omni.deleteProduct('prod_123');
239
+ ```typescript
240
+ const cart = await omni.removeCoupon(cartId);
64
241
  ```
65
242
 
66
- ## Orders
243
+ #### Cart Type Definition
67
244
 
68
245
  ```typescript
69
- // Create order (auto-deducts inventory, syncs to all platforms)
70
- const order = await omni.createOrder({
71
- items: [
72
- { productId: 'prod_123', quantity: 1, price: 29.99 },
73
- { productId: 'prod_456', quantity: 2, price: 49.99 },
74
- ],
246
+ interface Cart {
247
+ id: string;
248
+ sessionToken?: string | null;
249
+ customerId?: string | null;
250
+ status: 'ACTIVE' | 'MERGED' | 'CONVERTED' | 'ABANDONED';
251
+ currency: string;
252
+ subtotal: string;
253
+ discountAmount: string;
254
+ couponCode?: string | null;
255
+ items: CartItem[];
256
+ itemCount: number;
257
+ createdAt: string;
258
+ updatedAt: string;
259
+ }
260
+
261
+ interface CartItem {
262
+ id: string;
263
+ productId: string;
264
+ variantId?: string | null;
265
+ quantity: number;
266
+ unitPrice: string;
267
+ discountAmount: string;
268
+ notes?: string | null;
269
+ product: {
270
+ id: string;
271
+ name: string;
272
+ sku: string;
273
+ images?: unknown[];
274
+ };
275
+ variant?: {
276
+ id: string;
277
+ name?: string | null;
278
+ sku?: string | null;
279
+ } | null;
280
+ }
281
+ ```
282
+
283
+ ---
284
+
285
+ ### Checkout
286
+
287
+ #### Create Checkout from Cart
288
+
289
+ ```typescript
290
+ const checkout = await omni.createCheckout({
291
+ cartId: cartId,
292
+ });
293
+ ```
294
+
295
+ #### Set Customer Information
296
+
297
+ ```typescript
298
+ const checkout = await omni.setCheckoutCustomer(checkoutId, {
299
+ email: 'customer@example.com',
300
+ firstName: 'John',
301
+ lastName: 'Doe',
302
+ phone: '+1234567890', // Optional
303
+ });
304
+ ```
305
+
306
+ #### Set Shipping Address
307
+
308
+ ```typescript
309
+ const { checkout, rates } = await omni.setShippingAddress(checkoutId, {
310
+ firstName: 'John',
311
+ lastName: 'Doe',
312
+ line1: '123 Main St',
313
+ line2: 'Apt 4B', // Optional
314
+ city: 'New York',
315
+ region: 'NY', // State/Province
316
+ postalCode: '10001',
317
+ country: 'US',
318
+ phone: '+1234567890', // Optional
319
+ });
320
+
321
+ // rates contains available shipping options
322
+ console.log(rates); // ShippingRate[]
323
+ ```
324
+
325
+ #### Select Shipping Method
326
+
327
+ ```typescript
328
+ const checkout = await omni.selectShippingMethod(checkoutId, rates[0].id);
329
+ ```
330
+
331
+ #### Set Billing Address
332
+
333
+ ```typescript
334
+ // Same as shipping
335
+ const checkout = await omni.setBillingAddress(checkoutId, {
336
+ ...shippingAddress,
337
+ sameAsShipping: true, // Optional shortcut
338
+ });
339
+ ```
340
+
341
+ #### Complete Checkout
342
+
343
+ ```typescript
344
+ const { orderId } = await omni.completeCheckout(checkoutId);
345
+ clearCartId(); // Clear cart from localStorage
346
+ console.log('Order created:', orderId);
347
+ ```
348
+
349
+ #### Checkout Type Definition
350
+
351
+ ```typescript
352
+ interface Checkout {
353
+ id: string;
354
+ status: CheckoutStatus;
355
+ email?: string | null;
356
+ shippingAddress?: CheckoutAddress | null;
357
+ billingAddress?: CheckoutAddress | null;
358
+ shippingMethod?: ShippingRate | null;
359
+ currency: string;
360
+ subtotal: string;
361
+ discountAmount: string;
362
+ shippingAmount: string;
363
+ taxAmount: string;
364
+ total: string;
365
+ couponCode?: string | null;
366
+ items: CheckoutLineItem[];
367
+ itemCount: number;
368
+ availableShippingRates?: ShippingRate[];
369
+ }
370
+
371
+ type CheckoutStatus = 'PENDING' | 'SHIPPING_SET' | 'PAYMENT_PENDING' | 'COMPLETED' | 'FAILED';
372
+
373
+ interface ShippingRate {
374
+ id: string;
375
+ name: string;
376
+ description?: string | null;
377
+ price: string;
378
+ currency: string;
379
+ estimatedDays?: number | null;
380
+ }
381
+ ```
382
+
383
+ ---
384
+
385
+ ### Customer Authentication
386
+
387
+ #### Register Customer
388
+
389
+ ```typescript
390
+ const auth = await omni.registerCustomer({
391
+ email: 'customer@example.com',
392
+ password: 'securepassword123',
393
+ firstName: 'John',
394
+ lastName: 'Doe',
395
+ });
396
+
397
+ setCustomerToken(auth.token);
398
+ console.log('Registered:', auth.customer.email);
399
+ ```
400
+
401
+ #### Login Customer
402
+
403
+ ```typescript
404
+ const auth = await omni.loginCustomer('customer@example.com', 'password123');
405
+ setCustomerToken(auth.token);
406
+ ```
407
+
408
+ #### Logout Customer
409
+
410
+ ```typescript
411
+ setCustomerToken(null);
412
+ ```
413
+
414
+ #### Get Customer Profile
415
+
416
+ ```typescript
417
+ restoreCustomerToken(); // Restore from localStorage
418
+ const profile = await omni.getMyProfile();
419
+
420
+ console.log(profile.firstName);
421
+ console.log(profile.email);
422
+ console.log(profile.addresses);
423
+ ```
424
+
425
+ #### Get Customer Orders
426
+
427
+ ```typescript
428
+ const { data: orders, meta } = await omni.getMyOrders({
429
+ page: 1,
430
+ limit: 10,
431
+ });
432
+ ```
433
+
434
+ #### Auth Response Type
435
+
436
+ ```typescript
437
+ interface CustomerAuthResponse {
75
438
  customer: {
76
- email: 'john@example.com',
77
- name: 'John Doe',
78
- phone: '+1234567890',
79
- },
80
- totalAmount: 129.97,
439
+ id: string;
440
+ email: string;
441
+ firstName?: string;
442
+ lastName?: string;
443
+ emailVerified: boolean;
444
+ };
445
+ token: string;
446
+ expiresAt: string;
447
+ }
448
+ ```
449
+
450
+ ---
451
+
452
+ ### Customer Addresses
453
+
454
+ #### Get Addresses
455
+
456
+ ```typescript
457
+ const addresses = await omni.getMyAddresses();
458
+ ```
459
+
460
+ #### Add Address
461
+
462
+ ```typescript
463
+ const address = await omni.addMyAddress({
464
+ firstName: 'John',
465
+ lastName: 'Doe',
466
+ line1: '123 Main St',
467
+ city: 'New York',
468
+ region: 'NY',
469
+ postalCode: '10001',
470
+ country: 'US',
471
+ isDefault: true,
81
472
  });
473
+ ```
82
474
 
83
- // List orders
84
- const { data: orders } = await omni.getOrders({
85
- status: 'pending',
475
+ #### Update Address
476
+
477
+ ```typescript
478
+ const updated = await omni.updateMyAddress(addressId, {
479
+ line1: '456 New Street',
86
480
  });
481
+ ```
482
+
483
+ #### Delete Address
87
484
 
88
- // Update order status
89
- await omni.updateOrder('order_123', { status: 'shipped' });
485
+ ```typescript
486
+ await omni.deleteMyAddress(addressId);
90
487
  ```
91
488
 
92
- ## Inventory
489
+ ---
490
+
491
+ ### Store Info
492
+
493
+ ```typescript
494
+ const store = await omni.getStoreInfo();
495
+
496
+ console.log(store.name); // Store name
497
+ console.log(store.currency); // 'USD', 'ILS', etc.
498
+ console.log(store.language); // 'en', 'he', etc.
499
+ ```
500
+
501
+ ---
502
+
503
+ ## Complete Page Examples
504
+
505
+ ### Home Page
93
506
 
94
507
  ```typescript
95
- // Get current inventory
96
- const inventory = await omni.getInventory('prod_123');
97
- console.log(`Available: ${inventory.available}`);
508
+ 'use client';
509
+ import { useEffect, useState } from 'react';
510
+ import { omni } from '@/lib/omni-sync';
511
+ import type { Product } from 'omni-sync-sdk';
512
+
513
+ export default function HomePage() {
514
+ const [products, setProducts] = useState<Product[]>([]);
515
+ const [loading, setLoading] = useState(true);
516
+ const [error, setError] = useState<string | null>(null);
517
+
518
+ useEffect(() => {
519
+ async function loadProducts() {
520
+ try {
521
+ const { data } = await omni.getProducts({ limit: 8 });
522
+ setProducts(data);
523
+ } catch (err) {
524
+ setError('Failed to load products');
525
+ } finally {
526
+ setLoading(false);
527
+ }
528
+ }
529
+ loadProducts();
530
+ }, []);
531
+
532
+ if (loading) return <div>Loading...</div>;
533
+ if (error) return <div>{error}</div>;
534
+
535
+ return (
536
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-6">
537
+ {products.map((product) => (
538
+ <a key={product.id} href={`/products/${product.id}`} className="group">
539
+ <img
540
+ src={product.images?.[0]?.url || '/placeholder.jpg'}
541
+ alt={product.name}
542
+ className="w-full aspect-square object-cover"
543
+ />
544
+ <h3 className="mt-2 font-medium">{product.name}</h3>
545
+ <p className="text-lg">
546
+ {product.salePrice ? (
547
+ <>
548
+ <span className="text-red-600">${product.salePrice}</span>
549
+ <span className="line-through text-gray-400 ml-2">${product.basePrice}</span>
550
+ </>
551
+ ) : (
552
+ <span>${product.basePrice}</span>
553
+ )}
554
+ </p>
555
+ </a>
556
+ ))}
557
+ </div>
558
+ );
559
+ }
560
+ ```
561
+
562
+ ### Products List with Pagination
563
+
564
+ ```typescript
565
+ 'use client';
566
+ import { useEffect, useState } from 'react';
567
+ import { omni } from '@/lib/omni-sync';
568
+ import type { Product, PaginatedResponse } from 'omni-sync-sdk';
569
+
570
+ export default function ProductsPage() {
571
+ const [data, setData] = useState<PaginatedResponse<Product> | null>(null);
572
+ const [page, setPage] = useState(1);
573
+ const [loading, setLoading] = useState(true);
98
574
 
99
- // Update inventory (syncs to all platforms)
100
- await omni.updateInventory('prod_123', { quantity: 50 });
575
+ useEffect(() => {
576
+ async function load() {
577
+ setLoading(true);
578
+ try {
579
+ const result = await omni.getProducts({ page, limit: 12 });
580
+ setData(result);
581
+ } finally {
582
+ setLoading(false);
583
+ }
584
+ }
585
+ load();
586
+ }, [page]);
587
+
588
+ if (loading) return <div>Loading...</div>;
589
+ if (!data) return <div>No products found</div>;
590
+
591
+ return (
592
+ <div>
593
+ <div className="grid grid-cols-3 gap-6">
594
+ {data.data.map((product) => (
595
+ <a key={product.id} href={`/products/${product.id}`}>
596
+ <img src={product.images?.[0]?.url} alt={product.name} />
597
+ <h3>{product.name}</h3>
598
+ <p>${product.salePrice || product.basePrice}</p>
599
+ </a>
600
+ ))}
601
+ </div>
602
+
603
+ {/* Pagination */}
604
+ <div className="flex gap-2 mt-8">
605
+ <button onClick={() => setPage(p => Math.max(1, p - 1))} disabled={page === 1}>
606
+ Previous
607
+ </button>
608
+ <span>Page {data.meta.page} of {data.meta.totalPages}</span>
609
+ <button onClick={() => setPage(p => p + 1)} disabled={page >= data.meta.totalPages}>
610
+ Next
611
+ </button>
612
+ </div>
613
+ </div>
614
+ );
615
+ }
101
616
  ```
102
617
 
618
+ ### Product Detail with Add to Cart
619
+
620
+ ```typescript
621
+ 'use client';
622
+ import { useEffect, useState } from 'react';
623
+ import { omni, getCartId, setCartId } from '@/lib/omni-sync';
624
+ import type { Product } from 'omni-sync-sdk';
625
+
626
+ export default function ProductPage({ params }: { params: { id: string } }) {
627
+ const [product, setProduct] = useState<Product | null>(null);
628
+ const [selectedVariant, setSelectedVariant] = useState<string | null>(null);
629
+ const [quantity, setQuantity] = useState(1);
630
+ const [loading, setLoading] = useState(true);
631
+ const [adding, setAdding] = useState(false);
632
+
633
+ useEffect(() => {
634
+ async function load() {
635
+ try {
636
+ const p = await omni.getProduct(params.id);
637
+ setProduct(p);
638
+ if (p.variants && p.variants.length > 0) {
639
+ setSelectedVariant(p.variants[0].id);
640
+ }
641
+ } finally {
642
+ setLoading(false);
643
+ }
644
+ }
645
+ load();
646
+ }, [params.id]);
647
+
648
+ const handleAddToCart = async () => {
649
+ if (!product) return;
650
+ setAdding(true);
651
+ try {
652
+ let cartId = getCartId();
653
+
654
+ if (!cartId) {
655
+ const cart = await omni.createCart();
656
+ cartId = cart.id;
657
+ setCartId(cartId);
658
+ }
659
+
660
+ await omni.addToCart(cartId, {
661
+ productId: product.id,
662
+ variantId: selectedVariant || undefined,
663
+ quantity,
664
+ });
665
+
666
+ alert('Added to cart!');
667
+ } catch (err) {
668
+ alert('Failed to add to cart');
669
+ } finally {
670
+ setAdding(false);
671
+ }
672
+ };
673
+
674
+ if (loading) return <div>Loading...</div>;
675
+ if (!product) return <div>Product not found</div>;
676
+
677
+ return (
678
+ <div className="grid grid-cols-2 gap-8">
679
+ {/* Images */}
680
+ <div>
681
+ <img
682
+ src={product.images?.[0]?.url || '/placeholder.jpg'}
683
+ alt={product.name}
684
+ className="w-full"
685
+ />
686
+ </div>
687
+
688
+ {/* Details */}
689
+ <div>
690
+ <h1 className="text-3xl font-bold">{product.name}</h1>
691
+ <p className="text-2xl mt-4">
692
+ ${product.salePrice || product.basePrice}
693
+ </p>
694
+
695
+ {product.description && (
696
+ <p className="mt-4 text-gray-600">{product.description}</p>
697
+ )}
698
+
699
+ {/* Variant Selection */}
700
+ {product.variants && product.variants.length > 0 && (
701
+ <div className="mt-6">
702
+ <label className="block font-medium mb-2">Select Option</label>
703
+ <select
704
+ value={selectedVariant || ''}
705
+ onChange={(e) => setSelectedVariant(e.target.value)}
706
+ className="border rounded p-2 w-full"
707
+ >
708
+ {product.variants.map((v) => (
709
+ <option key={v.id} value={v.id}>
710
+ {v.name || v.sku} - ${v.price || product.basePrice}
711
+ </option>
712
+ ))}
713
+ </select>
714
+ </div>
715
+ )}
716
+
717
+ {/* Quantity */}
718
+ <div className="mt-4">
719
+ <label className="block font-medium mb-2">Quantity</label>
720
+ <input
721
+ type="number"
722
+ min="1"
723
+ value={quantity}
724
+ onChange={(e) => setQuantity(Number(e.target.value))}
725
+ className="border rounded p-2 w-20"
726
+ />
727
+ </div>
728
+
729
+ {/* Add to Cart Button */}
730
+ <button
731
+ onClick={handleAddToCart}
732
+ disabled={adding}
733
+ className="mt-6 w-full bg-black text-white py-3 rounded disabled:opacity-50"
734
+ >
735
+ {adding ? 'Adding...' : 'Add to Cart'}
736
+ </button>
737
+
738
+ {/* Stock Status */}
739
+ {product.inventory && (
740
+ <p className="mt-4 text-sm">
741
+ {product.inventory.available > 0
742
+ ? `${product.inventory.available} in stock`
743
+ : 'Out of stock'}
744
+ </p>
745
+ )}
746
+ </div>
747
+ </div>
748
+ );
749
+ }
750
+ ```
751
+
752
+ ### Cart Page
753
+
754
+ ```typescript
755
+ 'use client';
756
+ import { useEffect, useState } from 'react';
757
+ import { omni, getCartId } from '@/lib/omni-sync';
758
+ import type { Cart } from 'omni-sync-sdk';
759
+
760
+ export default function CartPage() {
761
+ const [cart, setCart] = useState<Cart | null>(null);
762
+ const [loading, setLoading] = useState(true);
763
+ const [updating, setUpdating] = useState<string | null>(null);
764
+
765
+ const loadCart = async () => {
766
+ const cartId = getCartId();
767
+ if (!cartId) {
768
+ setLoading(false);
769
+ return;
770
+ }
771
+ try {
772
+ const c = await omni.getCart(cartId);
773
+ setCart(c);
774
+ } finally {
775
+ setLoading(false);
776
+ }
777
+ };
778
+
779
+ useEffect(() => { loadCart(); }, []);
780
+
781
+ const updateQuantity = async (itemId: string, quantity: number) => {
782
+ if (!cart) return;
783
+ setUpdating(itemId);
784
+ try {
785
+ if (quantity <= 0) {
786
+ await omni.removeCartItem(cart.id, itemId);
787
+ } else {
788
+ await omni.updateCartItem(cart.id, itemId, { quantity });
789
+ }
790
+ await loadCart();
791
+ } finally {
792
+ setUpdating(null);
793
+ }
794
+ };
795
+
796
+ const removeItem = async (itemId: string) => {
797
+ if (!cart) return;
798
+ setUpdating(itemId);
799
+ try {
800
+ await omni.removeCartItem(cart.id, itemId);
801
+ await loadCart();
802
+ } finally {
803
+ setUpdating(null);
804
+ }
805
+ };
806
+
807
+ if (loading) return <div>Loading cart...</div>;
808
+ if (!cart || cart.items.length === 0) {
809
+ return (
810
+ <div className="text-center py-12">
811
+ <h1 className="text-2xl font-bold">Your cart is empty</h1>
812
+ <a href="/products" className="text-blue-600 mt-4 inline-block">Continue Shopping</a>
813
+ </div>
814
+ );
815
+ }
816
+
817
+ return (
818
+ <div>
819
+ <h1 className="text-2xl font-bold mb-6">Shopping Cart</h1>
820
+
821
+ {cart.items.map((item) => (
822
+ <div key={item.id} className="flex items-center gap-4 py-4 border-b">
823
+ <img
824
+ src={item.product.images?.[0]?.url || '/placeholder.jpg'}
825
+ alt={item.product.name}
826
+ className="w-20 h-20 object-cover"
827
+ />
828
+ <div className="flex-1">
829
+ <h3 className="font-medium">{item.product.name}</h3>
830
+ {item.variant && <p className="text-sm text-gray-500">{item.variant.name}</p>}
831
+ <p className="font-bold">${item.unitPrice}</p>
832
+ </div>
833
+ <div className="flex items-center gap-2">
834
+ <button
835
+ onClick={() => updateQuantity(item.id, item.quantity - 1)}
836
+ disabled={updating === item.id}
837
+ className="w-8 h-8 border rounded"
838
+ >-</button>
839
+ <span className="w-8 text-center">{item.quantity}</span>
840
+ <button
841
+ onClick={() => updateQuantity(item.id, item.quantity + 1)}
842
+ disabled={updating === item.id}
843
+ className="w-8 h-8 border rounded"
844
+ >+</button>
845
+ </div>
846
+ <button
847
+ onClick={() => removeItem(item.id)}
848
+ disabled={updating === item.id}
849
+ className="text-red-600"
850
+ >Remove</button>
851
+ </div>
852
+ ))}
853
+
854
+ <div className="mt-6 text-right">
855
+ <p className="text-xl">Subtotal: <strong>${cart.subtotal}</strong></p>
856
+ {cart.discountAmount && Number(cart.discountAmount) > 0 && (
857
+ <p className="text-green-600">Discount: -${cart.discountAmount}</p>
858
+ )}
859
+ <a
860
+ href="/checkout"
861
+ className="mt-4 inline-block bg-black text-white px-8 py-3 rounded"
862
+ >
863
+ Proceed to Checkout
864
+ </a>
865
+ </div>
866
+ </div>
867
+ );
868
+ }
869
+ ```
870
+
871
+ ### Multi-Step Checkout
872
+
873
+ ```typescript
874
+ 'use client';
875
+ import { useEffect, useState } from 'react';
876
+ import { omni, getCartId, clearCartId } from '@/lib/omni-sync';
877
+ import type { Checkout, ShippingRate } from 'omni-sync-sdk';
878
+
879
+ type Step = 'customer' | 'shipping' | 'payment' | 'complete';
880
+
881
+ export default function CheckoutPage() {
882
+ const [checkout, setCheckout] = useState<Checkout | null>(null);
883
+ const [step, setStep] = useState<Step>('customer');
884
+ const [shippingRates, setShippingRates] = useState<ShippingRate[]>([]);
885
+ const [loading, setLoading] = useState(true);
886
+ const [submitting, setSubmitting] = useState(false);
887
+
888
+ // Form state
889
+ const [email, setEmail] = useState('');
890
+ const [firstName, setFirstName] = useState('');
891
+ const [lastName, setLastName] = useState('');
892
+ const [address, setAddress] = useState('');
893
+ const [city, setCity] = useState('');
894
+ const [postalCode, setPostalCode] = useState('');
895
+ const [country, setCountry] = useState('US');
896
+
897
+ useEffect(() => {
898
+ async function initCheckout() {
899
+ const cartId = getCartId();
900
+ if (!cartId) {
901
+ window.location.href = '/cart';
902
+ return;
903
+ }
904
+ try {
905
+ const c = await omni.createCheckout({ cartId });
906
+ setCheckout(c);
907
+ } finally {
908
+ setLoading(false);
909
+ }
910
+ }
911
+ initCheckout();
912
+ }, []);
913
+
914
+ const handleCustomerSubmit = async (e: React.FormEvent) => {
915
+ e.preventDefault();
916
+ if (!checkout) return;
917
+ setSubmitting(true);
918
+ try {
919
+ await omni.setCheckoutCustomer(checkout.id, { email, firstName, lastName });
920
+ setStep('shipping');
921
+ } finally {
922
+ setSubmitting(false);
923
+ }
924
+ };
925
+
926
+ const handleShippingSubmit = async (e: React.FormEvent) => {
927
+ e.preventDefault();
928
+ if (!checkout) return;
929
+ setSubmitting(true);
930
+ try {
931
+ const { rates } = await omni.setShippingAddress(checkout.id, {
932
+ firstName, lastName,
933
+ line1: address,
934
+ city, postalCode, country,
935
+ });
936
+ setShippingRates(rates);
937
+ if (rates.length > 0) {
938
+ await omni.selectShippingMethod(checkout.id, rates[0].id);
939
+ }
940
+ setStep('payment');
941
+ } finally {
942
+ setSubmitting(false);
943
+ }
944
+ };
945
+
946
+ const handleCompleteOrder = async () => {
947
+ if (!checkout) return;
948
+ setSubmitting(true);
949
+ try {
950
+ const { orderId } = await omni.completeCheckout(checkout.id);
951
+ clearCartId();
952
+ setStep('complete');
953
+ } catch (err) {
954
+ alert('Failed to complete order');
955
+ } finally {
956
+ setSubmitting(false);
957
+ }
958
+ };
959
+
960
+ if (loading) return <div>Loading checkout...</div>;
961
+ if (!checkout) return <div>Failed to create checkout</div>;
962
+
963
+ if (step === 'complete') {
964
+ return (
965
+ <div className="text-center py-12">
966
+ <h1 className="text-3xl font-bold text-green-600">Order Complete!</h1>
967
+ <p className="mt-4">Thank you for your purchase.</p>
968
+ <a href="/" className="mt-6 inline-block text-blue-600">Continue Shopping</a>
969
+ </div>
970
+ );
971
+ }
972
+
973
+ return (
974
+ <div className="max-w-2xl mx-auto">
975
+ <h1 className="text-2xl font-bold mb-6">Checkout</h1>
976
+
977
+ {step === 'customer' && (
978
+ <form onSubmit={handleCustomerSubmit} className="space-y-4">
979
+ <input type="email" placeholder="Email" value={email} onChange={e => setEmail(e.target.value)} required className="w-full border p-2 rounded" />
980
+ <div className="grid grid-cols-2 gap-4">
981
+ <input placeholder="First Name" value={firstName} onChange={e => setFirstName(e.target.value)} required className="border p-2 rounded" />
982
+ <input placeholder="Last Name" value={lastName} onChange={e => setLastName(e.target.value)} required className="border p-2 rounded" />
983
+ </div>
984
+ <button type="submit" disabled={submitting} className="w-full bg-black text-white py-3 rounded">
985
+ {submitting ? 'Saving...' : 'Continue to Shipping'}
986
+ </button>
987
+ </form>
988
+ )}
989
+
990
+ {step === 'shipping' && (
991
+ <form onSubmit={handleShippingSubmit} className="space-y-4">
992
+ <input placeholder="Address" value={address} onChange={e => setAddress(e.target.value)} required className="w-full border p-2 rounded" />
993
+ <div className="grid grid-cols-2 gap-4">
994
+ <input placeholder="City" value={city} onChange={e => setCity(e.target.value)} required className="border p-2 rounded" />
995
+ <input placeholder="Postal Code" value={postalCode} onChange={e => setPostalCode(e.target.value)} required className="border p-2 rounded" />
996
+ </div>
997
+ <select value={country} onChange={e => setCountry(e.target.value)} className="w-full border p-2 rounded">
998
+ <option value="US">United States</option>
999
+ <option value="IL">Israel</option>
1000
+ <option value="GB">United Kingdom</option>
1001
+ </select>
1002
+ <button type="submit" disabled={submitting} className="w-full bg-black text-white py-3 rounded">
1003
+ {submitting ? 'Calculating Shipping...' : 'Continue to Payment'}
1004
+ </button>
1005
+ </form>
1006
+ )}
1007
+
1008
+ {step === 'payment' && (
1009
+ <div className="space-y-6">
1010
+ <div className="border p-4 rounded">
1011
+ <h3 className="font-bold mb-2">Order Summary</h3>
1012
+ <p>Subtotal: ${checkout.subtotal}</p>
1013
+ <p>Shipping: ${checkout.shippingAmount}</p>
1014
+ <p className="text-xl font-bold mt-2">Total: ${checkout.total}</p>
1015
+ </div>
1016
+ <button onClick={handleCompleteOrder} disabled={submitting} className="w-full bg-green-600 text-white py-3 rounded text-lg">
1017
+ {submitting ? 'Processing...' : 'Complete Order'}
1018
+ </button>
1019
+ </div>
1020
+ )}
1021
+ </div>
1022
+ );
1023
+ }
1024
+ ```
1025
+
1026
+ ### Login Page
1027
+
1028
+ ```typescript
1029
+ 'use client';
1030
+ import { useState } from 'react';
1031
+ import { omni, setCustomerToken } from '@/lib/omni-sync';
1032
+
1033
+ export default function LoginPage() {
1034
+ const [email, setEmail] = useState('');
1035
+ const [password, setPassword] = useState('');
1036
+ const [error, setError] = useState('');
1037
+ const [loading, setLoading] = useState(false);
1038
+
1039
+ const handleSubmit = async (e: React.FormEvent) => {
1040
+ e.preventDefault();
1041
+ setLoading(true);
1042
+ setError('');
1043
+ try {
1044
+ const auth = await omni.loginCustomer(email, password);
1045
+ setCustomerToken(auth.token);
1046
+ window.location.href = '/account';
1047
+ } catch (err) {
1048
+ setError('Invalid email or password');
1049
+ } finally {
1050
+ setLoading(false);
1051
+ }
1052
+ };
1053
+
1054
+ return (
1055
+ <div className="max-w-md mx-auto mt-12">
1056
+ <h1 className="text-2xl font-bold mb-6">Login</h1>
1057
+ {error && <div className="bg-red-100 text-red-600 p-3 rounded mb-4">{error}</div>}
1058
+ <form onSubmit={handleSubmit} className="space-y-4">
1059
+ <input type="email" placeholder="Email" value={email} onChange={e => setEmail(e.target.value)} required className="w-full border p-2 rounded" />
1060
+ <input type="password" placeholder="Password" value={password} onChange={e => setPassword(e.target.value)} required className="w-full border p-2 rounded" />
1061
+ <button type="submit" disabled={loading} className="w-full bg-black text-white py-3 rounded">
1062
+ {loading ? 'Logging in...' : 'Login'}
1063
+ </button>
1064
+ </form>
1065
+ <p className="mt-4 text-center">
1066
+ Don't have an account? <a href="/register" className="text-blue-600">Register</a>
1067
+ </p>
1068
+ </div>
1069
+ );
1070
+ }
1071
+ ```
1072
+
1073
+ ### Register Page
1074
+
1075
+ ```typescript
1076
+ 'use client';
1077
+ import { useState } from 'react';
1078
+ import { omni, setCustomerToken } from '@/lib/omni-sync';
1079
+
1080
+ export default function RegisterPage() {
1081
+ const [email, setEmail] = useState('');
1082
+ const [password, setPassword] = useState('');
1083
+ const [firstName, setFirstName] = useState('');
1084
+ const [lastName, setLastName] = useState('');
1085
+ const [error, setError] = useState('');
1086
+ const [loading, setLoading] = useState(false);
1087
+
1088
+ const handleSubmit = async (e: React.FormEvent) => {
1089
+ e.preventDefault();
1090
+ setLoading(true);
1091
+ setError('');
1092
+ try {
1093
+ const auth = await omni.registerCustomer({ email, password, firstName, lastName });
1094
+ setCustomerToken(auth.token);
1095
+ window.location.href = '/account';
1096
+ } catch (err) {
1097
+ setError('Registration failed. Email may already be in use.');
1098
+ } finally {
1099
+ setLoading(false);
1100
+ }
1101
+ };
1102
+
1103
+ return (
1104
+ <div className="max-w-md mx-auto mt-12">
1105
+ <h1 className="text-2xl font-bold mb-6">Create Account</h1>
1106
+ {error && <div className="bg-red-100 text-red-600 p-3 rounded mb-4">{error}</div>}
1107
+ <form onSubmit={handleSubmit} className="space-y-4">
1108
+ <div className="grid grid-cols-2 gap-4">
1109
+ <input placeholder="First Name" value={firstName} onChange={e => setFirstName(e.target.value)} required className="border p-2 rounded" />
1110
+ <input placeholder="Last Name" value={lastName} onChange={e => setLastName(e.target.value)} required className="border p-2 rounded" />
1111
+ </div>
1112
+ <input type="email" placeholder="Email" value={email} onChange={e => setEmail(e.target.value)} required className="w-full border p-2 rounded" />
1113
+ <input type="password" placeholder="Password (min 8 characters)" value={password} onChange={e => setPassword(e.target.value)} required minLength={8} className="w-full border p-2 rounded" />
1114
+ <button type="submit" disabled={loading} className="w-full bg-black text-white py-3 rounded">
1115
+ {loading ? 'Creating Account...' : 'Create Account'}
1116
+ </button>
1117
+ </form>
1118
+ <p className="mt-4 text-center">
1119
+ Already have an account? <a href="/login" className="text-blue-600">Login</a>
1120
+ </p>
1121
+ </div>
1122
+ );
1123
+ }
1124
+ ```
1125
+
1126
+ ### Account Page
1127
+
1128
+ ```typescript
1129
+ 'use client';
1130
+ import { useEffect, useState } from 'react';
1131
+ import { omni, restoreCustomerToken, setCustomerToken, isLoggedIn } from '@/lib/omni-sync';
1132
+ import type { CustomerProfile, Order } from 'omni-sync-sdk';
1133
+
1134
+ export default function AccountPage() {
1135
+ const [profile, setProfile] = useState<CustomerProfile | null>(null);
1136
+ const [orders, setOrders] = useState<Order[]>([]);
1137
+ const [loading, setLoading] = useState(true);
1138
+
1139
+ useEffect(() => {
1140
+ restoreCustomerToken();
1141
+ if (!isLoggedIn()) {
1142
+ window.location.href = '/login';
1143
+ return;
1144
+ }
1145
+
1146
+ async function load() {
1147
+ try {
1148
+ const [p, o] = await Promise.all([
1149
+ omni.getMyProfile(),
1150
+ omni.getMyOrders({ limit: 10 }),
1151
+ ]);
1152
+ setProfile(p);
1153
+ setOrders(o.data);
1154
+ } finally {
1155
+ setLoading(false);
1156
+ }
1157
+ }
1158
+ load();
1159
+ }, []);
1160
+
1161
+ const handleLogout = () => {
1162
+ setCustomerToken(null);
1163
+ window.location.href = '/';
1164
+ };
1165
+
1166
+ if (loading) return <div>Loading...</div>;
1167
+ if (!profile) return <div>Please log in</div>;
1168
+
1169
+ return (
1170
+ <div>
1171
+ <div className="flex justify-between items-center mb-8">
1172
+ <h1 className="text-2xl font-bold">My Account</h1>
1173
+ <button onClick={handleLogout} className="text-red-600">Logout</button>
1174
+ </div>
1175
+
1176
+ <div className="grid md:grid-cols-2 gap-8">
1177
+ <div className="border rounded p-6">
1178
+ <h2 className="text-xl font-bold mb-4">Profile</h2>
1179
+ <p><strong>Name:</strong> {profile.firstName} {profile.lastName}</p>
1180
+ <p><strong>Email:</strong> {profile.email}</p>
1181
+ </div>
1182
+
1183
+ <div className="border rounded p-6">
1184
+ <h2 className="text-xl font-bold mb-4">Recent Orders</h2>
1185
+ {orders.length === 0 ? (
1186
+ <p className="text-gray-500">No orders yet</p>
1187
+ ) : (
1188
+ <div className="space-y-4">
1189
+ {orders.map((order) => (
1190
+ <div key={order.id} className="border-b pb-4">
1191
+ <span className="font-medium">#{order.id.slice(-8)}</span>
1192
+ <span className="ml-2 text-sm">{order.status}</span>
1193
+ <p className="font-bold">${order.totalAmount}</p>
1194
+ </div>
1195
+ ))}
1196
+ </div>
1197
+ )}
1198
+ </div>
1199
+ </div>
1200
+ </div>
1201
+ );
1202
+ }
1203
+ ```
1204
+
1205
+ ### Header Component with Cart Count
1206
+
1207
+ ```typescript
1208
+ 'use client';
1209
+ import { useEffect, useState } from 'react';
1210
+ import { omni, getCartId, isLoggedIn } from '@/lib/omni-sync';
1211
+
1212
+ export function Header() {
1213
+ const [cartCount, setCartCount] = useState(0);
1214
+ const [loggedIn, setLoggedIn] = useState(false);
1215
+
1216
+ useEffect(() => {
1217
+ setLoggedIn(isLoggedIn());
1218
+
1219
+ async function loadCart() {
1220
+ const cartId = getCartId();
1221
+ if (cartId) {
1222
+ try {
1223
+ const cart = await omni.getCart(cartId);
1224
+ setCartCount(cart.itemCount);
1225
+ } catch {}
1226
+ }
1227
+ }
1228
+ loadCart();
1229
+ }, []);
1230
+
1231
+ return (
1232
+ <header className="flex justify-between items-center p-4 border-b">
1233
+ <a href="/" className="text-xl font-bold">Store Name</a>
1234
+ <nav className="flex gap-6 items-center">
1235
+ <a href="/products">Shop</a>
1236
+ <a href="/cart" className="relative">
1237
+ Cart
1238
+ {cartCount > 0 && (
1239
+ <span className="absolute -top-2 -right-2 bg-red-600 text-white text-xs w-5 h-5 rounded-full flex items-center justify-center">
1240
+ {cartCount}
1241
+ </span>
1242
+ )}
1243
+ </a>
1244
+ {loggedIn ? (
1245
+ <a href="/account">Account</a>
1246
+ ) : (
1247
+ <a href="/login">Login</a>
1248
+ )}
1249
+ </nav>
1250
+ </header>
1251
+ );
1252
+ }
1253
+ ```
1254
+
1255
+ ---
1256
+
1257
+ ## Error Handling
1258
+
1259
+ ```typescript
1260
+ import { OmniSyncClient, OmniSyncError } from 'omni-sync-sdk';
1261
+
1262
+ try {
1263
+ const product = await omni.getProduct('invalid_id');
1264
+ } catch (error) {
1265
+ if (error instanceof OmniSyncError) {
1266
+ console.error(`API Error: ${error.message}`);
1267
+ console.error(`Status Code: ${error.statusCode}`);
1268
+ console.error(`Details:`, error.details);
1269
+ }
1270
+ }
1271
+ ```
1272
+
1273
+ ---
1274
+
103
1275
  ## Webhooks
104
1276
 
105
- Receive real-time updates when products, orders, or inventory change on any connected platform.
1277
+ Receive real-time updates when products, orders, or inventory change.
106
1278
 
107
- ### Setup webhook endpoint
1279
+ ### Setup Webhook Endpoint
108
1280
 
109
1281
  ```typescript
110
1282
  // api/webhooks/omni-sync/route.ts (Next.js App Router)
@@ -141,44 +1313,116 @@ export async function POST(req: Request) {
141
1313
 
142
1314
  ### Webhook Events
143
1315
 
144
- | Event | Description |
145
- | ------------------- | ----------------------- |
146
- | `product.created` | New product created |
147
- | `product.updated` | Product details changed |
148
- | `product.deleted` | Product removed |
149
- | `inventory.updated` | Stock levels changed |
150
- | `order.created` | New order received |
151
- | `order.updated` | Order status changed |
1316
+ | Event | Description |
1317
+ | -------------------- | ------------------------------- |
1318
+ | `product.created` | New product created |
1319
+ | `product.updated` | Product details changed |
1320
+ | `product.deleted` | Product removed |
1321
+ | `inventory.updated` | Stock levels changed |
1322
+ | `order.created` | New order received |
1323
+ | `order.updated` | Order status changed |
1324
+ | `cart.abandoned` | Cart abandoned (no activity) |
1325
+ | `checkout.completed` | Checkout completed successfully |
152
1326
 
153
- ## Environment Variables
1327
+ ---
154
1328
 
155
- ```env
156
- OMNI_SYNC_API_KEY=omni_your_api_key_here
157
- OMNI_SYNC_WEBHOOK_SECRET=your_webhook_secret_here
158
- ```
1329
+ ## TypeScript Support
159
1330
 
160
- ## Error Handling
1331
+ All types are exported for full TypeScript support:
161
1332
 
162
1333
  ```typescript
163
- import { OmniSyncClient, OmniSyncError } from 'omni-sync-sdk';
1334
+ import type {
1335
+ // Products
1336
+ Product,
1337
+ ProductImage,
1338
+ ProductVariant,
1339
+ InventoryInfo,
1340
+ ProductQueryParams,
1341
+ PaginatedResponse,
164
1342
 
165
- try {
166
- const product = await omni.getProduct('invalid_id');
167
- } catch (error) {
168
- if (error instanceof OmniSyncError) {
169
- console.error(`API Error: ${error.message} (${error.statusCode})`);
170
- }
171
- }
1343
+ // Cart
1344
+ Cart,
1345
+ CartItem,
1346
+ AddToCartDto,
1347
+
1348
+ // Checkout
1349
+ Checkout,
1350
+ CheckoutStatus,
1351
+ ShippingRate,
1352
+ SetShippingAddressDto,
1353
+
1354
+ // Customer
1355
+ Customer,
1356
+ CustomerProfile,
1357
+ CustomerAddress,
1358
+ CustomerAuthResponse,
1359
+
1360
+ // Orders
1361
+ Order,
1362
+ OrderStatus,
1363
+ OrderItem,
1364
+
1365
+ // Webhooks
1366
+ WebhookEvent,
1367
+ WebhookEventType,
1368
+
1369
+ // Errors
1370
+ OmniSyncError,
1371
+ } from 'omni-sync-sdk';
172
1372
  ```
173
1373
 
174
- ## TypeScript Support
1374
+ ---
1375
+
1376
+ ## Environment Variables
175
1377
 
176
- Full TypeScript support with exported types:
1378
+ ```env
1379
+ # Required for vibe-coded sites
1380
+ NEXT_PUBLIC_OMNI_CONNECTION_ID=vc_your_connection_id
177
1381
 
178
- ```typescript
179
- import type { Product, Order, CreateProductDto, WebhookEvent } from 'omni-sync-sdk';
1382
+ # Optional: Override API URL (default: https://api.omni-sync.com)
1383
+ NEXT_PUBLIC_OMNI_API_URL=https://api.omni-sync.com
1384
+
1385
+ # For webhooks (server-side only)
1386
+ OMNI_SYNC_WEBHOOK_SECRET=your_webhook_secret
180
1387
  ```
181
1388
 
1389
+ ---
1390
+
1391
+ ## Required Pages Checklist
1392
+
1393
+ When building a store, implement these pages:
1394
+
1395
+ - [ ] **Home** (`/`) - Product grid
1396
+ - [ ] **Products** (`/products`) - Product list with pagination
1397
+ - [ ] **Product Detail** (`/products/[id]`) - Single product with Add to Cart
1398
+ - [ ] **Cart** (`/cart`) - Cart items, update quantity, remove
1399
+ - [ ] **Checkout** (`/checkout`) - Multi-step checkout flow
1400
+ - [ ] **Login** (`/login`) - Customer login
1401
+ - [ ] **Register** (`/register`) - Customer registration
1402
+ - [ ] **Account** (`/account`) - Profile and order history
1403
+
1404
+ ---
1405
+
1406
+ ## Important Rules
1407
+
1408
+ ### DO:
1409
+
1410
+ - Install `omni-sync-sdk` and use it for ALL data
1411
+ - Import types from the SDK
1412
+ - Handle loading states and errors
1413
+ - Persist cart ID in localStorage
1414
+ - Persist customer token after login
1415
+
1416
+ ### DON'T:
1417
+
1418
+ - Create mock/hardcoded product data
1419
+ - Use localStorage for products
1420
+ - Skip implementing required pages
1421
+ - Write `const products = [...]` - use the API!
1422
+ - Use `@apply group` in CSS - Tailwind doesn't allow 'group' in @apply. Use `className="group"` on the element instead
1423
+
1424
+ ---
1425
+
182
1426
  ## License
183
1427
 
184
1428
  MIT