omni-sync-sdk 0.7.11 → 0.8.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 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,53 @@ 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 shipping address
110
+ await omni.setShippingAddress(checkout.id, {
111
+ firstName: 'John',
112
+ lastName: 'Doe',
113
+ line1: '123 Main St',
114
+ city: 'New York',
115
+ postalCode: '10001',
116
+ country: 'US',
117
+ });
118
+
119
+ // 6. Get shipping rates and select one
120
+ const rates = await omni.getShippingRates(checkout.id);
121
+ await omni.selectShippingMethod(checkout.id, rates[0].id);
122
+
123
+ // 7. Complete checkout - order is linked to customer!
124
+ const { orderId } = await omni.completeCheckout(checkout.id);
125
+ console.log('Order created:', orderId);
126
+
127
+ // Customer can now see this order in omni.getMyOrders()
128
+ ```
129
+
130
+ > **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.
131
+
61
132
  ---
62
133
 
63
134
  ## Two Ways to Handle Cart
64
135
 
65
- ### Option 1: Local Cart (Guest Users) - RECOMMENDED
136
+ ### Option 1: Local Cart (Guest Users)
66
137
 
67
138
  For guest users, the cart is stored in **localStorage** - exactly like Amazon, Shopify, and other major platforms do. This means:
68
139
 
@@ -89,19 +160,34 @@ omni.removeFromLocalCart('prod_123');
89
160
  const order = await omni.submitGuestOrder();
90
161
  ```
91
162
 
92
- ### Option 2: Server Cart (Registered Users)
163
+ ### Option 2: Server Cart (Logged-In Customers)
93
164
 
94
- For logged-in customers, use server-side cart:
165
+ For logged-in customers, **you MUST use server-side cart** to link orders to their account:
95
166
 
96
167
  - ✅ Cart syncs across devices
97
168
  - ✅ Abandoned cart recovery
98
- - ✅ Customer history tracking
169
+ - ✅ Orders linked to customer account
170
+ - ✅ Customer can see orders in "My Orders"
99
171
 
100
172
  ```typescript
173
+ // 1. Set customer token (after login)
174
+ omni.setCustomerToken(token);
175
+
176
+ // 2. Create cart (auto-linked to customer)
101
177
  const cart = await omni.createCart();
178
+ localStorage.setItem('cartId', cart.id);
179
+
180
+ // 3. Add items
102
181
  await omni.addToCart(cart.id, { productId: 'prod_123', quantity: 2 });
182
+
183
+ // 4. At checkout - create checkout and complete
184
+ const checkout = await omni.createCheckout({ cartId: cart.id });
185
+ // ... set shipping address, select shipping method ...
186
+ const { orderId } = await omni.completeCheckout(checkout.id);
103
187
  ```
104
188
 
189
+ > **⚠️ CRITICAL:** If you use `submitGuestOrder()` for a logged-in customer, their order will NOT be linked to their account!
190
+
105
191
  ---
106
192
 
107
193
  ## Complete Store Setup
@@ -322,6 +408,62 @@ interface InventoryInfo {
322
408
  }
