omni-sync-sdk 0.4.1 → 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 +531 -115
- 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
|
|
|
@@ -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
|
|
@@ -615,12 +905,12 @@ export default function ProductsPage() {
|
|
|
615
905
|
}
|
|
616
906
|
```
|
|
617
907
|
|
|
618
|
-
### Product Detail with Add to Cart
|
|
908
|
+
### Product Detail with Add to Cart (Local Cart)
|
|
619
909
|
|
|
620
910
|
```typescript
|
|
621
911
|
'use client';
|
|
622
912
|
import { useEffect, useState } from 'react';
|
|
623
|
-
import { omni
|
|
913
|
+
import { omni } from '@/lib/omni-sync';
|
|
624
914
|
import type { Product } from 'omni-sync-sdk';
|
|
625
915
|
|
|
626
916
|
export default function ProductPage({ params }: { params: { id: string } }) {
|
|
@@ -628,7 +918,6 @@ export default function ProductPage({ params }: { params: { id: string } }) {
|
|
|
628
918
|
const [selectedVariant, setSelectedVariant] = useState<string | null>(null);
|
|
629
919
|
const [quantity, setQuantity] = useState(1);
|
|
630
920
|
const [loading, setLoading] = useState(true);
|
|
631
|
-
const [adding, setAdding] = useState(false);
|
|
632
921
|
|
|
633
922
|
useEffect(() => {
|
|
634
923
|
async function load() {
|
|
@@ -645,30 +934,25 @@ export default function ProductPage({ params }: { params: { id: string } }) {
|
|
|
645
934
|
load();
|
|
646
935
|
}, [params.id]);
|
|
647
936
|
|
|
648
|
-
const handleAddToCart =
|
|
937
|
+
const handleAddToCart = () => {
|
|
649
938
|
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
939
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
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!');
|
|
672
956
|
};
|
|
673
957
|
|
|
674
958
|
if (loading) return <div>Loading...</div>;
|
|
@@ -749,63 +1033,33 @@ export default function ProductPage({ params }: { params: { id: string } }) {
|
|
|
749
1033
|
}
|
|
750
1034
|
```
|
|
751
1035
|
|
|
752
|
-
### Cart Page
|
|
1036
|
+
### Cart Page (Local Cart)
|
|
753
1037
|
|
|
754
1038
|
```typescript
|
|
755
1039
|
'use client';
|
|
756
|
-
import {
|
|
757
|
-
import { omni
|
|
758
|
-
import type {
|
|
1040
|
+
import { useState } from 'react';
|
|
1041
|
+
import { omni } from '@/lib/omni-sync';
|
|
1042
|
+
import type { LocalCart } from 'omni-sync-sdk';
|
|
759
1043
|
|
|
760
1044
|
export default function CartPage() {
|
|
761
|
-
const [cart, setCart] = useState<
|
|
762
|
-
const [loading, setLoading] = useState(true);
|
|
763
|
-
const [updating, setUpdating] = useState<string | null>(null);
|
|
1045
|
+
const [cart, setCart] = useState<LocalCart>(omni.getLocalCart());
|
|
764
1046
|
|
|
765
|
-
const
|
|
766
|
-
const
|
|
767
|
-
|
|
768
|
-
setLoading(false);
|
|
769
|
-
return;
|
|
770
|
-
}
|
|
771
|
-
try {
|
|
772
|
-
const c = await omni.getCart(cartId);
|
|
773
|
-
setCart(c);
|
|
774
|
-
} finally {
|
|
775
|
-
setLoading(false);
|
|
776
|
-
}
|
|
1047
|
+
const updateQuantity = (productId: string, quantity: number, variantId?: string) => {
|
|
1048
|
+
const updated = omni.updateLocalCartItem(productId, quantity, variantId);
|
|
1049
|
+
setCart(updated);
|
|
777
1050
|
};
|
|
778
1051
|
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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
|
-
}
|
|
1052
|
+
const removeItem = (productId: string, variantId?: string) => {
|
|
1053
|
+
const updated = omni.removeFromLocalCart(productId, variantId);
|
|
1054
|
+
setCart(updated);
|
|
794
1055
|
};
|
|
795
1056
|
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
await omni.removeCartItem(cart.id, itemId);
|
|
801
|
-
await loadCart();
|
|
802
|
-
} finally {
|
|
803
|
-
setUpdating(null);
|
|
804
|
-
}
|
|
805
|
-
};
|
|
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);
|
|
806
1061
|
|
|
807
|
-
if (
|
|
808
|
-
if (!cart || cart.items.length === 0) {
|
|
1062
|
+
if (cart.items.length === 0) {
|
|
809
1063
|
return (
|
|
810
1064
|
<div className="text-center py-12">
|
|
811
1065
|
<h1 className="text-2xl font-bold">Your cart is empty</h1>
|
|
@@ -819,42 +1073,38 @@ export default function CartPage() {
|
|
|
819
1073
|
<h1 className="text-2xl font-bold mb-6">Shopping Cart</h1>
|
|
820
1074
|
|
|
821
1075
|
{cart.items.map((item) => (
|
|
822
|
-
<div key={item.
|
|
1076
|
+
<div key={`${item.productId}-${item.variantId || ''}`} className="flex items-center gap-4 py-4 border-b">
|
|
823
1077
|
<img
|
|
824
|
-
src={item.
|
|
825
|
-
alt={item.
|
|
1078
|
+
src={item.image || '/placeholder.jpg'}
|
|
1079
|
+
alt={item.name || 'Product'}
|
|
826
1080
|
className="w-20 h-20 object-cover"
|
|
827
1081
|
/>
|
|
828
1082
|
<div className="flex-1">
|
|
829
|
-
<h3 className="font-medium">{item.
|
|
830
|
-
|
|
831
|
-
<p className="font-bold">${item.unitPrice}</p>
|
|
1083
|
+
<h3 className="font-medium">{item.name || 'Product'}</h3>
|
|
1084
|
+
<p className="font-bold">${item.price}</p>
|
|
832
1085
|
</div>
|
|
833
1086
|
<div className="flex items-center gap-2">
|
|
834
1087
|
<button
|
|
835
|
-
onClick={() => updateQuantity(item.
|
|
836
|
-
disabled={updating === item.id}
|
|
1088
|
+
onClick={() => updateQuantity(item.productId, item.quantity - 1, item.variantId)}
|
|
837
1089
|
className="w-8 h-8 border rounded"
|
|
838
1090
|
>-</button>
|
|
839
1091
|
<span className="w-8 text-center">{item.quantity}</span>
|
|
840
1092
|
<button
|
|
841
|
-
onClick={() => updateQuantity(item.
|
|
842
|
-
disabled={updating === item.id}
|
|
1093
|
+
onClick={() => updateQuantity(item.productId, item.quantity + 1, item.variantId)}
|
|
843
1094
|
className="w-8 h-8 border rounded"
|
|
844
1095
|
>+</button>
|
|
845
1096
|
</div>
|
|
846
1097
|
<button
|
|
847
|
-
onClick={() => removeItem(item.
|
|
848
|
-
disabled={updating === item.id}
|
|
1098
|
+
onClick={() => removeItem(item.productId, item.variantId)}
|
|
849
1099
|
className="text-red-600"
|
|
850
1100
|
>Remove</button>
|
|
851
1101
|
</div>
|
|
852
1102
|
))}
|
|
853
1103
|
|
|
854
1104
|
<div className="mt-6 text-right">
|
|
855
|
-
<p className="text-xl">Subtotal: <strong>${
|
|
856
|
-
{cart.
|
|
857
|
-
<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>
|
|
858
1108
|
)}
|
|
859
1109
|
<a
|
|
860
1110
|
href="/checkout"
|
|
@@ -868,12 +1118,182 @@ export default function CartPage() {
|
|
|
868
1118
|
}
|
|
869
1119
|
```
|
|
870
1120
|
|
|
871
|
-
###
|
|
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:
|
|
872
1292
|
|
|
873
1293
|
```typescript
|
|
874
1294
|
'use client';
|
|
875
1295
|
import { useEffect, useState } from 'react';
|
|
876
|
-
import { omni,
|
|
1296
|
+
import { omni, getServerCartId } from '@/lib/omni-sync';
|
|
877
1297
|
import type { Checkout, ShippingRate } from 'omni-sync-sdk';
|
|
878
1298
|
|
|
879
1299
|
type Step = 'customer' | 'shipping' | 'payment' | 'complete';
|
|
@@ -896,7 +1316,7 @@ export default function CheckoutPage() {
|
|
|
896
1316
|
|
|
897
1317
|
useEffect(() => {
|
|
898
1318
|
async function initCheckout() {
|
|
899
|
-
const cartId =
|
|
1319
|
+
const cartId = getServerCartId();
|
|
900
1320
|
if (!cartId) {
|
|
901
1321
|
window.location.href = '/cart';
|
|
902
1322
|
return;
|
|
@@ -948,7 +1368,6 @@ export default function CheckoutPage() {
|
|
|
948
1368
|
setSubmitting(true);
|
|
949
1369
|
try {
|
|
950
1370
|
const { orderId } = await omni.completeCheckout(checkout.id);
|
|
951
|
-
clearCartId();
|
|
952
1371
|
setStep('complete');
|
|
953
1372
|
} catch (err) {
|
|
954
1373
|
alert('Failed to complete order');
|
|
@@ -1202,12 +1621,12 @@ export default function AccountPage() {
|
|
|
1202
1621
|
}
|
|
1203
1622
|
```
|
|
1204
1623
|
|
|
1205
|
-
### Header Component with Cart Count
|
|
1624
|
+
### Header Component with Cart Count (Local Cart)
|
|
1206
1625
|
|
|
1207
1626
|
```typescript
|
|
1208
1627
|
'use client';
|
|
1209
|
-
import {
|
|
1210
|
-
import { omni,
|
|
1628
|
+
import { useState, useEffect } from 'react';
|
|
1629
|
+
import { omni, isLoggedIn } from '@/lib/omni-sync';
|
|
1211
1630
|
|
|
1212
1631
|
export function Header() {
|
|
1213
1632
|
const [cartCount, setCartCount] = useState(0);
|
|
@@ -1215,17 +1634,8 @@ export function Header() {
|
|
|
1215
1634
|
|
|
1216
1635
|
useEffect(() => {
|
|
1217
1636
|
setLoggedIn(isLoggedIn());
|
|
1218
|
-
|
|
1219
|
-
|
|
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();
|
|
1637
|
+
// Get cart count from local storage (NO API call!)
|
|
1638
|
+
setCartCount(omni.getLocalCartItemCount());
|
|
1229
1639
|
}, []);
|
|
1230
1640
|
|
|
1231
1641
|
return (
|
|
@@ -1340,7 +1750,13 @@ import type {
|
|
|
1340
1750
|
ProductQueryParams,
|
|
1341
1751
|
PaginatedResponse,
|
|
1342
1752
|
|
|
1343
|
-
// Cart
|
|
1753
|
+
// Local Cart (Guest Users)
|
|
1754
|
+
LocalCart,
|
|
1755
|
+
LocalCartItem,
|
|
1756
|
+
CreateGuestOrderDto,
|
|
1757
|
+
GuestOrderResponse,
|
|
1758
|
+
|
|
1759
|
+
// Server Cart (Registered Users)
|
|
1344
1760
|
Cart,
|
|
1345
1761
|
CartItem,
|
|
1346
1762
|
AddToCartDto,
|