omni-sync-sdk 0.8.0 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -30,8 +30,37 @@ const omni = new OmniSyncClient({
30
30
 
31
31
  // Fetch products
32
32
  const { data: products } = await omni.getProducts();
33
+ ```
34
+
35
+ ---
36
+
37
+ ## Checkout: Guest vs Logged-In Customer
38
+
39
+ > **IMPORTANT:** There are TWO different checkout flows. You MUST use the correct one based on whether the customer is logged in or not.
33
40
 
34
- // ===== GUEST CHECKOUT (Recommended for most sites) =====
41
+ | Customer Type | Cart Type | Checkout Method | Orders Linked to Account? |
42
+ |---------------|-----------|-----------------|---------------------------|
43
+ | **Guest** | Local Cart (localStorage) | `submitGuestOrder()` | No |
44
+ | **Logged In** | Server Cart | `completeCheckout()` | Yes |
45
+
46
+ ### Decision Flow
47
+
48
+ ```typescript
49
+ // ALWAYS check this at checkout!
50
+ if (isLoggedIn()) {
51
+ // ✅ Logged-in customer → Server Cart + Checkout flow
52
+ // Orders will be linked to their account
53
+ const order = await completeServerCheckout();
54
+ } else {
55
+ // ✅ Guest → Local Cart + submitGuestOrder
56
+ // Orders are standalone (not linked to any account)
57
+ const order = await omni.submitGuestOrder();
58
+ }
59
+ ```
60
+
61
+ ### Guest Checkout (for visitors without account)
62
+
63
+ ```typescript
35
64
  // Cart stored locally - NO API calls until checkout!
36
65
 
37
66
  // Add to local cart (stored in localStorage)
@@ -58,11 +87,60 @@ const order = await omni.submitGuestOrder();
58
87
  console.log('Order created:', order.orderId);
59
88
  ```
60
89
 
90
+ ### Logged-In Customer Checkout (orders linked to account)
91
+
92
+ ```typescript
93
+ // 1. Make sure customer token is set (after login)
94
+ omni.setCustomerToken(authResponse.token);
95
+
96
+ // 2. Create server cart (auto-linked to customer!)
97
+ const cart = await omni.createCart();
98
+ localStorage.setItem('cartId', cart.id);
99
+
100
+ // 3. Add items to server cart
101
+ await omni.addToCart(cart.id, {
102
+ productId: products[0].id,
103
+ quantity: 1,
104
+ });
105
+
106
+ // 4. Create checkout from cart
107
+ const checkout = await omni.createCheckout({ cartId: cart.id });
108
+
109
+ // 5. Set customer info (REQUIRED - email is needed for order!)
110
+ await omni.setCheckoutCustomer(checkout.id, {
111
+ email: 'customer@example.com',
112
+ firstName: 'John',
113
+ lastName: 'Doe',
114
+ });
115
+
116
+ // 6. Set shipping address
117
+ await omni.setShippingAddress(checkout.id, {
118
+ firstName: 'John',
119
+ lastName: 'Doe',
120
+ line1: '123 Main St',
121
+ city: 'New York',
122
+ postalCode: '10001',
123
+ country: 'US',
124
+ });
125
+
126
+ // 7. Get shipping rates and select one
127
+ const rates = await omni.getShippingRates(checkout.id);
128
+ await omni.selectShippingMethod(checkout.id, rates[0].id);
129
+
130
+ // 8. Complete checkout - order is linked to customer!
131
+ const { orderId } = await omni.completeCheckout(checkout.id);
132
+ console.log('Order created:', orderId);
133
+
134
+ // Customer can now see this order in omni.getMyOrders()
135
+ ```
136
+
137
+ > **WARNING:** Do NOT use `submitGuestOrder()` for logged-in customers! Their orders won't be linked to their account and won't appear in their order history.
138
+
61
139
  ---
62
140
 
63
141
  ## Two Ways to Handle Cart
64
142
 
65
- ### Option 1: Local Cart (Guest Users) - RECOMMENDED
143
+ ### Option 1: Local Cart (Guest Users)
66
144
 
67
145
  For guest users, the cart is stored in **localStorage** - exactly like Amazon, Shopify, and other major platforms do. This means:
68
146
 
@@ -89,19 +167,34 @@ omni.removeFromLocalCart('prod_123');
89
167
  const order = await omni.submitGuestOrder();
90
168
  ```
91
169
 
92
- ### Option 2: Server Cart (Registered Users)
170
+ ### Option 2: Server Cart (Logged-In Customers)
93
171
 
94
- For logged-in customers, use server-side cart:
172
+ For logged-in customers, **you MUST use server-side cart** to link orders to their account:
95
173
 
96
174
  - ✅ Cart syncs across devices
97
175
  - ✅ Abandoned cart recovery
98
- - ✅ Customer history tracking
176
+ - ✅ Orders linked to customer account
177
+ - ✅ Customer can see orders in "My Orders"
99
178
 
100
179
  ```typescript
180
+ // 1. Set customer token (after login)
181
+ omni.setCustomerToken(token);
182
+
183
+ // 2. Create cart (auto-linked to customer)
101
184
  const cart = await omni.createCart();
185
+ localStorage.setItem('cartId', cart.id);
186
+
187
+ // 3. Add items
102
188
  await omni.addToCart(cart.id, { productId: 'prod_123', quantity: 2 });
189
+
190
+ // 4. At checkout - create checkout and complete
191
+ const checkout = await omni.createCheckout({ cartId: cart.id });
192
+ // ... set shipping address, select shipping method ...
193
+ const { orderId } = await omni.completeCheckout(checkout.id);
103
194
  ```
104
195
 
196
+ > **⚠️ CRITICAL:** If you use `submitGuestOrder()` for a logged-in customer, their order will NOT be linked to their account!
197
+
105
198
  ---
106
199
 
107
200
  ## Complete Store Setup
@@ -322,6 +415,62 @@ interface InventoryInfo {
322
415
  }
323
416
  ```
324
417
 
418
+ #### Displaying Price Range for Variable Products
419
+
420
+ For products with `type: 'VARIABLE'` and multiple variants with different prices, display a **price range** instead of a single price:
421
+
422
+ ```typescript
423
+ // Helper function to get price range from variants
424
+ function getPriceRange(product: Product): { min: number; max: number } | null {
425
+ if (product.type !== 'VARIABLE' || !product.variants?.length) {
426
+ return null;
427
+ }
428
+
429
+ const prices = product.variants
430
+ .map(v => v.price ?? product.basePrice)
431
+ .filter((p): p is number => p !== null);
432
+
433
+ if (prices.length === 0) return null;
434
+
435
+ const min = Math.min(...prices);
436
+ const max = Math.max(...prices);
437
+
438
+ // Return null if all variants have the same price
439
+ return min !== max ? { min, max } : null;
440
+ }
441
+
442
+ // Usage in component
443
+ function ProductPrice({ product }: { product: Product }) {
444
+ const priceRange = getPriceRange(product);
445
+
446
+ if (priceRange) {
447
+ // Variable product with different variant prices - show range
448
+ return <span>${priceRange.min} - ${priceRange.max}</span>;
449
+ }
450
+
451
+ // Simple product or all variants same price - show single price
452
+ return product.salePrice ? (
453
+ <>
454
+ <span className="text-red-600">${product.salePrice}</span>
455
+ <span className="line-through text-gray-400 ml-2">${product.basePrice}</span>
456
+ </>
457
+ ) : (
458
+ <span>${product.basePrice}</span>
459
+ );
460
+ }
461
+ ```
462
+
463
+ **When to show price range:**
464
+
465
+ - Product `type` is `'VARIABLE'`
466
+ - Has 2+ variants with **different** prices
467
+ - Example: T-shirt sizes S/M/L at $29, XL/XXL at $34 → Display "$29 - $34"
468
+
469
+ **When to show single price:**
470
+
471
+ - Product `type` is `'SIMPLE'`
472
+ - Variable product where all variants have the same price
473
+
325
474
  #### Rendering Product Descriptions
326
475
 
327
476
  **IMPORTANT**: Product descriptions may contain HTML (from Shopify/WooCommerce) or plain text. Always check `descriptionFormat` before rendering:
@@ -1789,9 +1938,190 @@ export default function CartPage() {
1789
1938
  }
1790
1939
  ```
1791
1940
 
1941
+ ### Universal Checkout (Handles Both Guest & Logged-In)
1942
+
1943
+ > **RECOMMENDED:** Use this pattern to properly handle both guest and logged-in customers in a single checkout page.
1944
+
1945
+ ```typescript
1946
+ 'use client';
1947
+ import { useState, useEffect } from 'react';
1948
+ import { omni, isLoggedIn, getServerCartId, setServerCartId, restoreCustomerToken } from '@/lib/omni-sync';
1949
+
1950
+ export default function CheckoutPage() {
1951
+ const [loading, setLoading] = useState(true);
1952
+ const [submitting, setSubmitting] = useState(false);
1953
+ const [customerLoggedIn, setCustomerLoggedIn] = useState(false);
1954
+
1955
+ // Form state
1956
+ const [email, setEmail] = useState('');
1957
+ const [shippingAddress, setShippingAddress] = useState({
1958
+ firstName: '', lastName: '', line1: '', city: '', postalCode: '', country: 'US'
1959
+ });
1960
+
1961
+ // Server checkout state (for logged-in customers)
1962
+ const [checkoutId, setCheckoutId] = useState<string | null>(null);
1963
+ const [shippingRates, setShippingRates] = useState<any[]>([]);
1964
+ const [selectedRate, setSelectedRate] = useState<string | null>(null);
1965
+
1966
+ useEffect(() => {
1967
+ restoreCustomerToken();
1968
+ const loggedIn = isLoggedIn();
1969
+ setCustomerLoggedIn(loggedIn);
1970
+
1971
+ async function initCheckout() {
1972
+ if (loggedIn) {
1973
+ // Logged-in customer: Create server cart + checkout
1974
+ let cartId = getServerCartId();
1975
+
1976
+ if (!cartId) {
1977
+ // Create new cart (auto-linked to customer)
1978
+ const cart = await omni.createCart();
1979
+ cartId = cart.id;
1980
+ setServerCartId(cartId);
1981
+
1982
+ // Migrate local cart items to server cart
1983
+ const localCart = omni.getLocalCart();
1984
+ for (const item of localCart.items) {
1985
+ await omni.addToCart(cartId, {
1986
+ productId: item.productId,
1987
+ variantId: item.variantId,
1988
+ quantity: item.quantity,
1989
+ });
1990
+ }
1991
+ omni.clearLocalCart(); // Clear local cart after migration
1992
+ }
1993
+
1994
+ // Create checkout from server cart
1995
+ const checkout = await omni.createCheckout({ cartId });
1996
+ setCheckoutId(checkout.id);
1997
+
1998
+ // Pre-fill from customer profile if available
1999
+ try {
2000
+ const profile = await omni.getMyOrders({ limit: 1 }); // Just to check auth works
2001
+ } catch (e) {
2002
+ console.log('Could not fetch profile');
2003
+ }
2004
+ }
2005
+ setLoading(false);
2006
+ }
2007
+
2008
+ initCheckout();
2009
+ }, []);
2010
+
2011
+ const handleSubmit = async (e: React.FormEvent) => {
2012
+ e.preventDefault();
2013
+ setSubmitting(true);
2014
+
2015
+ try {
2016
+ if (customerLoggedIn && checkoutId) {
2017
+ // ===== LOGGED-IN CUSTOMER: Server Checkout =====
2018
+
2019
+ // 1. Set customer info (REQUIRED - even for logged-in customers!)
2020
+ await omni.setCheckoutCustomer(checkoutId, {
2021
+ email: email, // Get from form or customer profile
2022
+ firstName: shippingAddress.firstName,
2023
+ lastName: shippingAddress.lastName,
2024
+ });
2025
+
2026
+ // 2. Set shipping address
2027
+ await omni.setShippingAddress(checkoutId, shippingAddress);
2028
+
2029
+ // 3. Get and select shipping rate
2030
+ const rates = await omni.getShippingRates(checkoutId);
2031
+ if (rates.length > 0) {
2032
+ await omni.selectShippingMethod(checkoutId, selectedRate || rates[0].id);
2033
+ }
2034
+
2035
+ // 4. Complete checkout - ORDER IS LINKED TO CUSTOMER!
2036
+ const { orderId } = await omni.completeCheckout(checkoutId);
2037
+
2038
+ // Clear cart ID
2039
+ localStorage.removeItem('cartId');
2040
+
2041
+ // Redirect to success page
2042
+ window.location.href = `/order-success?orderId=${orderId}`;
2043
+
2044
+ } else {
2045
+ // ===== GUEST: Local Cart + submitGuestOrder =====
2046
+
2047
+ // Set customer and shipping info on local cart
2048
+ omni.setLocalCartCustomer({ email });
2049
+ omni.setLocalCartShippingAddress(shippingAddress);
2050
+
2051
+ // Submit guest order (single API call)
2052
+ const order = await omni.submitGuestOrder();
2053
+
2054
+ // Redirect to success page
2055
+ window.location.href = `/order-success?orderId=${order.orderId}`;
2056
+ }
2057
+ } catch (error) {
2058
+ console.error('Checkout failed:', error);
2059
+ alert('Checkout failed. Please try again.');
2060
+ } finally {
2061
+ setSubmitting(false);
2062
+ }
2063
+ };
2064
+
2065
+ if (loading) return <div>Loading checkout...</div>;
2066
+
2067
+ return (
2068
+ <form onSubmit={handleSubmit}>
2069
+ {/* Show email field only for guests */}
2070
+ {!customerLoggedIn && (
2071
+ <input
2072
+ type="email"
2073
+ value={email}
2074
+ onChange={(e) => setEmail(e.target.value)}
2075
+ placeholder="Email"
2076
+ required
2077
+ />
2078
+ )}
2079
+
2080
+ {/* Shipping address fields */}
2081
+ <input value={shippingAddress.firstName} onChange={(e) => setShippingAddress({...shippingAddress, firstName: e.target.value})} placeholder="First Name" required />
2082
+ <input value={shippingAddress.lastName} onChange={(e) => setShippingAddress({...shippingAddress, lastName: e.target.value})} placeholder="Last Name" required />
2083
+ <input value={shippingAddress.line1} onChange={(e) => setShippingAddress({...shippingAddress, line1: e.target.value})} placeholder="Address" required />
2084
+ <input value={shippingAddress.city} onChange={(e) => setShippingAddress({...shippingAddress, city: e.target.value})} placeholder="City" required />
2085
+ <input value={shippingAddress.postalCode} onChange={(e) => setShippingAddress({...shippingAddress, postalCode: e.target.value})} placeholder="Postal Code" required />
2086
+
2087
+ {/* Shipping rates (for logged-in customers) */}
2088
+ {customerLoggedIn && shippingRates.length > 0 && (
2089
+ <select value={selectedRate || ''} onChange={(e) => setSelectedRate(e.target.value)}>
2090
+ {shippingRates.map((rate) => (
2091
+ <option key={rate.id} value={rate.id}>
2092
+ {rate.name} - ${rate.price}
2093
+ </option>
2094
+ ))}
2095
+ </select>
2096
+ )}
2097
+
2098
+ <button type="submit" disabled={submitting}>
2099
+ {submitting ? 'Processing...' : 'Place Order'}
2100
+ </button>
2101
+
2102
+ {customerLoggedIn && (
2103
+ <p className="text-sm text-green-600">
2104
+ ✓ Logged in - Order will be saved to your account
2105
+ </p>
2106
+ )}
2107
+ </form>
2108
+ );
2109
+ }
2110
+ ```
2111
+
2112
+ > **Key Points:**
2113
+ > - `isLoggedIn()` determines which flow to use
2114
+ > - Logged-in customers use `createCart()` → `createCheckout()` → `completeCheckout()`
2115
+ > - Guests use local cart + `submitGuestOrder()`
2116
+ > - Local cart items are migrated to server cart when customer logs in
2117
+
2118
+ ---
2119
+
1792
2120
  ### Guest Checkout (Single API Call)
1793
2121
 
1794
- This is the recommended checkout for guest users. All cart data is in localStorage, and we submit it in one API call.
2122
+ This checkout is for **guest users only**. All cart data is in localStorage, and we submit it in one API call.
2123
+
2124
+ > **⚠️ WARNING:** Do NOT use this for logged-in customers! Use the Universal Checkout pattern above instead.
1795
2125
 
1796
2126
  ```typescript
1797
2127
  'use client';
@@ -1957,9 +2287,11 @@ export default function CheckoutPage() {
1957
2287
  }
1958
2288
  ```
1959
2289
 
1960
- ### Multi-Step Checkout (Server Cart - For Registered Users)
2290
+ ### Multi-Step Checkout (Server Cart - For Logged-In Customers Only)
2291
+
2292
+ > **IMPORTANT:** This checkout pattern is ONLY for logged-in customers. For a checkout page that handles both guests and logged-in customers, see the "Universal Checkout" example above.
1961
2293
 
1962
- For logged-in users with server-side cart:
2294
+ For logged-in users with server-side cart - orders will be linked to their account:
1963
2295
 
1964
2296
  ```typescript
1965
2297
  'use client';
package/dist/index.d.mts CHANGED
@@ -76,6 +76,7 @@ interface Product {
76
76
  costPrice?: number | null;
77
77
  status: 'active' | 'draft' | 'archived';
78
78
  type: 'SIMPLE' | 'VARIABLE';
79
+ isDownloadable?: boolean;
79
80
  images?: ProductImage[];
80
81
  inventory?: InventoryInfo | null;
81
82
  variants?: ProductVariant[];
@@ -110,6 +111,7 @@ interface ProductQueryParams {
110
111
  search?: string;
111
112
  status?: 'active' | 'draft' | 'archived';
112
113
  type?: 'SIMPLE' | 'VARIABLE';
114
+ isDownloadable?: boolean;
113
115
  sortBy?: 'name' | 'createdAt' | 'updatedAt' | 'basePrice';
114
116
  sortOrder?: 'asc' | 'desc';
115
117
  }
@@ -157,6 +159,7 @@ interface CreateProductDto {
157
159
  costPrice?: number;
158
160
  status?: 'active' | 'draft';
159
161
  type?: 'SIMPLE' | 'VARIABLE';
162
+ isDownloadable?: boolean;
160
163
  categories?: string[];
161
164
  tags?: string[];
162
165
  images?: ProductImage[];
@@ -169,6 +172,7 @@ interface UpdateProductDto {
169
172
  salePrice?: number | null;
170
173
  costPrice?: number | null;
171
174
  status?: 'active' | 'draft' | 'archived';
175
+ isDownloadable?: boolean;
172
176
  categories?: string[];
173
177
  tags?: string[];
174
178
  images?: ProductImage[];
package/dist/index.d.ts CHANGED
@@ -76,6 +76,7 @@ interface Product {
76
76
  costPrice?: number | null;
77
77
  status: 'active' | 'draft' | 'archived';
78
78
  type: 'SIMPLE' | 'VARIABLE';
79
+ isDownloadable?: boolean;
79
80
  images?: ProductImage[];
80
81
  inventory?: InventoryInfo | null;
81
82
  variants?: ProductVariant[];
@@ -110,6 +111,7 @@ interface ProductQueryParams {
110
111
  search?: string;
111
112
  status?: 'active' | 'draft' | 'archived';
112
113
  type?: 'SIMPLE' | 'VARIABLE';
114
+ isDownloadable?: boolean;
113
115
  sortBy?: 'name' | 'createdAt' | 'updatedAt' | 'basePrice';
114
116
  sortOrder?: 'asc' | 'desc';
115
117
  }
@@ -157,6 +159,7 @@ interface CreateProductDto {
157
159
  costPrice?: number;
158
160
  status?: 'active' | 'draft';
159
161
  type?: 'SIMPLE' | 'VARIABLE';
162
+ isDownloadable?: boolean;
160
163
  categories?: string[];
161
164
  tags?: string[];
162
165
  images?: ProductImage[];
@@ -169,6 +172,7 @@ interface UpdateProductDto {
169
172
  salePrice?: number | null;
170
173
  costPrice?: number | null;
171
174
  status?: 'active' | 'draft' | 'archived';
175
+ isDownloadable?: boolean;
172
176
  categories?: string[];
173
177
  tags?: string[];
174
178
  images?: ProductImage[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omni-sync-sdk",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "Official SDK for building e-commerce storefronts with OmniSync Platform. Perfect for vibe-coded sites, AI-built stores (Cursor, Lovable, v0), and custom storefronts.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",