323
409
  ```
324
410
 
411
+ #### Displaying Price Range for Variable Products
412
+
413
+ For products with `type: 'VARIABLE'` and multiple variants with different prices, display a **price range** instead of a single price:
414
+
415
+ ```typescript
416
+ // Helper function to get price range from variants
417
+ function getPriceRange(product: Product): { min: number; max: number } | null {
418
+ if (product.type !== 'VARIABLE' || !product.variants?.length) {
419
+ return null;
420
+ }
421
+
422
+ const prices = product.variants
423
+ .map(v => v.price ?? product.basePrice)
424
+ .filter((p): p is number => p !== null);
425
+
426
+ if (prices.length === 0) return null;
427
+
428
+ const min = Math.min(...prices);
429
+ const max = Math.max(...prices);
430
+
431
+ // Return null if all variants have the same price
432
+ return min !== max ? { min, max } : null;
433
+ }
434
+
435
+ // Usage in component
436
+ function ProductPrice({ product }: { product: Product }) {
437
+ const priceRange = getPriceRange(product);
438
+
439
+ if (priceRange) {
440
+ // Variable product with different variant prices - show range
441
+ return <span>${priceRange.min} - ${priceRange.max}</span>;
442
+ }
443
+
444
+ // Simple product or all variants same price - show single price
445
+ return product.salePrice ? (
446
+ <>
447
+ <span className="text-red-600">${product.salePrice}</span>
448
+ <span className="line-through text-gray-400 ml-2">${product.basePrice}</span>
449
+ </>
450
+ ) : (
451
+ <span>${product.basePrice}</span>
452
+ );
453
+ }
454
+ ```
455
+
456
+ **When to show price range:**
457
+
458
+ - Product `type` is `'VARIABLE'`
459
+ - Has 2+ variants with **different** prices
460
+ - Example: T-shirt sizes S/M/L at $29, XL/XXL at $34 → Display "$29 - $34"
461
+
462
+ **When to show single price:**
463
+
464
+ - Product `type` is `'SIMPLE'`
465
+ - Variable product where all variants have the same price
466
+
325
467
  #### Rendering Product Descriptions
326
468
 
327
469
  **IMPORTANT**: Product descriptions may contain HTML (from Shopify/WooCommerce) or plain text. Always check `descriptionFormat` before rendering:
@@ -1076,6 +1218,335 @@ export default function VerifyEmailPage() {
1076
1218
 
1077
1219
  ---
1078
1220
 
1221
+ ### Social Login (OAuth)
1222
+
1223
+ Allow customers to sign in with Google, Facebook, or GitHub. The store owner configures which providers are available in their OmniSync admin panel.
1224
+
1225
+ #### Check Available Providers
1226
+
1227
+ ```typescript
1228
+ // Returns only the providers the store owner has enabled
1229
+ const { providers } = await omni.getAvailableOAuthProviders();
1230
+ // providers = ['GOOGLE', 'FACEBOOK'] - varies by store configuration
1231
+ ```
1232
+
1233
+ #### OAuth Login Flow
1234
+
1235
+ **Step 1: User clicks "Sign in with Google"**
1236
+
1237
+ ```typescript
1238
+ // Save cart ID before redirect (user will leave your site!)
1239
+ const cartId = localStorage.getItem('cartId');
1240
+ if (cartId) sessionStorage.setItem('pendingCartId', cartId);
1241
+
1242
+ // Get authorization URL
1243
+ const { authorizationUrl, state } = await omni.getOAuthAuthorizeUrl('GOOGLE', {
1244
+ redirectUrl: window.location.origin + '/auth/callback', // Where Google sends them back
1245
+ });
1246
+
1247
+ // Save state for verification (CSRF protection)
1248
+ sessionStorage.setItem('oauthState', state);
1249
+
1250
+ // Redirect to Google
1251
+ window.location.href = authorizationUrl;
1252
+ ```
1253
+
1254
+ **Step 2: Create callback page (`/auth/callback`)**
1255
+
1256
+ ```typescript
1257
+ // pages/auth/callback.tsx or app/auth/callback/page.tsx
1258
+ 'use client';
1259
+ import { useEffect, useState } from 'react';
1260
+ import { useSearchParams } from 'next/navigation';
1261
+ import { omni, setCustomerToken } from '@/lib/omni-sync';
1262
+
1263
+ export default function AuthCallback() {
1264
+ const searchParams = useSearchParams();
1265
+ const [error, setError] = useState<string | null>(null);
1266
+
1267
+ useEffect(() => {
1268
+ async function handleCallback() {
1269
+ const code = searchParams.get('code');
1270
+ const state = searchParams.get('state');
1271
+ const errorParam = searchParams.get('error');
1272
+
1273
+ // Check for OAuth errors (user cancelled, etc.)
1274
+ if (errorParam) {
1275
+ window.location.href = '/login?error=cancelled';
1276
+ return;
1277
+ }
1278
+
1279
+ if (!code || !state) {
1280
+ setError('Missing OAuth parameters');
1281
+ return;
1282
+ }
1283
+
1284
+ // Verify state matches (CSRF protection)
1285
+ const savedState = sessionStorage.getItem('oauthState');
1286
+ if (state !== savedState) {
1287
+ setError('Invalid state - please try again');
1288
+ return;
1289
+ }
1290
+
1291
+ try {
1292
+ // Exchange code for customer token
1293
+ const { customer, token, isNewCustomer } = await omni.handleOAuthCallback(
1294
+ 'GOOGLE',
1295
+ code,
1296
+ state
1297
+ );
1298
+
1299
+ // Save the customer token
1300
+ setCustomerToken(token);
1301
+ sessionStorage.removeItem('oauthState');
1302
+
1303
+ // Link any pending cart to the now-logged-in customer
1304
+ const pendingCartId = sessionStorage.getItem('pendingCartId');
1305
+ if (pendingCartId) {
1306
+ try {
1307
+ await omni.linkCart(pendingCartId);
1308
+ } catch {
1309
+ // Cart may have expired - that's ok
1310
+ }
1311
+ sessionStorage.removeItem('pendingCartId');
1312
+ }
1313
+
1314
+ // Redirect to return URL or home
1315
+ const returnUrl = localStorage.getItem('returnUrl') || '/';
1316
+ localStorage.removeItem('returnUrl');
1317
+ window.location.href = returnUrl;
1318
+ } catch (err) {
1319
+ setError(err instanceof Error ? err.message : 'Login failed');
1320
+ }
1321
+ }
1322
+
1323
+ handleCallback();
1324
+ }, [searchParams]);
1325
+
1326
+ if (error) {
1327
+ return (
1328
+ <div className="max-w-md mx-auto mt-12 text-center">
1329
+ <h1 className="text-xl font-bold text-red-600">Login Failed</h1>
1330
+ <p className="mt-2 text-gray-600">{error}</p>
1331
+ <a href="/login" className="mt-4 inline-block text-blue-600">
1332
+ Try again
1333
+ </a>
1334
+ </div>
1335
+ );
1336
+ }
1337
+
1338
+ return (
1339
+ <div className="max-w-md mx-auto mt-12 text-center">
1340
+ <div className="animate-spin h-8 w-8 border-4 border-blue-600 border-t-transparent rounded-full mx-auto"></div>
1341
+ <p className="mt-4 text-gray-600">Completing login...</p>
1342
+ </div>
1343
+ );
1344
+ }
1345
+ ```
1346
+
1347
+ #### Login Page with Social Buttons
1348
+
1349
+ ```typescript
1350
+ 'use client';
1351
+ import { useState, useEffect } from 'react';
1352
+ import { omni, setCustomerToken } from '@/lib/omni-sync';
1353
+
1354
+ export default function LoginPage() {
1355
+ const [providers, setProviders] = useState<string[]>([]);
1356
+ const [email, setEmail] = useState('');
1357
+ const [password, setPassword] = useState('');
1358
+ const [loading, setLoading] = useState(false);
1359
+ const [error, setError] = useState('');
1360
+
1361
+ // Load available OAuth providers
1362
+ useEffect(() => {
1363
+ omni.getAvailableOAuthProviders()
1364
+ .then(({ providers }) => setProviders(providers))
1365
+ .catch(() => {}); // OAuth not configured - that's ok
1366
+ }, []);
1367
+
1368
+ // Social login handler
1369
+ const handleSocialLogin = async (provider: string) => {
1370
+ try {
1371
+ // Save cart ID before redirect
1372
+ const cartId = localStorage.getItem('cartId');
1373
+ if (cartId) sessionStorage.setItem('pendingCartId', cartId);
1374
+
1375
+ const { authorizationUrl, state } = await omni.getOAuthAuthorizeUrl(
1376
+ provider as 'GOOGLE' | 'FACEBOOK' | 'GITHUB',
1377
+ { redirectUrl: window.location.origin + '/auth/callback' }
1378
+ );
1379
+
1380
+ sessionStorage.setItem('oauthState', state);
1381
+ window.location.href = authorizationUrl;
1382
+ } catch (err) {
1383
+ setError('Failed to start login');
1384
+ }
1385
+ };
1386
+
1387
+ // Regular email/password login
1388
+ const handleSubmit = async (e: React.FormEvent) => {
1389
+ e.preventDefault();
1390
+ setLoading(true);
1391
+ setError('');
1392
+ try {
1393
+ const auth = await omni.loginCustomer(email, password);
1394
+ setCustomerToken(auth.token);
1395
+ const returnUrl = localStorage.getItem('returnUrl') || '/';
1396
+ localStorage.removeItem('returnUrl');
1397
+ window.location.href = returnUrl;
1398
+ } catch (err) {
1399
+ setError('Invalid email or password');
1400
+ } finally {
1401
+ setLoading(false);
1402
+ }
1403
+ };
1404
+
1405
+ return (
1406
+ <div className="max-w-md mx-auto mt-12">
1407
+ <h1 className="text-2xl font-bold mb-6">Login</h1>
1408
+
1409
+ {/* Social Login Buttons */}
1410
+ {providers.length > 0 && (
1411
+ <div className="space-y-3 mb-6">
1412
+ {providers.includes('GOOGLE') && (
1413
+ <button
1414
+ onClick={() => handleSocialLogin('GOOGLE')}
1415
+ className="w-full flex items-center justify-center gap-2 border py-3 rounded hover:bg-gray-50"
1416
+ >
1417
+ <GoogleIcon />
1418
+ Continue with Google
1419
+ </button>
1420
+ )}
1421
+ {providers.includes('FACEBOOK') && (
1422
+ <button
1423
+ onClick={() => handleSocialLogin('FACEBOOK')}
1424
+ className="w-full flex items-center justify-center gap-2 bg-[#1877F2] text-white py-3 rounded"
1425
+ >
1426
+ <FacebookIcon />
1427
+ Continue with Facebook
1428
+ </button>
1429
+ )}
1430
+ {providers.includes('GITHUB') && (
1431
+ <button
1432
+ onClick={() => handleSocialLogin('GITHUB')}
1433
+ className="w-full flex items-center justify-center gap-2 bg-[#24292F] text-white py-3 rounded"
1434
+ >
1435
+ <GithubIcon />
1436
+ Continue with GitHub
1437
+ </button>
1438
+ )}
1439
+ <div className="relative my-4">
1440
+ <div className="absolute inset-0 flex items-center">
1441
+ <div className="w-full border-t" />
1442
+ </div>
1443
+ <div className="relative flex justify-center text-sm">
1444
+ <span className="px-2 bg-white text-gray-500">or</span>
1445
+ </div>
1446
+ </div>
1447
+ </div>
1448
+ )}
1449
+
1450
+ {/* Email/Password Form */}
1451
+ {error && <div className="bg-red-100 text-red-600 p-3 rounded mb-4">{error}</div>}
1452
+ <form onSubmit={handleSubmit} className="space-y-4">
1453
+ <input
1454
+ type="email"
1455
+ placeholder="Email"
1456
+ value={email}
1457
+ onChange={(e) => setEmail(e.target.value)}
1458
+ required
1459
+ className="w-full border p-2 rounded"
1460
+ />
1461
+ <input
1462
+ type="password"
1463
+ placeholder="Password"
1464
+ value={password}
1465
+ onChange={(e) => setPassword(e.target.value)}
1466
+ required
1467
+ className="w-full border p-2 rounded"
1468
+ />
1469
+ <button
1470
+ type="submit"
1471
+ disabled={loading}
1472
+ className="w-full bg-black text-white py-3 rounded"
1473
+ >
1474
+ {loading ? 'Logging in...' : 'Login'}
1475
+ </button>
1476
+ </form>
1477
+
1478
+ <p className="mt-4 text-center">
1479
+ Don't have an account? <a href="/register" className="text-blue-600">Register</a>
1480
+ </p>
1481
+ </div>
1482
+ );
1483
+ }
1484
+
1485
+ // Simple SVG icons (or use lucide-react)
1486
+ const GoogleIcon = () => (
1487
+ <svg className="w-5 h-5" viewBox="0 0 24 24">
1488
+ <path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
1489
+ <path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
1490
+ <path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
1491
+ <path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
1492
+ </svg>
1493
+ );
1494
+
1495
+ const FacebookIcon = () => (
1496
+ <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
1497
+ <path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/>
1498
+ </svg>
1499
+ );
1500
+
1501
+ const GithubIcon = () => (
1502
+ <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
1503
+ <path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
1504
+ </svg>
1505
+ );
1506
+ ```
1507
+
1508
+ #### Account Linking (Add Social Account to Existing User)
1509
+
1510
+ Logged-in customers can link additional social accounts to their profile:
1511
+
1512
+ ```typescript
1513
+ // Get currently linked accounts
1514
+ const connections = await omni.getOAuthConnections();
1515
+ // [{ provider: 'GOOGLE', email: 'user@gmail.com', linkedAt: '...' }]
1516
+
1517
+ // Link a new provider (redirects to OAuth flow)
1518
+ const { authorizationUrl } = await omni.linkOAuthProvider('GITHUB');
1519
+ window.location.href = authorizationUrl;
1520
+
1521
+ // Unlink a provider
1522
+ await omni.unlinkOAuthProvider('GOOGLE');
1523
+ ```
1524
+
1525
+ #### Cart Linking After OAuth Login
1526
+
1527
+ When a customer logs in via OAuth, their guest cart should be linked to their account:
1528
+
1529
+ ```typescript
1530
+ // omni.linkCart() associates a guest cart with the logged-in customer
1531
+ await omni.linkCart(cartId);
1532
+ ```
1533
+
1534
+ This is automatically handled in the callback example above.
1535
+
1536
+ #### OAuth Method Reference
1537
+
1538
+ | Method | Description |
1539
+ | -------------------------------------------- | -------------------------------------------- |
1540
+ | `getAvailableOAuthProviders()` | Get list of enabled providers for this store |
1541
+ | `getOAuthAuthorizeUrl(provider, options?)` | Get URL to redirect user to OAuth provider |
1542
+ | `handleOAuthCallback(provider, code, state)` | Exchange OAuth code for customer token |
1543
+ | `linkOAuthProvider(provider)` | Link social account to current customer |
1544
+ | `unlinkOAuthProvider(provider)` | Remove linked social account |
1545
+ | `getOAuthConnections()` | Get list of linked social accounts |
1546
+ | `linkCart(cartId)` | Link guest cart to logged-in customer |
1547
+
1548
+ ---
1549
+
1079
1550
  ### Customer Addresses
1080
1551
 
1081
1552
  #### Get Addresses
@@ -1460,9 +1931,183 @@ export default function CartPage() {
1460
1931
  }
1461
1932
  ```
