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 +564 -150
- package/dist/index.d.mts +247 -2
- package/dist/index.d.ts +247 -2
- package/dist/index.js +333 -1
- package/dist/index.mjs +333 -1
- package/package.json +1 -1
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
|
-
//
|
|
35
|
-
|
|
34
|
+
// ===== GUEST CHECKOUT (Recommended for most sites) =====
|
|
35
|
+
// Cart stored locally - NO API calls until checkout!
|
|
36
36
|
|
|
37
|
-
// Add
|
|
38
|
-
|
|
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
|
|
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
|
|
135
|
+
export function setServerCartId(id: string): void {
|
|
67
136
|
localStorage.setItem('cartId', id);
|
|
68
137
|
}
|
|
69
138
|
|
|
70
|
-
export function
|
|
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',
|
|
113
|
-
status: 'active',
|
|
114
|
-
type: 'SIMPLE',
|
|
115
|
-
sortBy: 'createdAt',
|
|
116
|
-
sortOrder: '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);
|
|
120
|
-
console.log(response.meta.total);
|
|
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);
|
|
132
|
-
console.log(product.images);
|
|
133
|
-
console.log(product.variants);
|
|
134
|
-
console.log(product.inventory);
|
|
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
|
-
|
|
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);
|
|
199
|
-
console.log(cart.itemCount);
|
|
200
|
-
console.log(cart.subtotal);
|
|
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',
|
|
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);
|
|
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',
|
|
603
|
+
line2: 'Apt 4B', // Optional
|
|
314
604
|
city: 'New York',
|
|
315
|
-
region: 'NY',
|
|
605
|
+
region: 'NY', // State/Province
|
|
316
606
|
postalCode: '10001',
|
|
317
607
|
country: 'US',
|
|
318
|
-
phone: '+1234567890',
|
|
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);
|
|
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
|
|
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 =
|
|
937
|
+
const handleAddToCart = () => {
|
|
654
938
|
if (!product) return;
|
|
655
|
-
setAdding(true);
|
|
656
|
-
try {
|
|
657
|
-
let cartId = getCartId();
|
|
658
939
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
|
|
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 {
|
|
762
|
-
import { omni
|
|
763
|
-
import type {
|
|
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<
|
|
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
|
|
771
|
-
const
|
|
772
|
-
|
|
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
|
-
|
|
785
|
-
|
|
786
|
-
|
|
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
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
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 (
|
|
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.
|
|
1076
|
+
<div key={`${item.productId}-${item.variantId || ''}`} className="flex items-center gap-4 py-4 border-b">
|
|
828
1077
|
<img
|
|
829
|
-
src={item.
|
|
830
|
-
alt={item.
|
|
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.
|
|
835
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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>${
|
|
861
|
-
{cart.
|
|
862
|
-
<p className="text-green-600">
|
|
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
|
-
###
|
|
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,
|
|
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 =
|
|
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 {
|
|
1215
|
-
import { omni,
|
|
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
|
-
|
|
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
|
|
1322
|
-
|
|
1323
|
-
| `product.created`
|
|
1324
|
-
| `product.updated`
|
|
1325
|
-
| `product.deleted`
|
|
1326
|
-
| `inventory.updated`
|
|
1327
|
-
| `order.created`
|
|
1328
|
-
| `order.updated`
|
|
1329
|
-
| `cart.abandoned`
|
|
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
|
|