omni-sync-sdk 0.4.1 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +535 -115
- package/dist/index.d.mts +391 -2
- package/dist/index.d.ts +391 -2
- package/dist/index.js +440 -1
- package/dist/index.mjs +440 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,14 +31,75 @@ 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
|
+
|
|
69
|
+
- ✅ No API calls when browsing/adding to cart
|
|
70
|
+
- ✅ Cart persists across page refreshes
|
|
71
|
+
- ✅ Single API call at checkout
|
|
72
|
+
- ✅ No server load for window shoppers
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
// Add product to local cart
|
|
76
|
+
omni.addToLocalCart({ productId: 'prod_123', quantity: 2 });
|
|
77
|
+
|
|
78
|
+
// View cart
|
|
79
|
+
const cart = omni.getLocalCart();
|
|
80
|
+
console.log('Items:', cart.items.length);
|
|
81
|
+
|
|
82
|
+
// Update quantity
|
|
83
|
+
omni.updateLocalCartItem('prod_123', 5);
|
|
84
|
+
|
|
85
|
+
// Remove item
|
|
86
|
+
omni.removeFromLocalCart('prod_123');
|
|
87
|
+
|
|
88
|
+
// At checkout - submit everything in ONE API call
|
|
89
|
+
const order = await omni.submitGuestOrder();
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Option 2: Server Cart (Registered Users)
|
|
93
|
+
|
|
94
|
+
For logged-in customers, use server-side cart:
|
|
95
|
+
|
|
96
|
+
- ✅ Cart syncs across devices
|
|
97
|
+
- ✅ Abandoned cart recovery
|
|
98
|
+
- ✅ Customer history tracking
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
const cart = await omni.createCart();
|
|
102
|
+
await omni.addToCart(cart.id, { productId: 'prod_123', quantity: 2 });
|
|
42
103
|
```
|
|
43
104
|
|
|
44
105
|
---
|
|
@@ -56,18 +117,28 @@ export const omni = new OmniSyncClient({
|
|
|
56
117
|
connectionId: 'vc_YOUR_CONNECTION_ID', // Your Connection ID from OmniSync
|
|
57
118
|
});
|
|
58
119
|
|
|
59
|
-
// ----- Cart Helpers -----
|
|
120
|
+
// ----- Guest Cart Helpers (localStorage) -----
|
|
121
|
+
|
|
122
|
+
export function getCartItemCount(): number {
|
|
123
|
+
return omni.getLocalCartItemCount();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function getCart() {
|
|
127
|
+
return omni.getLocalCart();
|
|
128
|
+
}
|
|
60
129
|
|
|
61
|
-
|
|
130
|
+
// ----- For Registered Users (server cart) -----
|
|
131
|
+
|
|
132
|
+
export function getServerCartId(): string | null {
|
|
62
133
|
if (typeof window === 'undefined') return null;
|
|
63
134
|
return localStorage.getItem('cartId');
|
|
64
135
|
}
|
|
65
136
|
|
|
66
|
-
export function
|
|
137
|
+
export function setServerCartId(id: string): void {
|
|
67
138
|
localStorage.setItem('cartId', id);
|
|
68
139
|
}
|
|
69
140
|
|
|
70
|
-
export function
|
|
141
|
+
export function clearServerCartId(): void {
|
|
71
142
|
localStorage.removeItem('cartId');
|
|
72
143
|
}
|
|
73
144
|
|
|
@@ -180,13 +251,236 @@ interface InventoryInfo {
|
|
|
180
251
|
|
|
181
252
|
---
|
|
182
253
|
|
|
183
|
-
### Cart
|
|
254
|
+
### Local Cart (Guest Users) - RECOMMENDED
|
|
255
|
+
|
|
256
|
+
The local cart stores everything in **localStorage** until checkout. This is the recommended approach for most storefronts.
|
|
257
|
+
|
|
258
|
+
#### Add to Local Cart
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
// Add item with product info (for display)
|
|
262
|
+
omni.addToLocalCart({
|
|
263
|
+
productId: 'prod_123',
|
|
264
|
+
variantId: 'var_456', // Optional: for products with variants
|
|
265
|
+
quantity: 2,
|
|
266
|
+
name: 'Cool T-Shirt', // Optional: for cart display
|
|
267
|
+
price: '29.99', // Optional: for cart display
|
|
268
|
+
image: 'https://...', // Optional: for cart display
|
|
269
|
+
});
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
#### Get Local Cart
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
const cart = omni.getLocalCart();
|
|
276
|
+
|
|
277
|
+
console.log(cart.items); // Array of cart items
|
|
278
|
+
console.log(cart.customer); // Customer info (if set)
|
|
279
|
+
console.log(cart.shippingAddress); // Shipping address (if set)
|
|
280
|
+
console.log(cart.couponCode); // Applied coupon (if any)
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
#### Update Item Quantity
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
// Set quantity to 5
|
|
287
|
+
omni.updateLocalCartItem('prod_123', 5);
|
|
288
|
+
|
|
289
|
+
// For variant products
|
|
290
|
+
omni.updateLocalCartItem('prod_123', 3, 'var_456');
|
|
291
|
+
|
|
292
|
+
// Set to 0 to remove
|
|
293
|
+
omni.updateLocalCartItem('prod_123', 0);
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
#### Remove Item
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
omni.removeFromLocalCart('prod_123');
|
|
300
|
+
omni.removeFromLocalCart('prod_123', 'var_456'); // With variant
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
#### Clear Cart
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
omni.clearLocalCart();
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
#### Set Customer Info
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
omni.setLocalCartCustomer({
|
|
313
|
+
email: 'customer@example.com', // Required
|
|
314
|
+
firstName: 'John', // Optional
|
|
315
|
+
lastName: 'Doe', // Optional
|
|
316
|
+
phone: '+1234567890', // Optional
|
|
317
|
+
});
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
#### Set Shipping Address
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
omni.setLocalCartShippingAddress({
|
|
324
|
+
firstName: 'John',
|
|
325
|
+
lastName: 'Doe',
|
|
326
|
+
line1: '123 Main St',
|
|
327
|
+
line2: 'Apt 4B', // Optional
|
|
328
|
+
city: 'New York',
|
|
329
|
+
region: 'NY', // Optional: State/Province
|
|
330
|
+
postalCode: '10001',
|
|
331
|
+
country: 'US',
|
|
332
|
+
phone: '+1234567890', // Optional
|
|
333
|
+
});
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
#### Set Billing Address (Optional)
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
omni.setLocalCartBillingAddress({
|
|
340
|
+
firstName: 'John',
|
|
341
|
+
lastName: 'Doe',
|
|
342
|
+
line1: '456 Business Ave',
|
|
343
|
+
city: 'New York',
|
|
344
|
+
postalCode: '10002',
|
|
345
|
+
country: 'US',
|
|
346
|
+
});
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
#### Apply Coupon
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
omni.setLocalCartCoupon('SAVE20');
|
|
353
|
+
|
|
354
|
+
// Remove coupon
|
|
355
|
+
omni.setLocalCartCoupon(undefined);
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
#### Get Cart Item Count
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
const count = omni.getLocalCartItemCount();
|
|
362
|
+
console.log(`${count} items in cart`);
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
#### Local Cart Type Definition
|
|
366
|
+
|
|
367
|
+
```typescript
|
|
368
|
+
interface LocalCart {
|
|
369
|
+
items: LocalCartItem[];
|
|
370
|
+
couponCode?: string;
|
|
371
|
+
customer?: {
|
|
372
|
+
email: string;
|
|
373
|
+
firstName?: string;
|
|
374
|
+
lastName?: string;
|
|
375
|
+
phone?: string;
|
|
376
|
+
};
|
|
377
|
+
shippingAddress?: {
|
|
378
|
+
firstName: string;
|
|
379
|
+
lastName: string;
|
|
380
|
+
line1: string;
|
|
381
|
+
line2?: string;
|
|
382
|
+
city: string;
|
|
383
|
+
region?: string;
|
|
384
|
+
postalCode: string;
|
|
385
|
+
country: string;
|
|
386
|
+
phone?: string;
|
|
387
|
+
};
|
|
388
|
+
billingAddress?: {
|
|
389
|
+
/* same as shipping */
|
|
390
|
+
};
|
|
391
|
+
notes?: string;
|
|
392
|
+
updatedAt: string;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
interface LocalCartItem {
|
|
396
|
+
productId: string;
|
|
397
|
+
variantId?: string;
|
|
398
|
+
quantity: number;
|
|
399
|
+
name?: string;
|
|
400
|
+
sku?: string;
|
|
401
|
+
price?: string;
|
|
402
|
+
image?: string;
|
|
403
|
+
addedAt: string;
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
|
|
409
|
+
### Guest Checkout (Submit Order)
|
|
410
|
+
|
|
411
|
+
Submit the local cart as an order with a **single API call**:
|
|
412
|
+
|
|
413
|
+
```typescript
|
|
414
|
+
// Make sure cart has items, customer email, and shipping address
|
|
415
|
+
const order = await omni.submitGuestOrder();
|
|
416
|
+
|
|
417
|
+
console.log(order.orderId); // 'order_abc123...'
|
|
418
|
+
console.log(order.orderNumber); // 'ORD-12345'
|
|
419
|
+
console.log(order.status); // 'pending'
|
|
420
|
+
console.log(order.total); // 59.98
|
|
421
|
+
console.log(order.message); // 'Order created successfully'
|
|
422
|
+
|
|
423
|
+
// Cart is automatically cleared after successful order
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
#### Keep Cart After Order
|
|
427
|
+
|
|
428
|
+
```typescript
|
|
429
|
+
// If you want to keep the cart data (e.g., for order review page)
|
|
430
|
+
const order = await omni.submitGuestOrder({ clearCartOnSuccess: false });
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
#### Create Order with Custom Data
|
|
434
|
+
|
|
435
|
+
If you manage cart state yourself instead of using local cart:
|
|
436
|
+
|
|
437
|
+
```typescript
|
|
438
|
+
const order = await omni.createGuestOrder({
|
|
439
|
+
items: [
|
|
440
|
+
{ productId: 'prod_123', quantity: 2 },
|
|
441
|
+
{ productId: 'prod_456', variantId: 'var_789', quantity: 1 },
|
|
442
|
+
],
|
|
443
|
+
customer: {
|
|
444
|
+
email: 'customer@example.com',
|
|
445
|
+
firstName: 'John',
|
|
446
|
+
lastName: 'Doe',
|
|
447
|
+
},
|
|
448
|
+
shippingAddress: {
|
|
449
|
+
firstName: 'John',
|
|
450
|
+
lastName: 'Doe',
|
|
451
|
+
line1: '123 Main St',
|
|
452
|
+
city: 'New York',
|
|
453
|
+
postalCode: '10001',
|
|
454
|
+
country: 'US',
|
|
455
|
+
},
|
|
456
|
+
couponCode: 'SAVE20', // Optional
|
|
457
|
+
notes: 'Please gift wrap', // Optional
|
|
458
|
+
});
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
#### Guest Order Response Type
|
|
462
|
+
|
|
463
|
+
```typescript
|
|
464
|
+
interface GuestOrderResponse {
|
|
465
|
+
orderId: string;
|
|
466
|
+
orderNumber: string;
|
|
467
|
+
status: string;
|
|
468
|
+
total: number;
|
|
469
|
+
message: string;
|
|
470
|
+
}
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
---
|
|
474
|
+
|
|
475
|
+
### Server Cart (Registered Users)
|
|
476
|
+
|
|
477
|
+
For logged-in customers who want cart sync across devices.
|
|
184
478
|
|
|
185
479
|
#### Create Cart
|
|
186
480
|
|
|
187
481
|
```typescript
|
|
188
482
|
const cart = await omni.createCart();
|
|
189
|
-
|
|
483
|
+
setServerCartId(cart.id); // Save to localStorage
|
|
190
484
|
```
|
|
191
485
|
|
|
192
486
|
#### Get Cart
|
|
@@ -615,12 +909,12 @@ export default function ProductsPage() {
|
|
|
615
909
|
}
|
|
616
910
|
```
|
|
617
911
|
|
|
618
|
-
### Product Detail with Add to Cart
|
|
912
|
+
### Product Detail with Add to Cart (Local Cart)
|
|
619
913
|
|
|
620
914
|
```typescript
|
|
621
915
|
'use client';
|
|
622
916
|
import { useEffect, useState } from 'react';
|
|
623
|
-
import { omni
|
|
917
|
+
import { omni } from '@/lib/omni-sync';
|
|
624
918
|
import type { Product } from 'omni-sync-sdk';
|
|
625
919
|
|
|
626
920
|
export default function ProductPage({ params }: { params: { id: string } }) {
|
|
@@ -628,7 +922,6 @@ export default function ProductPage({ params }: { params: { id: string } }) {
|
|
|
628
922
|
const [selectedVariant, setSelectedVariant] = useState<string | null>(null);
|
|
629
923
|
const [quantity, setQuantity] = useState(1);
|
|
630
924
|
const [loading, setLoading] = useState(true);
|
|
631
|
-
const [adding, setAdding] = useState(false);
|
|
632
925
|
|
|
633
926
|
useEffect(() => {
|
|
634
927
|
async function load() {
|
|
@@ -645,30 +938,25 @@ export default function ProductPage({ params }: { params: { id: string } }) {
|
|
|
645
938
|
load();
|
|
646
939
|
}, [params.id]);
|
|
647
940
|
|
|
648
|
-
const handleAddToCart =
|
|
941
|
+
const handleAddToCart = () => {
|
|
649
942
|
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
943
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
944
|
+
// Get variant if selected
|
|
945
|
+
const variant = selectedVariant
|
|
946
|
+
? product.variants?.find(v => v.id === selectedVariant)
|
|
947
|
+
: null;
|
|
948
|
+
|
|
949
|
+
// Add to local cart (NO API call!)
|
|
950
|
+
omni.addToLocalCart({
|
|
951
|
+
productId: product.id,
|
|
952
|
+
variantId: selectedVariant || undefined,
|
|
953
|
+
quantity,
|
|
954
|
+
name: variant?.name || product.name,
|
|
955
|
+
price: String(variant?.price || product.salePrice || product.basePrice),
|
|
956
|
+
image: product.images?.[0]?.url,
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
alert('Added to cart!');
|
|
672
960
|
};
|
|
673
961
|
|
|
674
962
|
if (loading) return <div>Loading...</div>;
|
|
@@ -749,63 +1037,33 @@ export default function ProductPage({ params }: { params: { id: string } }) {
|
|
|
749
1037
|
}
|
|
750
1038
|
```
|
|
751
1039
|
|
|
752
|
-
### Cart Page
|
|
1040
|
+
### Cart Page (Local Cart)
|
|
753
1041
|
|
|
754
1042
|
```typescript
|
|
755
1043
|
'use client';
|
|
756
|
-
import {
|
|
757
|
-
import { omni
|
|
758
|
-
import type {
|
|
1044
|
+
import { useState } from 'react';
|
|
1045
|
+
import { omni } from '@/lib/omni-sync';
|
|
1046
|
+
import type { LocalCart } from 'omni-sync-sdk';
|
|
759
1047
|
|
|
760
1048
|
export default function CartPage() {
|
|
761
|
-
const [cart, setCart] = useState<
|
|
762
|
-
const [loading, setLoading] = useState(true);
|
|
763
|
-
const [updating, setUpdating] = useState<string | null>(null);
|
|
1049
|
+
const [cart, setCart] = useState<LocalCart>(omni.getLocalCart());
|
|
764
1050
|
|
|
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
|
-
}
|
|
1051
|
+
const updateQuantity = (productId: string, quantity: number, variantId?: string) => {
|
|
1052
|
+
const updated = omni.updateLocalCartItem(productId, quantity, variantId);
|
|
1053
|
+
setCart(updated);
|
|
777
1054
|
};
|
|
778
1055
|
|
|
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
|
-
}
|
|
1056
|
+
const removeItem = (productId: string, variantId?: string) => {
|
|
1057
|
+
const updated = omni.removeFromLocalCart(productId, variantId);
|
|
1058
|
+
setCart(updated);
|
|
794
1059
|
};
|
|
795
1060
|
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
await omni.removeCartItem(cart.id, itemId);
|
|
801
|
-
await loadCart();
|
|
802
|
-
} finally {
|
|
803
|
-
setUpdating(null);
|
|
804
|
-
}
|
|
805
|
-
};
|
|
1061
|
+
// Calculate subtotal from local cart
|
|
1062
|
+
const subtotal = cart.items.reduce((sum, item) => {
|
|
1063
|
+
return sum + (parseFloat(item.price || '0') * item.quantity);
|
|
1064
|
+
}, 0);
|
|
806
1065
|
|
|
807
|
-
if (
|
|
808
|
-
if (!cart || cart.items.length === 0) {
|
|
1066
|
+
if (cart.items.length === 0) {
|
|
809
1067
|
return (
|
|
810
1068
|
<div className="text-center py-12">
|
|
811
1069
|
<h1 className="text-2xl font-bold">Your cart is empty</h1>
|
|
@@ -819,42 +1077,38 @@ export default function CartPage() {
|
|
|
819
1077
|
<h1 className="text-2xl font-bold mb-6">Shopping Cart</h1>
|
|
820
1078
|
|
|
821
1079
|
{cart.items.map((item) => (
|
|
822
|
-
<div key={item.
|
|
1080
|
+
<div key={`${item.productId}-${item.variantId || ''}`} className="flex items-center gap-4 py-4 border-b">
|
|
823
1081
|
<img
|
|
824
|
-
src={item.
|
|
825
|
-
alt={item.
|
|
1082
|
+
src={item.image || '/placeholder.jpg'}
|
|
1083
|
+
alt={item.name || 'Product'}
|
|
826
1084
|
className="w-20 h-20 object-cover"
|
|
827
1085
|
/>
|
|
828
1086
|
<div className="flex-1">
|
|
829
|
-
<h3 className="font-medium">{item.
|
|
830
|
-
|
|
831
|
-
<p className="font-bold">${item.unitPrice}</p>
|
|
1087
|
+
<h3 className="font-medium">{item.name || 'Product'}</h3>
|
|
1088
|
+
<p className="font-bold">${item.price}</p>
|
|
832
1089
|
</div>
|
|
833
1090
|
<div className="flex items-center gap-2">
|
|
834
1091
|
<button
|
|
835
|
-
onClick={() => updateQuantity(item.
|
|
836
|
-
disabled={updating === item.id}
|
|
1092
|
+
onClick={() => updateQuantity(item.productId, item.quantity - 1, item.variantId)}
|
|
837
1093
|
className="w-8 h-8 border rounded"
|
|
838
1094
|
>-</button>
|
|
839
1095
|
<span className="w-8 text-center">{item.quantity}</span>
|
|
840
1096
|
<button
|
|
841
|
-
onClick={() => updateQuantity(item.
|
|
842
|
-
disabled={updating === item.id}
|
|
1097
|
+
onClick={() => updateQuantity(item.productId, item.quantity + 1, item.variantId)}
|
|
843
1098
|
className="w-8 h-8 border rounded"
|
|
844
1099
|
>+</button>
|
|
845
1100
|
</div>
|
|
846
1101
|
<button
|
|
847
|
-
onClick={() => removeItem(item.
|
|
848
|
-
disabled={updating === item.id}
|
|
1102
|
+
onClick={() => removeItem(item.productId, item.variantId)}
|
|
849
1103
|
className="text-red-600"
|
|
850
1104
|
>Remove</button>
|
|
851
1105
|
</div>
|
|
852
1106
|
))}
|
|
853
1107
|
|
|
854
1108
|
<div className="mt-6 text-right">
|
|
855
|
-
<p className="text-xl">Subtotal: <strong>${
|
|
856
|
-
{cart.
|
|
857
|
-
<p className="text-green-600">
|
|
1109
|
+
<p className="text-xl">Subtotal: <strong>${subtotal.toFixed(2)}</strong></p>
|
|
1110
|
+
{cart.couponCode && (
|
|
1111
|
+
<p className="text-green-600">Coupon applied: {cart.couponCode}</p>
|
|
858
1112
|
)}
|
|
859
1113
|
<a
|
|
860
1114
|
href="/checkout"
|
|
@@ -868,12 +1122,182 @@ export default function CartPage() {
|
|
|
868
1122
|
}
|
|
869
1123
|
```
|
|
870
1124
|
|
|
871
|
-
###
|
|
1125
|
+
### Guest Checkout (Single API Call)
|
|
1126
|
+
|
|
1127
|
+
This is the recommended checkout for guest users. All cart data is in localStorage, and we submit it in one API call.
|
|
1128
|
+
|
|
1129
|
+
```typescript
|
|
1130
|
+
'use client';
|
|
1131
|
+
import { useState, useEffect } from 'react';
|
|
1132
|
+
import { omni } from '@/lib/omni-sync';
|
|
1133
|
+
import type { LocalCart, GuestOrderResponse } from 'omni-sync-sdk';
|
|
1134
|
+
|
|
1135
|
+
type Step = 'info' | 'review' | 'complete';
|
|
1136
|
+
|
|
1137
|
+
export default function CheckoutPage() {
|
|
1138
|
+
const [cart, setCart] = useState<LocalCart>(omni.getLocalCart());
|
|
1139
|
+
const [step, setStep] = useState<Step>('info');
|
|
1140
|
+
const [order, setOrder] = useState<GuestOrderResponse | null>(null);
|
|
1141
|
+
const [submitting, setSubmitting] = useState(false);
|
|
1142
|
+
const [error, setError] = useState('');
|
|
1143
|
+
|
|
1144
|
+
// Form state
|
|
1145
|
+
const [email, setEmail] = useState(cart.customer?.email || '');
|
|
1146
|
+
const [firstName, setFirstName] = useState(cart.customer?.firstName || '');
|
|
1147
|
+
const [lastName, setLastName] = useState(cart.customer?.lastName || '');
|
|
1148
|
+
const [address, setAddress] = useState(cart.shippingAddress?.line1 || '');
|
|
1149
|
+
const [city, setCity] = useState(cart.shippingAddress?.city || '');
|
|
1150
|
+
const [postalCode, setPostalCode] = useState(cart.shippingAddress?.postalCode || '');
|
|
1151
|
+
const [country, setCountry] = useState(cart.shippingAddress?.country || 'US');
|
|
1152
|
+
|
|
1153
|
+
// Calculate subtotal
|
|
1154
|
+
const subtotal = cart.items.reduce((sum, item) => {
|
|
1155
|
+
return sum + (parseFloat(item.price || '0') * item.quantity);
|
|
1156
|
+
}, 0);
|
|
1157
|
+
|
|
1158
|
+
// Redirect if cart is empty
|
|
1159
|
+
useEffect(() => {
|
|
1160
|
+
if (cart.items.length === 0 && step !== 'complete') {
|
|
1161
|
+
window.location.href = '/cart';
|
|
1162
|
+
}
|
|
1163
|
+
}, [cart.items.length, step]);
|
|
1164
|
+
|
|
1165
|
+
const handleInfoSubmit = (e: React.FormEvent) => {
|
|
1166
|
+
e.preventDefault();
|
|
1167
|
+
|
|
1168
|
+
// Save to local cart
|
|
1169
|
+
omni.setLocalCartCustomer({ email, firstName, lastName });
|
|
1170
|
+
omni.setLocalCartShippingAddress({
|
|
1171
|
+
firstName,
|
|
1172
|
+
lastName,
|
|
1173
|
+
line1: address,
|
|
1174
|
+
city,
|
|
1175
|
+
postalCode,
|
|
1176
|
+
country,
|
|
1177
|
+
});
|
|
1178
|
+
|
|
1179
|
+
setStep('review');
|
|
1180
|
+
};
|
|
1181
|
+
|
|
1182
|
+
const handlePlaceOrder = async () => {
|
|
1183
|
+
setSubmitting(true);
|
|
1184
|
+
setError('');
|
|
1185
|
+
|
|
1186
|
+
try {
|
|
1187
|
+
// Single API call to create order!
|
|
1188
|
+
const result = await omni.submitGuestOrder();
|
|
1189
|
+
setOrder(result);
|
|
1190
|
+
setStep('complete');
|
|
1191
|
+
} catch (err) {
|
|
1192
|
+
setError(err instanceof Error ? err.message : 'Failed to place order');
|
|
1193
|
+
} finally {
|
|
1194
|
+
setSubmitting(false);
|
|
1195
|
+
}
|
|
1196
|
+
};
|
|
1197
|
+
|
|
1198
|
+
if (step === 'complete' && order) {
|
|
1199
|
+
return (
|
|
1200
|
+
<div className="text-center py-12">
|
|
1201
|
+
<h1 className="text-3xl font-bold text-green-600">Order Complete!</h1>
|
|
1202
|
+
<p className="mt-4">Order Number: <strong>{order.orderNumber}</strong></p>
|
|
1203
|
+
<p className="mt-2">Total: <strong>${order.total.toFixed(2)}</strong></p>
|
|
1204
|
+
<p className="mt-4 text-gray-600">A confirmation email will be sent to {email}</p>
|
|
1205
|
+
<a href="/" className="mt-6 inline-block text-blue-600">Continue Shopping</a>
|
|
1206
|
+
</div>
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
return (
|
|
1211
|
+
<div className="max-w-2xl mx-auto">
|
|
1212
|
+
<h1 className="text-2xl font-bold mb-6">Checkout</h1>
|
|
1213
|
+
|
|
1214
|
+
{error && (
|
|
1215
|
+
<div className="bg-red-100 text-red-600 p-3 rounded mb-4">{error}</div>
|
|
1216
|
+
)}
|
|
1217
|
+
|
|
1218
|
+
{step === 'info' && (
|
|
1219
|
+
<form onSubmit={handleInfoSubmit} className="space-y-4">
|
|
1220
|
+
<h2 className="text-lg font-bold">Contact Information</h2>
|
|
1221
|
+
<input
|
|
1222
|
+
type="email"
|
|
1223
|
+
placeholder="Email"
|
|
1224
|
+
value={email}
|
|
1225
|
+
onChange={e => setEmail(e.target.value)}
|
|
1226
|
+
required
|
|
1227
|
+
className="w-full border p-2 rounded"
|
|
1228
|
+
/>
|
|
1229
|
+
|
|
1230
|
+
<h2 className="text-lg font-bold mt-6">Shipping Address</h2>
|
|
1231
|
+
<div className="grid grid-cols-2 gap-4">
|
|
1232
|
+
<input placeholder="First Name" value={firstName} onChange={e => setFirstName(e.target.value)} required className="border p-2 rounded" />
|
|
1233
|
+
<input placeholder="Last Name" value={lastName} onChange={e => setLastName(e.target.value)} required className="border p-2 rounded" />
|
|
1234
|
+
</div>
|
|
1235
|
+
<input placeholder="Address" value={address} onChange={e => setAddress(e.target.value)} required className="w-full border p-2 rounded" />
|
|
1236
|
+
<div className="grid grid-cols-2 gap-4">
|
|
1237
|
+
<input placeholder="City" value={city} onChange={e => setCity(e.target.value)} required className="border p-2 rounded" />
|
|
1238
|
+
<input placeholder="Postal Code" value={postalCode} onChange={e => setPostalCode(e.target.value)} required className="border p-2 rounded" />
|
|
1239
|
+
</div>
|
|
1240
|
+
<select value={country} onChange={e => setCountry(e.target.value)} className="w-full border p-2 rounded">
|
|
1241
|
+
<option value="US">United States</option>
|
|
1242
|
+
<option value="IL">Israel</option>
|
|
1243
|
+
<option value="GB">United Kingdom</option>
|
|
1244
|
+
</select>
|
|
1245
|
+
|
|
1246
|
+
<button type="submit" className="w-full bg-black text-white py-3 rounded">
|
|
1247
|
+
Review Order
|
|
1248
|
+
</button>
|
|
1249
|
+
</form>
|
|
1250
|
+
)}
|
|
1251
|
+
|
|
1252
|
+
{step === 'review' && (
|
|
1253
|
+
<div className="space-y-6">
|
|
1254
|
+
{/* Order Summary */}
|
|
1255
|
+
<div className="border p-4 rounded">
|
|
1256
|
+
<h3 className="font-bold mb-4">Order Summary</h3>
|
|
1257
|
+
{cart.items.map((item) => (
|
|
1258
|
+
<div key={`${item.productId}-${item.variantId || ''}`} className="flex justify-between py-2">
|
|
1259
|
+
<span>{item.name} x {item.quantity}</span>
|
|
1260
|
+
<span>${(parseFloat(item.price || '0') * item.quantity).toFixed(2)}</span>
|
|
1261
|
+
</div>
|
|
1262
|
+
))}
|
|
1263
|
+
<hr className="my-2" />
|
|
1264
|
+
<div className="flex justify-between font-bold">
|
|
1265
|
+
<span>Total</span>
|
|
1266
|
+
<span>${subtotal.toFixed(2)}</span>
|
|
1267
|
+
</div>
|
|
1268
|
+
</div>
|
|
1269
|
+
|
|
1270
|
+
{/* Shipping Info */}
|
|
1271
|
+
<div className="border p-4 rounded">
|
|
1272
|
+
<h3 className="font-bold mb-2">Shipping To</h3>
|
|
1273
|
+
<p>{firstName} {lastName}</p>
|
|
1274
|
+
<p>{address}</p>
|
|
1275
|
+
<p>{city}, {postalCode}, {country}</p>
|
|
1276
|
+
<button onClick={() => setStep('info')} className="text-blue-600 text-sm mt-2">Edit</button>
|
|
1277
|
+
</div>
|
|
1278
|
+
|
|
1279
|
+
<button
|
|
1280
|
+
onClick={handlePlaceOrder}
|
|
1281
|
+
disabled={submitting}
|
|
1282
|
+
className="w-full bg-green-600 text-white py-3 rounded text-lg"
|
|
1283
|
+
>
|
|
1284
|
+
{submitting ? 'Processing...' : 'Place Order'}
|
|
1285
|
+
</button>
|
|
1286
|
+
</div>
|
|
1287
|
+
)}
|
|
1288
|
+
</div>
|
|
1289
|
+
);
|
|
1290
|
+
}
|
|
1291
|
+
```
|
|
1292
|
+
|
|
1293
|
+
### Multi-Step Checkout (Server Cart - For Registered Users)
|
|
1294
|
+
|
|
1295
|
+
For logged-in users with server-side cart:
|
|
872
1296
|
|
|
873
1297
|
```typescript
|
|
874
1298
|
'use client';
|
|
875
1299
|
import { useEffect, useState } from 'react';
|
|
876
|
-
import { omni,
|
|
1300
|
+
import { omni, getServerCartId } from '@/lib/omni-sync';
|
|
877
1301
|
import type { Checkout, ShippingRate } from 'omni-sync-sdk';
|
|
878
1302
|
|
|
879
1303
|
type Step = 'customer' | 'shipping' | 'payment' | 'complete';
|
|
@@ -896,7 +1320,7 @@ export default function CheckoutPage() {
|
|
|
896
1320
|
|
|
897
1321
|
useEffect(() => {
|
|
898
1322
|
async function initCheckout() {
|
|
899
|
-
const cartId =
|
|
1323
|
+
const cartId = getServerCartId();
|
|
900
1324
|
if (!cartId) {
|
|
901
1325
|
window.location.href = '/cart';
|
|
902
1326
|
return;
|
|
@@ -948,7 +1372,6 @@ export default function CheckoutPage() {
|
|
|
948
1372
|
setSubmitting(true);
|
|
949
1373
|
try {
|
|
950
1374
|
const { orderId } = await omni.completeCheckout(checkout.id);
|
|
951
|
-
clearCartId();
|
|
952
1375
|
setStep('complete');
|
|
953
1376
|
} catch (err) {
|
|
954
1377
|
alert('Failed to complete order');
|
|
@@ -1202,12 +1625,12 @@ export default function AccountPage() {
|
|
|
1202
1625
|
}
|
|
1203
1626
|
```
|
|
1204
1627
|
|
|
1205
|
-
### Header Component with Cart Count
|
|
1628
|
+
### Header Component with Cart Count (Local Cart)
|
|
1206
1629
|
|
|
1207
1630
|
```typescript
|
|
1208
1631
|
'use client';
|
|
1209
|
-
import {
|
|
1210
|
-
import { omni,
|
|
1632
|
+
import { useState, useEffect } from 'react';
|
|
1633
|
+
import { omni, isLoggedIn } from '@/lib/omni-sync';
|
|
1211
1634
|
|
|
1212
1635
|
export function Header() {
|
|
1213
1636
|
const [cartCount, setCartCount] = useState(0);
|
|
@@ -1215,17 +1638,8 @@ export function Header() {
|
|
|
1215
1638
|
|
|
1216
1639
|
useEffect(() => {
|
|
1217
1640
|
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();
|
|
1641
|
+
// Get cart count from local storage (NO API call!)
|
|
1642
|
+
setCartCount(omni.getLocalCartItemCount());
|
|
1229
1643
|
}, []);
|
|
1230
1644
|
|
|
1231
1645
|
return (
|
|
@@ -1340,7 +1754,13 @@ import type {
|
|
|
1340
1754
|
ProductQueryParams,
|
|
1341
1755
|
PaginatedResponse,
|
|
1342
1756
|
|
|
1343
|
-
// Cart
|
|
1757
|
+
// Local Cart (Guest Users)
|
|
1758
|
+
LocalCart,
|
|
1759
|
+
LocalCartItem,
|
|
1760
|
+
CreateGuestOrderDto,
|
|
1761
|
+
GuestOrderResponse,
|
|
1762
|
+
|
|
1763
|
+
// Server Cart (Registered Users)
|
|
1344
1764
|
Cart,
|
|
1345
1765
|
CartItem,
|
|
1346
1766
|
AddToCartDto,
|