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