1462
1933
 
1934
+ ### Universal Checkout (Handles Both Guest & Logged-In)
1935
+
1936
+ > **RECOMMENDED:** Use this pattern to properly handle both guest and logged-in customers in a single checkout page.
1937
+
1938
+ ```typescript
1939
+ 'use client';
1940
+ import { useState, useEffect } from 'react';
1941
+ import { omni, isLoggedIn, getServerCartId, setServerCartId, restoreCustomerToken } from '@/lib/omni-sync';
1942
+
1943
+ export default function CheckoutPage() {
1944
+ const [loading, setLoading] = useState(true);
1945
+ const [submitting, setSubmitting] = useState(false);
1946
+ const [customerLoggedIn, setCustomerLoggedIn] = useState(false);
1947
+
1948
+ // Form state
1949
+ const [email, setEmail] = useState('');
1950
+ const [shippingAddress, setShippingAddress] = useState({
1951
+ firstName: '', lastName: '', line1: '', city: '', postalCode: '', country: 'US'
1952
+ });
1953
+
1954
+ // Server checkout state (for logged-in customers)
1955
+ const [checkoutId, setCheckoutId] = useState<string | null>(null);
1956
+ const [shippingRates, setShippingRates] = useState<any[]>([]);
1957
+ const [selectedRate, setSelectedRate] = useState<string | null>(null);
1958
+
1959
+ useEffect(() => {
1960
+ restoreCustomerToken();
1961
+ const loggedIn = isLoggedIn();
1962
+ setCustomerLoggedIn(loggedIn);
1963
+
1964
+ async function initCheckout() {
1965
+ if (loggedIn) {
1966
+ // Logged-in customer: Create server cart + checkout
1967
+ let cartId = getServerCartId();
1968
+
1969
+ if (!cartId) {
1970
+ // Create new cart (auto-linked to customer)
1971
+ const cart = await omni.createCart();
1972
+ cartId = cart.id;
1973
+ setServerCartId(cartId);
1974
+
1975
+ // Migrate local cart items to server cart
1976
+ const localCart = omni.getLocalCart();
1977
+ for (const item of localCart.items) {
1978
+ await omni.addToCart(cartId, {
1979
+ productId: item.productId,
1980
+ variantId: item.variantId,
1981
+ quantity: item.quantity,
1982
+ });
1983
+ }
1984
+ omni.clearLocalCart(); // Clear local cart after migration
1985
+ }
1986
+
1987
+ // Create checkout from server cart
1988
+ const checkout = await omni.createCheckout({ cartId });
1989
+ setCheckoutId(checkout.id);
1990
+
1991
+ // Pre-fill from customer profile if available
1992
+ try {
1993
+ const profile = await omni.getMyOrders({ limit: 1 }); // Just to check auth works
1994
+ } catch (e) {
1995
+ console.log('Could not fetch profile');
1996
+ }
1997
+ }
1998
+ setLoading(false);
1999
+ }
2000
+
2001
+ initCheckout();
2002
+ }, []);
2003
+
2004
+ const handleSubmit = async (e: React.FormEvent) => {
2005
+ e.preventDefault();
2006
+ setSubmitting(true);
2007
+
2008
+ try {
2009
+ if (customerLoggedIn && checkoutId) {
2010
+ // ===== LOGGED-IN CUSTOMER: Server Checkout =====
2011
+
2012
+ // 1. Set shipping address
2013
+ await omni.setShippingAddress(checkoutId, shippingAddress);
2014
+
2015
+ // 2. Get and select shipping rate
2016
+ const rates = await omni.getShippingRates(checkoutId);
2017
+ if (rates.length > 0) {
2018
+ await omni.selectShippingMethod(checkoutId, selectedRate || rates[0].id);
2019
+ }
2020
+
2021
+ // 3. Complete checkout - ORDER IS LINKED TO CUSTOMER!
2022
+ const { orderId } = await omni.completeCheckout(checkoutId);
2023
+
2024
+ // Clear cart ID
2025
+ localStorage.removeItem('cartId');
2026
+
2027
+ // Redirect to success page
2028
+ window.location.href = `/order-success?orderId=${orderId}`;
2029
+
2030
+ } else {
2031
+ // ===== GUEST: Local Cart + submitGuestOrder =====
2032
+
2033
+ // Set customer and shipping info on local cart
2034
+ omni.setLocalCartCustomer({ email });
2035
+ omni.setLocalCartShippingAddress(shippingAddress);
2036
+
2037
+ // Submit guest order (single API call)
2038
+ const order = await omni.submitGuestOrder();
2039
+
2040
+ // Redirect to success page
2041
+ window.location.href = `/order-success?orderId=${order.orderId}`;
2042
+ }
2043
+ } catch (error) {
2044
+ console.error('Checkout failed:', error);
2045
+ alert('Checkout failed. Please try again.');
2046
+ } finally {
2047
+ setSubmitting(false);
2048
+ }
2049
+ };
2050
+
2051
+ if (loading) return <div>Loading checkout...</div>;
2052
+
2053
+ return (
2054
+ <form onSubmit={handleSubmit}>
2055
+ {/* Show email field only for guests */}
2056
+ {!customerLoggedIn && (
2057
+ <input
2058
+ type="email"
2059
+ value={email}
2060
+ onChange={(e) => setEmail(e.target.value)}
2061
+ placeholder="Email"
2062
+ required
2063
+ />
2064
+ )}
2065
+
2066
+ {/* Shipping address fields */}
2067
+ <input value={shippingAddress.firstName} onChange={(e) => setShippingAddress({...shippingAddress, firstName: e.target.value})} placeholder="First Name" required />
2068
+ <input value={shippingAddress.lastName} onChange={(e) => setShippingAddress({...shippingAddress, lastName: e.target.value})} placeholder="Last Name" required />
2069
+ <input value={shippingAddress.line1} onChange={(e) => setShippingAddress({...shippingAddress, line1: e.target.value})} placeholder="Address" required />
2070
+ <input value={shippingAddress.city} onChange={(e) => setShippingAddress({...shippingAddress, city: e.target.value})} placeholder="City" required />
2071
+ <input value={shippingAddress.postalCode} onChange={(e) => setShippingAddress({...shippingAddress, postalCode: e.target.value})} placeholder="Postal Code" required />
2072
+
2073
+ {/* Shipping rates (for logged-in customers) */}
2074
+ {customerLoggedIn && shippingRates.length > 0 && (
2075
+ <select value={selectedRate || ''} onChange={(e) => setSelectedRate(e.target.value)}>
2076
+ {shippingRates.map((rate) => (
2077
+ <option key={rate.id} value={rate.id}>
2078
+ {rate.name} - ${rate.price}
2079
+ </option>
2080
+ ))}
2081
+ </select>
2082
+ )}
2083
+
2084
+ <button type="submit" disabled={submitting}>
2085
+ {submitting ? 'Processing...' : 'Place Order'}
2086
+ </button>
2087
+
2088
+ {customerLoggedIn && (
2089
+ <p className="text-sm text-green-600">
2090
+ ✓ Logged in - Order will be saved to your account
2091
+ </p>
2092
+ )}
2093
+ </form>
2094
+ );
2095
+ }
2096
+ ```
2097
+
2098
+ > **Key Points:**
2099
+ > - `isLoggedIn()` determines which flow to use
2100
+ > - Logged-in customers use `createCart()` → `createCheckout()` → `completeCheckout()`
2101
+ > - Guests use local cart + `submitGuestOrder()`
2102
+ > - Local cart items are migrated to server cart when customer logs in
2103
+
2104
+ ---
2105
+
1463
2106
  ### Guest Checkout (Single API Call)
1464
2107
 
1465
- This is the recommended checkout for guest users. All cart data is in localStorage, and we submit it in one API call.
2108
+ This checkout is for **guest users only**. All cart data is in localStorage, and we submit it in one API call.
2109
+
2110
+ > **⚠️ WARNING:** Do NOT use this for logged-in customers! Use the Universal Checkout pattern above instead.
1466
2111
 
1467
2112
  ```typescript
1468
2113
  'use client';
@@ -1628,9 +2273,11 @@ export default function CheckoutPage() {
1628
2273
  }
1629
2274
  ```
1630
2275
 
1631
- ### Multi-Step Checkout (Server Cart - For Registered Users)
2276
+ ### Multi-Step Checkout (Server Cart - For Logged-In Customers Only)
2277
+
2278
+ > **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.
1632
2279
 
1633
- For logged-in users with server-side cart:
2280
+ For logged-in users with server-side cart - orders will be linked to their account:
1634
2281
 
1635
2282
  ```typescript
1636
2283
  'use client';
@@ -2188,8 +2835,9 @@ When building a store, implement these pages:
2188
2835
  - [ ] **Product Detail** (`/products/[id]`) - Single product with Add to Cart
2189
2836
  - [ ] **Cart** (`/cart`) - Cart items, update quantity, remove
2190
2837
  - [ ] **Checkout** (`/checkout`) - Multi-step checkout flow
2191
- - [ ] **Login** (`/login`) - Customer login
2192
- - [ ] **Register** (`/register`) - Customer registration
2838
+ - [ ] **Login** (`/login`) - Customer login + social login buttons (Google/Facebook/GitHub if available)
2839
+ - [ ] **Register** (`/register`) - Customer registration + social signup buttons
2840
+ - [ ] **Auth Callback** (`/auth/callback`) - Handle OAuth redirects from Google/Facebook/GitHub
2193
2841
  - [ ] **Verify Email** (`/verify-email`) - Email verification with 6-digit code (if store requires it)
2194
2842
  - [ ] **Account** (`/account`) - Profile and order history
2195
2843