omni-sync-sdk 0.22.1 → 0.22.3
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/AI_BUILDER_PROMPT.md +563 -548
- package/LICENSE +0 -0
- package/README.md +4409 -4386
- package/dist/index.d.mts +118 -14
- package/dist/index.d.ts +118 -14
- package/dist/index.js +10 -6
- package/dist/index.mjs +10 -6
- package/package.json +76 -77
package/AI_BUILDER_PROMPT.md
CHANGED
|
@@ -1,548 +1,563 @@
|
|
|
1
|
-
# OmniSync Store Builder
|
|
2
|
-
|
|
3
|
-
Build a **{store_type}** store called "{store_name}" | Style: **{style}** | Currency: **{currency}**
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## ⛔ STOP! Read These 3 Rules First (Breaking = Store Won't Work)
|
|
8
|
-
|
|
9
|
-
### Rule 1: Guest vs Logged-In = Different Checkout Methods!
|
|
10
|
-
|
|
11
|
-
```typescript
|
|
12
|
-
// ❌ THIS WILL FAIL - "Cart not found" error!
|
|
13
|
-
const cart = await omni.smartGetCart(); // Guest cart has id: "__local__"
|
|
14
|
-
await omni.createCheckout({ cartId: cart.id }); // 💥 "__local__" doesn't exist on server!
|
|
15
|
-
|
|
16
|
-
// ✅ CORRECT - Check user type first!
|
|
17
|
-
if (omni.isCustomerLoggedIn()) {
|
|
18
|
-
// Logged-in user → server cart exists
|
|
19
|
-
const checkout = await omni.createCheckout({ cartId: cart.id });
|
|
20
|
-
const checkoutId = checkout.id;
|
|
21
|
-
} else {
|
|
22
|
-
// Guest user → use startGuestCheckout()
|
|
23
|
-
const result = await omni.startGuestCheckout();
|
|
24
|
-
const checkoutId = result.checkoutId;
|
|
25
|
-
}
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
| User Type | Cart Location | Checkout Method | Get Checkout ID |
|
|
29
|
-
| ------------- | ------------- | ---------------------------- | ------------------- |
|
|
30
|
-
| **Guest** | localStorage | `startGuestCheckout()` | `result.checkoutId` |
|
|
31
|
-
| **Logged-in** | Server | `createCheckout({ cartId })` | `checkout.id` |
|
|
32
|
-
|
|
33
|
-
### Rule 2: Complete Checkout & Clear Cart After Payment!
|
|
34
|
-
|
|
35
|
-
```typescript
|
|
36
|
-
// On /checkout/success page - MUST DO THIS!
|
|
37
|
-
export default function CheckoutSuccessPage() {
|
|
38
|
-
const checkoutId = new URLSearchParams(window.location.search).get('checkout_id');
|
|
39
|
-
|
|
40
|
-
useEffect(() => {
|
|
41
|
-
if (checkoutId) {
|
|
42
|
-
// ⚠️ CRITICAL: This sends the order to the server AND clears the cart!
|
|
43
|
-
// handlePaymentSuccess() only clears the local cart - it does NOT create the order!
|
|
44
|
-
omni.completeGuestCheckout(checkoutId);
|
|
45
|
-
}
|
|
46
|
-
}, []);
|
|
47
|
-
|
|
48
|
-
return <div>Thank you for your order!</div>;
|
|
49
|
-
}
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
> **WARNING:** Do NOT use `handlePaymentSuccess()` to complete an order. It only clears
|
|
53
|
-
> the local cart (localStorage) and does NOT communicate with the server.
|
|
54
|
-
> Always use `completeGuestCheckout()` after payment succeeds.
|
|
55
|
-
|
|
56
|
-
### Rule 3: Never Hardcode Products!
|
|
57
|
-
|
|
58
|
-
```typescript
|
|
59
|
-
// ❌ FORBIDDEN - Store will show fake data!
|
|
60
|
-
const products = [{ id: '1', name: 'T-Shirt', price: 29.99 }];
|
|
61
|
-
|
|
62
|
-
// ✅ CORRECT - Fetch from API
|
|
63
|
-
const { data: products } = await omni.getProducts();
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
---
|
|
67
|
-
|
|
68
|
-
## Quick Setup
|
|
69
|
-
|
|
70
|
-
```bash
|
|
71
|
-
npm install omni-sync-sdk
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
```typescript
|
|
75
|
-
// lib/omni-sync.ts
|
|
76
|
-
import { OmniSyncClient } from 'omni-sync-sdk';
|
|
77
|
-
|
|
78
|
-
export const omni = new OmniSyncClient({
|
|
79
|
-
connectionId: '{connection_id}',
|
|
80
|
-
baseUrl: '{api_url}',
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
// Restore customer session on page load
|
|
84
|
-
export function initOmniSync() {
|
|
85
|
-
if (typeof window === 'undefined') return;
|
|
86
|
-
const token = localStorage.getItem('customerToken');
|
|
87
|
-
if (token) omni.setCustomerToken(token);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Save/clear customer token
|
|
91
|
-
export function setCustomerToken(token: string | null) {
|
|
92
|
-
if (token) {
|
|
93
|
-
localStorage.setItem('customerToken', token);
|
|
94
|
-
omni.setCustomerToken(token);
|
|
95
|
-
} else {
|
|
96
|
-
localStorage.removeItem('customerToken');
|
|
97
|
-
omni.clearCustomerToken();
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
---
|
|
103
|
-
|
|
104
|
-
## Cart (Works for Both Guest & Logged-in)
|
|
105
|
-
|
|
106
|
-
```typescript
|
|
107
|
-
// Get or create cart - handles both guest (localStorage) and logged-in (server) automatically
|
|
108
|
-
const cart = await omni.smartGetCart();
|
|
109
|
-
|
|
110
|
-
// Add to cart
|
|
111
|
-
await omni.smartAddToCart({
|
|
112
|
-
productId: 'prod_xxx',
|
|
113
|
-
variantId: 'var_xxx', // optional, for products with variants
|
|
114
|
-
quantity: 1,
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
// Update quantity (by productId, not itemId!)
|
|
118
|
-
await omni.smartUpdateCartItem('prod_xxx', 2); // productId, quantity
|
|
119
|
-
await omni.smartUpdateCartItem('prod_xxx', 3, 'var_xxx'); // with variant
|
|
120
|
-
|
|
121
|
-
// Remove item (by productId, not itemId!)
|
|
122
|
-
await omni.smartRemoveFromCart('prod_xxx');
|
|
123
|
-
await omni.smartRemoveFromCart('prod_xxx', 'var_xxx'); // with variant
|
|
124
|
-
|
|
125
|
-
// Get cart totals (cart doesn't have .total field!)
|
|
126
|
-
import { getCartTotals } from 'omni-sync-sdk';
|
|
127
|
-
const totals = getCartTotals(cart);
|
|
128
|
-
// { subtotal: 59.98, discount: 10, shipping: 0, total: 49.98 }
|
|
129
|
-
|
|
130
|
-
// ⚠️ LocalCart vs Cart - KEY DIFFERENCES:
|
|
131
|
-
// Server Cart has: id, itemCount, subtotal, discountAmount
|
|
132
|
-
// Guest LocalCart has NONE of these! Only: items, couponCode, customer
|
|
133
|
-
// To check type: if ('id' in cart) { /* server Cart */ } else { /* LocalCart */ }
|
|
134
|
-
// Item count for both: cart.items.length
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
---
|
|
138
|
-
|
|
139
|
-
## 🛒 Partial Checkout (AliExpress Style) - REQUIRED!
|
|
140
|
-
|
|
141
|
-
Cart page MUST have checkboxes so users can select which items to buy:
|
|
142
|
-
|
|
143
|
-
```typescript
|
|
144
|
-
// Cart page - track selected items
|
|
145
|
-
const [selectedIndices, setSelectedIndices] = useState<number[]>(
|
|
146
|
-
cart.items.map((_, i) => i) // All selected by default
|
|
147
|
-
);
|
|
148
|
-
|
|
149
|
-
const toggleItem = (index: number) => {
|
|
150
|
-
setSelectedIndices(prev =>
|
|
151
|
-
prev.includes(index)
|
|
152
|
-
? prev.filter(i => i !== index)
|
|
153
|
-
: [...prev, index]
|
|
154
|
-
);
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
const toggleAll = () => {
|
|
158
|
-
if (selectedIndices.length === cart.items.length) {
|
|
159
|
-
setSelectedIndices([]); // Deselect all
|
|
160
|
-
} else {
|
|
161
|
-
setSelectedIndices(cart.items.map((_, i) => i)); // Select all
|
|
162
|
-
}
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
// In your cart UI:
|
|
166
|
-
<div>
|
|
167
|
-
<label>
|
|
168
|
-
<input
|
|
169
|
-
type="checkbox"
|
|
170
|
-
checked={selectedIndices.length === cart.items.length}
|
|
171
|
-
onChange={toggleAll}
|
|
172
|
-
/>
|
|
173
|
-
Select All
|
|
174
|
-
</label>
|
|
175
|
-
</div>
|
|
176
|
-
|
|
177
|
-
{cart.items.map((item, index) => (
|
|
178
|
-
<div key={index}>
|
|
179
|
-
<input
|
|
180
|
-
type="checkbox"
|
|
181
|
-
checked={selectedIndices.includes(index)}
|
|
182
|
-
onChange={() => toggleItem(index)}
|
|
183
|
-
/>
|
|
184
|
-
{/* ... item details ... */}
|
|
185
|
-
</div>
|
|
186
|
-
))}
|
|
187
|
-
|
|
188
|
-
// On checkout button - pass selected items!
|
|
189
|
-
const handleCheckout = async () => {
|
|
190
|
-
if (selectedIndices.length === 0) {
|
|
191
|
-
alert('Please select items to checkout');
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const result = await omni.startGuestCheckout({ selectedIndices });
|
|
196
|
-
// Only selected items go to checkout, others stay in cart!
|
|
197
|
-
};
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
**Why this matters:**
|
|
201
|
-
|
|
202
|
-
- Users can buy some items now, leave others for later
|
|
203
|
-
- After payment, `completeGuestCheckout()` sends the order and only removes purchased items
|
|
204
|
-
- Remaining items stay in cart for future purchase
|
|
205
|
-
|
|
206
|
-
**⚠️ Order Summary on Checkout Page - Use checkout.lineItems!**
|
|
207
|
-
|
|
208
|
-
```typescript
|
|
209
|
-
// ❌ WRONG - Shows ALL cart items (even unselected ones!)
|
|
210
|
-
<div className="order-summary">
|
|
211
|
-
{cart.items.map(item => (
|
|
212
|
-
<div>{item.product.name} - ${item.price}</div>
|
|
213
|
-
))}
|
|
214
|
-
</div>
|
|
215
|
-
|
|
216
|
-
// ✅ CORRECT - Shows only items being purchased in this checkout
|
|
217
|
-
<div className="order-summary">
|
|
218
|
-
{checkout.lineItems.map(item => (
|
|
219
|
-
<div>{item.product.name} - ${item.price}</div>
|
|
220
|
-
))}
|
|
221
|
-
</div>
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
The `checkout` object's `lineItems` array contains ONLY the items selected for this checkout!
|
|
225
|
-
|
|
226
|
-
---
|
|
227
|
-
|
|
228
|
-
## Complete Checkout Flow
|
|
229
|
-
|
|
230
|
-
### Step 1: Start Checkout (Different for Guest vs Logged-in!)
|
|
231
|
-
|
|
232
|
-
```typescript
|
|
233
|
-
async function startCheckout() {
|
|
234
|
-
const cart = await omni.smartGetCart();
|
|
235
|
-
|
|
236
|
-
if (cart.items.length === 0) {
|
|
237
|
-
alert('Cart is empty');
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
let checkoutId: string;
|
|
242
|
-
|
|
243
|
-
if (omni.isCustomerLoggedIn()) {
|
|
244
|
-
// Logged-in: create checkout from server cart
|
|
245
|
-
const checkout = await omni.createCheckout({ cartId: cart.id });
|
|
246
|
-
checkoutId = checkout.id;
|
|
247
|
-
} else {
|
|
248
|
-
// Guest: use startGuestCheckout (syncs local cart to server)
|
|
249
|
-
const result = await omni.startGuestCheckout();
|
|
250
|
-
if (!result.tracked || !result.checkoutId) {
|
|
251
|
-
throw new Error('Failed to create checkout');
|
|
252
|
-
}
|
|
253
|
-
checkoutId = result.checkoutId;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// Save for payment page
|
|
257
|
-
localStorage.setItem('checkoutId', checkoutId);
|
|
258
|
-
|
|
259
|
-
// Navigate to checkout
|
|
260
|
-
window.location.href = '/checkout';
|
|
261
|
-
}
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
### Step 2: Shipping Address
|
|
265
|
-
|
|
266
|
-
```typescript
|
|
267
|
-
const checkoutId = localStorage.getItem('checkoutId')!;
|
|
268
|
-
|
|
269
|
-
// Set shipping address (email is required!)
|
|
270
|
-
const { checkout, rates } = await omni.setShippingAddress(checkoutId, {
|
|
271
|
-
email: 'customer@example.com',
|
|
272
|
-
firstName: 'John',
|
|
273
|
-
lastName: 'Doe',
|
|
274
|
-
line1: '123 Main St',
|
|
275
|
-
city: 'New York',
|
|
276
|
-
region: 'NY', // ⚠️ Use 'region', NOT 'state'!
|
|
277
|
-
postalCode: '10001',
|
|
278
|
-
country: 'US',
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
// Show available shipping rates
|
|
282
|
-
rates.forEach((rate) => {
|
|
283
|
-
console.log(`${rate.name}: $${rate.price}`);
|
|
284
|
-
});
|
|
285
|
-
```
|
|
286
|
-
|
|
287
|
-
### Step 3: Select Shipping Method
|
|
288
|
-
|
|
289
|
-
```typescript
|
|
290
|
-
await omni.selectShippingMethod(checkoutId, selectedRateId);
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
### Step 4: Payment with Stripe
|
|
294
|
-
|
|
295
|
-
```typescript
|
|
296
|
-
// Install: npm install @stripe/stripe-js @stripe/react-stripe-js
|
|
297
|
-
|
|
298
|
-
// 1. Check if payment is configured
|
|
299
|
-
const { hasPayments, providers } = await omni.getPaymentProviders();
|
|
300
|
-
if (!hasPayments) {
|
|
301
|
-
return <div>Payment not configured for this store</div>;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// 2. Get Stripe config
|
|
305
|
-
const stripeProvider = providers.find(p => p.provider === 'stripe');
|
|
306
|
-
if (!stripeProvider) {
|
|
307
|
-
return <div>Stripe not configured</div>;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// 3. Create payment intent
|
|
311
|
-
const intent = await omni.createPaymentIntent(checkoutId);
|
|
312
|
-
|
|
313
|
-
// 4. Initialize Stripe
|
|
314
|
-
import { loadStripe } from '@stripe/stripe-js';
|
|
315
|
-
const stripe = await loadStripe(stripeProvider.publicKey, {
|
|
316
|
-
stripeAccount: stripeProvider.stripeAccountId,
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
// 5. Confirm payment (in your payment form)
|
|
320
|
-
const { error } = await stripe.confirmPayment({
|
|
321
|
-
elements,
|
|
322
|
-
confirmParams: {
|
|
323
|
-
return_url: `${window.location.origin}/checkout/success?checkout_id=${checkoutId}`,
|
|
324
|
-
},
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
if (error) {
|
|
328
|
-
setError(error.message);
|
|
329
|
-
}
|
|
330
|
-
// If no error, Stripe redirects to success page
|
|
331
|
-
```
|
|
332
|
-
|
|
333
|
-
### Step 5: Success Page (Complete Order & Clear Cart!)
|
|
334
|
-
|
|
335
|
-
```typescript
|
|
336
|
-
// /checkout/success/page.tsx
|
|
337
|
-
'use client';
|
|
338
|
-
import { useEffect, useState } from 'react';
|
|
339
|
-
import { omni } from '@/lib/omni-sync';
|
|
340
|
-
|
|
341
|
-
export default function CheckoutSuccessPage() {
|
|
342
|
-
const [orderNumber, setOrderNumber] = useState<string>();
|
|
343
|
-
const [loading, setLoading] = useState(true);
|
|
344
|
-
|
|
345
|
-
useEffect(() => {
|
|
346
|
-
const checkoutId = new URLSearchParams(window.location.search).get('checkout_id');
|
|
347
|
-
|
|
348
|
-
if (checkoutId) {
|
|
349
|
-
// ⚠️ CRITICAL: Complete the order on the server AND clear the cart!
|
|
350
|
-
// Do NOT use handlePaymentSuccess() - it only clears localStorage!
|
|
351
|
-
omni.completeGuestCheckout(checkoutId).then(result => {
|
|
352
|
-
setOrderNumber(result.orderNumber);
|
|
353
|
-
setLoading(false);
|
|
354
|
-
}).catch(() => {
|
|
355
|
-
// Order may already be completed (e.g., page refresh) - check status
|
|
356
|
-
omni.getPaymentStatus(checkoutId).then(status => {
|
|
357
|
-
if (status.orderNumber) {
|
|
358
|
-
setOrderNumber(status.orderNumber);
|
|
359
|
-
}
|
|
360
|
-
setLoading(false);
|
|
361
|
-
});
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
}, []);
|
|
365
|
-
|
|
366
|
-
return (
|
|
367
|
-
<div className="text-center py-12">
|
|
368
|
-
<h1 className="text-2xl font-bold text-green-600">Thank you for your order!</h1>
|
|
369
|
-
{loading && <p className="mt-2">Processing your order...</p>}
|
|
370
|
-
{orderNumber && <p className="mt-2">Order #{orderNumber}</p>}
|
|
371
|
-
<p className="mt-4">A confirmation email will be sent shortly.</p>
|
|
372
|
-
</div>
|
|
373
|
-
);
|
|
374
|
-
}
|
|
375
|
-
```
|
|
376
|
-
|
|
377
|
-
---
|
|
378
|
-
|
|
379
|
-
## Partial Checkout (AliExpress Style)
|
|
380
|
-
|
|
381
|
-
Allow customers to buy only some items from their cart:
|
|
382
|
-
|
|
383
|
-
```typescript
|
|
384
|
-
// Start checkout with only selected items (by index)
|
|
385
|
-
const result = await omni.startGuestCheckout({
|
|
386
|
-
selectedIndices: [0, 2], // Buy items at index 0 and 2 only
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
// After payment, completeGuestCheckout() sends the order AND removes only those items!
|
|
390
|
-
// Other items stay in cart.
|
|
391
|
-
```
|
|
392
|
-
|
|
393
|
-
---
|
|
394
|
-
|
|
395
|
-
## Products API
|
|
396
|
-
|
|
397
|
-
```typescript
|
|
398
|
-
// List products with pagination
|
|
399
|
-
const { data: products, meta } = await omni.getProducts({
|
|
400
|
-
page: 1,
|
|
401
|
-
limit: 20,
|
|
402
|
-
search: 'blue shirt', // Searches name, description, SKU, categories, tags
|
|
403
|
-
});
|
|
404
|
-
// meta = { page: 1, limit: 20, total: 150, totalPages: 8 }
|
|
405
|
-
|
|
406
|
-
// Get single product by slug (for product detail page)
|
|
407
|
-
const product = await omni.getProductBySlug('blue-cotton-shirt');
|
|
408
|
-
|
|
409
|
-
// Search suggestions (for autocomplete)
|
|
410
|
-
const suggestions = await omni.getSearchSuggestions('blue', 5);
|
|
411
|
-
// { products: [...], categories: [...] }
|
|
412
|
-
```
|
|
413
|
-
|
|
414
|
-
---
|
|
415
|
-
|
|
416
|
-
## Product Custom Fields (Metafields)
|
|
417
|
-
|
|
418
|
-
Products may have custom fields defined by the store owner (e.g., "Material", "Care Instructions", "Warranty").
|
|
419
|
-
|
|
420
|
-
```typescript
|
|
421
|
-
import { getProductMetafield, getProductMetafieldValue } from 'omni-sync-sdk';
|
|
422
|
-
|
|
423
|
-
// Access metafields on a product
|
|
424
|
-
const product = await omni.getProductBySlug('blue-shirt');
|
|
425
|
-
|
|
426
|
-
// Get all custom fields
|
|
427
|
-
product.metafields?.forEach((field) => {
|
|
428
|
-
console.log(`${field.definitionName}: ${field.value}`);
|
|
429
|
-
});
|
|
430
|
-
|
|
431
|
-
// Get specific field by key
|
|
432
|
-
const material = getProductMetafieldValue(product, 'material');
|
|
433
|
-
const careInstructions = getProductMetafield(product, 'care_instructions');
|
|
434
|
-
|
|
435
|
-
// Get available metafield definitions (schema)
|
|
436
|
-
const { definitions } = await omni.getPublicMetafieldDefinitions();
|
|
437
|
-
// Use definitions to build dynamic UI (filters, forms, etc.)
|
|
438
|
-
```
|
|
439
|
-
|
|
440
|
-
> **Tip:** `metafields` may be empty if the store hasn't defined custom fields. Always use optional chaining.
|
|
441
|
-
|
|
442
|
-
---
|
|
443
|
-
|
|
444
|
-
## Customer Authentication
|
|
445
|
-
|
|
446
|
-
```typescript
|
|
447
|
-
// Register
|
|
448
|
-
const { customer, token } = await omni.registerCustomer({
|
|
449
|
-
email: 'john@example.com',
|
|
450
|
-
password: 'securepass123',
|
|
451
|
-
firstName: 'John',
|
|
452
|
-
lastName: 'Doe',
|
|
453
|
-
});
|
|
454
|
-
setCustomerToken(token);
|
|
455
|
-
|
|
456
|
-
// Login
|
|
457
|
-
const { customer, token } = await omni.loginCustomer('john@example.com', 'password');
|
|
458
|
-
setCustomerToken(token);
|
|
459
|
-
|
|
460
|
-
// Logout
|
|
461
|
-
setCustomerToken(null);
|
|
462
|
-
|
|
463
|
-
// Get profile (requires token)
|
|
464
|
-
const profile = await omni.getMyProfile();
|
|
465
|
-
|
|
466
|
-
// Get order history
|
|
467
|
-
const { data: orders, meta } = await omni.getMyOrders({ page: 1, limit: 10 });
|
|
468
|
-
```
|
|
469
|
-
|
|
470
|
-
---
|
|
471
|
-
|
|
472
|
-
## OAuth / Social Login
|
|
473
|
-
|
|
474
|
-
```typescript
|
|
475
|
-
// Get available providers for this store
|
|
476
|
-
const { providers } = await omni.getAvailableOAuthProviders();
|
|
477
|
-
// providers = ['GOOGLE', 'FACEBOOK', 'GITHUB']
|
|
478
|
-
|
|
479
|
-
// Redirect to OAuth provider
|
|
480
|
-
const { authorizationUrl } = await omni.getOAuthAuthorizeUrl('GOOGLE', {
|
|
481
|
-
redirectUrl: `${window.location.origin}/auth/callback`,
|
|
482
|
-
});
|
|
483
|
-
window.location.href = authorizationUrl;
|
|
484
|
-
|
|
485
|
-
// Handle callback (on /auth/callback page)
|
|
486
|
-
const code = new URLSearchParams(window.location.search).get('code');
|
|
487
|
-
const state = new URLSearchParams(window.location.search).get('state');
|
|
488
|
-
const { customer, token } = await omni.handleOAuthCallback('GOOGLE', code!, state!);
|
|
489
|
-
setCustomerToken(token);
|
|
490
|
-
```
|
|
491
|
-
|
|
492
|
-
---
|
|
493
|
-
|
|
494
|
-
## Required Pages Checklist
|
|
495
|
-
|
|
496
|
-
- [ ] **Home** (`/`) - Featured products grid
|
|
497
|
-
- [ ] **Products** (`/products`) - Product list with infinite scroll
|
|
498
|
-
- [ ] **Product Detail** (`/products/[slug]`) - Use `getProductBySlug(slug)`
|
|
499
|
-
- [ ] **Cart** (`/cart`) - Show items, quantities, totals
|
|
500
|
-
- [ ] **Checkout** (`/checkout`) - Address → Shipping → Payment
|
|
501
|
-
- [ ] **Success** (`/checkout/success`) - **Must call `completeGuestCheckout()`!**
|
|
502
|
-
- [ ] **Login** (`/login`) - Email/password + social buttons
|
|
503
|
-
- [ ] **Register** (`/register`) - Registration form
|
|
504
|
-
- [ ] **Account** (`/account`) - Profile + order history
|
|
505
|
-
- [ ] **Header** - Logo, nav, cart icon with count, search
|
|
506
|
-
|
|
507
|
-
---
|
|
508
|
-
|
|
509
|
-
## Common Type Gotchas
|
|
510
|
-
|
|
511
|
-
```typescript
|
|
512
|
-
// ❌ WRONG // ✅ CORRECT
|
|
513
|
-
address.state address.region
|
|
514
|
-
cart.total getCartTotals(cart).total
|
|
515
|
-
cart.discount cart.discountAmount
|
|
516
|
-
item.name (in cart) item.product.name
|
|
517
|
-
response.url (OAuth) response.authorizationUrl
|
|
518
|
-
providers.forEach (OAuth) response.providers.forEach
|
|
519
|
-
status === 'completed' status === 'succeeded'
|
|
520
|
-
product.metafields.name product.metafields[0].definitionName
|
|
521
|
-
product.metafields.key product.metafields[0].definitionKey
|
|
522
|
-
orderItem.unitPrice orderItem.price (OrderItem is FLAT, not nested!)
|
|
523
|
-
cartItem.price cartItem.unitPrice (Cart/Checkout items use unitPrice)
|
|
524
|
-
waitResult.orderNumber waitResult.status.orderNumber (nested in PaymentStatus)
|
|
525
|
-
variant.attributes.map(...) Object.entries(variant.attributes || {}) (it's an object!)
|
|
526
|
-
categorySuggestion.slug // ❌ doesn't exist! Only: id, name, productCount
|
|
527
|
-
order.status === 'COMPLETED' order.status === 'delivered' (OrderStatus is lowercase!)
|
|
528
|
-
getCartTotals(localCart) // ❌ LocalCart has no subtotal! Calculate manually
|
|
529
|
-
result.checkoutId (guest checkout) // ⚠️ Check result.tracked first! It's a union type
|
|
530
|
-
```
|
|
531
|
-
|
|
532
|
-
**Key distinctions:**
|
|
533
|
-
|
|
534
|
-
- **OrderItem** (from orders): Flat structure — `item.price`, `item.name`, `item.image`
|
|
535
|
-
- **CartItem / CheckoutLineItem**: Nested structure — `item.unitPrice`, `item.product.name`, `item.product.images`
|
|
536
|
-
- **`getCartTotals()`** only works with **server Cart** (has `subtotal`/`discountAmount`). For **LocalCart**, calculate manually:
|
|
537
|
-
```typescript
|
|
538
|
-
const subtotal = cart.items.reduce(
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
1
|
+
# OmniSync Store Builder
|
|
2
|
+
|
|
3
|
+
Build a **{store_type}** store called "{store_name}" | Style: **{style}** | Currency: **{currency}**
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## ⛔ STOP! Read These 3 Rules First (Breaking = Store Won't Work)
|
|
8
|
+
|
|
9
|
+
### Rule 1: Guest vs Logged-In = Different Checkout Methods!
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
// ❌ THIS WILL FAIL - "Cart not found" error!
|
|
13
|
+
const cart = await omni.smartGetCart(); // Guest cart has id: "__local__"
|
|
14
|
+
await omni.createCheckout({ cartId: cart.id }); // 💥 "__local__" doesn't exist on server!
|
|
15
|
+
|
|
16
|
+
// ✅ CORRECT - Check user type first!
|
|
17
|
+
if (omni.isCustomerLoggedIn()) {
|
|
18
|
+
// Logged-in user → server cart exists
|
|
19
|
+
const checkout = await omni.createCheckout({ cartId: cart.id });
|
|
20
|
+
const checkoutId = checkout.id;
|
|
21
|
+
} else {
|
|
22
|
+
// Guest user → use startGuestCheckout()
|
|
23
|
+
const result = await omni.startGuestCheckout();
|
|
24
|
+
const checkoutId = result.checkoutId;
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
| User Type | Cart Location | Checkout Method | Get Checkout ID |
|
|
29
|
+
| ------------- | ------------- | ---------------------------- | ------------------- |
|
|
30
|
+
| **Guest** | localStorage | `startGuestCheckout()` | `result.checkoutId` |
|
|
31
|
+
| **Logged-in** | Server | `createCheckout({ cartId })` | `checkout.id` |
|
|
32
|
+
|
|
33
|
+
### Rule 2: Complete Checkout & Clear Cart After Payment!
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
// On /checkout/success page - MUST DO THIS!
|
|
37
|
+
export default function CheckoutSuccessPage() {
|
|
38
|
+
const checkoutId = new URLSearchParams(window.location.search).get('checkout_id');
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (checkoutId) {
|
|
42
|
+
// ⚠️ CRITICAL: This sends the order to the server AND clears the cart!
|
|
43
|
+
// handlePaymentSuccess() only clears the local cart - it does NOT create the order!
|
|
44
|
+
omni.completeGuestCheckout(checkoutId);
|
|
45
|
+
}
|
|
46
|
+
}, []);
|
|
47
|
+
|
|
48
|
+
return <div>Thank you for your order!</div>;
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
> **WARNING:** Do NOT use `handlePaymentSuccess()` to complete an order. It only clears
|
|
53
|
+
> the local cart (localStorage) and does NOT communicate with the server.
|
|
54
|
+
> Always use `completeGuestCheckout()` after payment succeeds.
|
|
55
|
+
|
|
56
|
+
### Rule 3: Never Hardcode Products!
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
// ❌ FORBIDDEN - Store will show fake data!
|
|
60
|
+
const products = [{ id: '1', name: 'T-Shirt', price: 29.99 }];
|
|
61
|
+
|
|
62
|
+
// ✅ CORRECT - Fetch from API
|
|
63
|
+
const { data: products } = await omni.getProducts();
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Quick Setup
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npm install omni-sync-sdk
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
// lib/omni-sync.ts
|
|
76
|
+
import { OmniSyncClient } from 'omni-sync-sdk';
|
|
77
|
+
|
|
78
|
+
export const omni = new OmniSyncClient({
|
|
79
|
+
connectionId: '{connection_id}',
|
|
80
|
+
baseUrl: '{api_url}',
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Restore customer session on page load
|
|
84
|
+
export function initOmniSync() {
|
|
85
|
+
if (typeof window === 'undefined') return;
|
|
86
|
+
const token = localStorage.getItem('customerToken');
|
|
87
|
+
if (token) omni.setCustomerToken(token);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Save/clear customer token
|
|
91
|
+
export function setCustomerToken(token: string | null) {
|
|
92
|
+
if (token) {
|
|
93
|
+
localStorage.setItem('customerToken', token);
|
|
94
|
+
omni.setCustomerToken(token);
|
|
95
|
+
} else {
|
|
96
|
+
localStorage.removeItem('customerToken');
|
|
97
|
+
omni.clearCustomerToken();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Cart (Works for Both Guest & Logged-in)
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
// Get or create cart - handles both guest (localStorage) and logged-in (server) automatically
|
|
108
|
+
const cart = await omni.smartGetCart();
|
|
109
|
+
|
|
110
|
+
// Add to cart
|
|
111
|
+
await omni.smartAddToCart({
|
|
112
|
+
productId: 'prod_xxx',
|
|
113
|
+
variantId: 'var_xxx', // optional, for products with variants
|
|
114
|
+
quantity: 1,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Update quantity (by productId, not itemId!)
|
|
118
|
+
await omni.smartUpdateCartItem('prod_xxx', 2); // productId, quantity
|
|
119
|
+
await omni.smartUpdateCartItem('prod_xxx', 3, 'var_xxx'); // with variant
|
|
120
|
+
|
|
121
|
+
// Remove item (by productId, not itemId!)
|
|
122
|
+
await omni.smartRemoveFromCart('prod_xxx');
|
|
123
|
+
await omni.smartRemoveFromCart('prod_xxx', 'var_xxx'); // with variant
|
|
124
|
+
|
|
125
|
+
// Get cart totals (cart doesn't have .total field!)
|
|
126
|
+
import { getCartTotals } from 'omni-sync-sdk';
|
|
127
|
+
const totals = getCartTotals(cart);
|
|
128
|
+
// { subtotal: 59.98, discount: 10, shipping: 0, total: 49.98 }
|
|
129
|
+
|
|
130
|
+
// ⚠️ LocalCart vs Cart - KEY DIFFERENCES:
|
|
131
|
+
// Server Cart has: id, itemCount, subtotal, discountAmount
|
|
132
|
+
// Guest LocalCart has NONE of these! Only: items, couponCode, customer
|
|
133
|
+
// To check type: if ('id' in cart) { /* server Cart */ } else { /* LocalCart */ }
|
|
134
|
+
// Item count for both: cart.items.length
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## 🛒 Partial Checkout (AliExpress Style) - REQUIRED!
|
|
140
|
+
|
|
141
|
+
Cart page MUST have checkboxes so users can select which items to buy:
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
// Cart page - track selected items
|
|
145
|
+
const [selectedIndices, setSelectedIndices] = useState<number[]>(
|
|
146
|
+
cart.items.map((_, i) => i) // All selected by default
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const toggleItem = (index: number) => {
|
|
150
|
+
setSelectedIndices(prev =>
|
|
151
|
+
prev.includes(index)
|
|
152
|
+
? prev.filter(i => i !== index)
|
|
153
|
+
: [...prev, index]
|
|
154
|
+
);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const toggleAll = () => {
|
|
158
|
+
if (selectedIndices.length === cart.items.length) {
|
|
159
|
+
setSelectedIndices([]); // Deselect all
|
|
160
|
+
} else {
|
|
161
|
+
setSelectedIndices(cart.items.map((_, i) => i)); // Select all
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// In your cart UI:
|
|
166
|
+
<div>
|
|
167
|
+
<label>
|
|
168
|
+
<input
|
|
169
|
+
type="checkbox"
|
|
170
|
+
checked={selectedIndices.length === cart.items.length}
|
|
171
|
+
onChange={toggleAll}
|
|
172
|
+
/>
|
|
173
|
+
Select All
|
|
174
|
+
</label>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
{cart.items.map((item, index) => (
|
|
178
|
+
<div key={index}>
|
|
179
|
+
<input
|
|
180
|
+
type="checkbox"
|
|
181
|
+
checked={selectedIndices.includes(index)}
|
|
182
|
+
onChange={() => toggleItem(index)}
|
|
183
|
+
/>
|
|
184
|
+
{/* ... item details ... */}
|
|
185
|
+
</div>
|
|
186
|
+
))}
|
|
187
|
+
|
|
188
|
+
// On checkout button - pass selected items!
|
|
189
|
+
const handleCheckout = async () => {
|
|
190
|
+
if (selectedIndices.length === 0) {
|
|
191
|
+
alert('Please select items to checkout');
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const result = await omni.startGuestCheckout({ selectedIndices });
|
|
196
|
+
// Only selected items go to checkout, others stay in cart!
|
|
197
|
+
};
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**Why this matters:**
|
|
201
|
+
|
|
202
|
+
- Users can buy some items now, leave others for later
|
|
203
|
+
- After payment, `completeGuestCheckout()` sends the order and only removes purchased items
|
|
204
|
+
- Remaining items stay in cart for future purchase
|
|
205
|
+
|
|
206
|
+
**⚠️ Order Summary on Checkout Page - Use checkout.lineItems!**
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
// ❌ WRONG - Shows ALL cart items (even unselected ones!)
|
|
210
|
+
<div className="order-summary">
|
|
211
|
+
{cart.items.map(item => (
|
|
212
|
+
<div>{item.product.name} - ${item.price}</div>
|
|
213
|
+
))}
|
|
214
|
+
</div>
|
|
215
|
+
|
|
216
|
+
// ✅ CORRECT - Shows only items being purchased in this checkout
|
|
217
|
+
<div className="order-summary">
|
|
218
|
+
{checkout.lineItems.map(item => (
|
|
219
|
+
<div>{item.product.name} - ${item.price}</div>
|
|
220
|
+
))}
|
|
221
|
+
</div>
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
The `checkout` object's `lineItems` array contains ONLY the items selected for this checkout!
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## Complete Checkout Flow
|
|
229
|
+
|
|
230
|
+
### Step 1: Start Checkout (Different for Guest vs Logged-in!)
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
async function startCheckout() {
|
|
234
|
+
const cart = await omni.smartGetCart();
|
|
235
|
+
|
|
236
|
+
if (cart.items.length === 0) {
|
|
237
|
+
alert('Cart is empty');
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
let checkoutId: string;
|
|
242
|
+
|
|
243
|
+
if (omni.isCustomerLoggedIn()) {
|
|
244
|
+
// Logged-in: create checkout from server cart
|
|
245
|
+
const checkout = await omni.createCheckout({ cartId: cart.id });
|
|
246
|
+
checkoutId = checkout.id;
|
|
247
|
+
} else {
|
|
248
|
+
// Guest: use startGuestCheckout (syncs local cart to server)
|
|
249
|
+
const result = await omni.startGuestCheckout();
|
|
250
|
+
if (!result.tracked || !result.checkoutId) {
|
|
251
|
+
throw new Error('Failed to create checkout');
|
|
252
|
+
}
|
|
253
|
+
checkoutId = result.checkoutId;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Save for payment page
|
|
257
|
+
localStorage.setItem('checkoutId', checkoutId);
|
|
258
|
+
|
|
259
|
+
// Navigate to checkout
|
|
260
|
+
window.location.href = '/checkout';
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### Step 2: Shipping Address
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
const checkoutId = localStorage.getItem('checkoutId')!;
|
|
268
|
+
|
|
269
|
+
// Set shipping address (email is required!)
|
|
270
|
+
const { checkout, rates } = await omni.setShippingAddress(checkoutId, {
|
|
271
|
+
email: 'customer@example.com',
|
|
272
|
+
firstName: 'John',
|
|
273
|
+
lastName: 'Doe',
|
|
274
|
+
line1: '123 Main St',
|
|
275
|
+
city: 'New York',
|
|
276
|
+
region: 'NY', // ⚠️ Use 'region', NOT 'state'!
|
|
277
|
+
postalCode: '10001',
|
|
278
|
+
country: 'US',
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// Show available shipping rates
|
|
282
|
+
rates.forEach((rate) => {
|
|
283
|
+
console.log(`${rate.name}: $${rate.price}`);
|
|
284
|
+
});
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Step 3: Select Shipping Method
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
await omni.selectShippingMethod(checkoutId, selectedRateId);
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Step 4: Payment with Stripe
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
// Install: npm install @stripe/stripe-js @stripe/react-stripe-js
|
|
297
|
+
|
|
298
|
+
// 1. Check if payment is configured
|
|
299
|
+
const { hasPayments, providers } = await omni.getPaymentProviders();
|
|
300
|
+
if (!hasPayments) {
|
|
301
|
+
return <div>Payment not configured for this store</div>;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// 2. Get Stripe config
|
|
305
|
+
const stripeProvider = providers.find(p => p.provider === 'stripe');
|
|
306
|
+
if (!stripeProvider) {
|
|
307
|
+
return <div>Stripe not configured</div>;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// 3. Create payment intent
|
|
311
|
+
const intent = await omni.createPaymentIntent(checkoutId);
|
|
312
|
+
|
|
313
|
+
// 4. Initialize Stripe
|
|
314
|
+
import { loadStripe } from '@stripe/stripe-js';
|
|
315
|
+
const stripe = await loadStripe(stripeProvider.publicKey, {
|
|
316
|
+
stripeAccount: stripeProvider.stripeAccountId,
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// 5. Confirm payment (in your payment form)
|
|
320
|
+
const { error } = await stripe.confirmPayment({
|
|
321
|
+
elements,
|
|
322
|
+
confirmParams: {
|
|
323
|
+
return_url: `${window.location.origin}/checkout/success?checkout_id=${checkoutId}`,
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
if (error) {
|
|
328
|
+
setError(error.message);
|
|
329
|
+
}
|
|
330
|
+
// If no error, Stripe redirects to success page
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### Step 5: Success Page (Complete Order & Clear Cart!)
|
|
334
|
+
|
|
335
|
+
```typescript
|
|
336
|
+
// /checkout/success/page.tsx
|
|
337
|
+
'use client';
|
|
338
|
+
import { useEffect, useState } from 'react';
|
|
339
|
+
import { omni } from '@/lib/omni-sync';
|
|
340
|
+
|
|
341
|
+
export default function CheckoutSuccessPage() {
|
|
342
|
+
const [orderNumber, setOrderNumber] = useState<string>();
|
|
343
|
+
const [loading, setLoading] = useState(true);
|
|
344
|
+
|
|
345
|
+
useEffect(() => {
|
|
346
|
+
const checkoutId = new URLSearchParams(window.location.search).get('checkout_id');
|
|
347
|
+
|
|
348
|
+
if (checkoutId) {
|
|
349
|
+
// ⚠️ CRITICAL: Complete the order on the server AND clear the cart!
|
|
350
|
+
// Do NOT use handlePaymentSuccess() - it only clears localStorage!
|
|
351
|
+
omni.completeGuestCheckout(checkoutId).then(result => {
|
|
352
|
+
setOrderNumber(result.orderNumber);
|
|
353
|
+
setLoading(false);
|
|
354
|
+
}).catch(() => {
|
|
355
|
+
// Order may already be completed (e.g., page refresh) - check status
|
|
356
|
+
omni.getPaymentStatus(checkoutId).then(status => {
|
|
357
|
+
if (status.orderNumber) {
|
|
358
|
+
setOrderNumber(status.orderNumber);
|
|
359
|
+
}
|
|
360
|
+
setLoading(false);
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
}, []);
|
|
365
|
+
|
|
366
|
+
return (
|
|
367
|
+
<div className="text-center py-12">
|
|
368
|
+
<h1 className="text-2xl font-bold text-green-600">Thank you for your order!</h1>
|
|
369
|
+
{loading && <p className="mt-2">Processing your order...</p>}
|
|
370
|
+
{orderNumber && <p className="mt-2">Order #{orderNumber}</p>}
|
|
371
|
+
<p className="mt-4">A confirmation email will be sent shortly.</p>
|
|
372
|
+
</div>
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
---
|
|
378
|
+
|
|
379
|
+
## Partial Checkout (AliExpress Style)
|
|
380
|
+
|
|
381
|
+
Allow customers to buy only some items from their cart:
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
// Start checkout with only selected items (by index)
|
|
385
|
+
const result = await omni.startGuestCheckout({
|
|
386
|
+
selectedIndices: [0, 2], // Buy items at index 0 and 2 only
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// After payment, completeGuestCheckout() sends the order AND removes only those items!
|
|
390
|
+
// Other items stay in cart.
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
---
|
|
394
|
+
|
|
395
|
+
## Products API
|
|
396
|
+
|
|
397
|
+
```typescript
|
|
398
|
+
// List products with pagination
|
|
399
|
+
const { data: products, meta } = await omni.getProducts({
|
|
400
|
+
page: 1,
|
|
401
|
+
limit: 20,
|
|
402
|
+
search: 'blue shirt', // Searches name, description, SKU, categories, tags
|
|
403
|
+
});
|
|
404
|
+
// meta = { page: 1, limit: 20, total: 150, totalPages: 8 }
|
|
405
|
+
|
|
406
|
+
// Get single product by slug (for product detail page)
|
|
407
|
+
const product = await omni.getProductBySlug('blue-cotton-shirt');
|
|
408
|
+
|
|
409
|
+
// Search suggestions (for autocomplete)
|
|
410
|
+
const suggestions = await omni.getSearchSuggestions('blue', 5);
|
|
411
|
+
// { products: [...], categories: [...] }
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
---
|
|
415
|
+
|
|
416
|
+
## Product Custom Fields (Metafields)
|
|
417
|
+
|
|
418
|
+
Products may have custom fields defined by the store owner (e.g., "Material", "Care Instructions", "Warranty").
|
|
419
|
+
|
|
420
|
+
```typescript
|
|
421
|
+
import { getProductMetafield, getProductMetafieldValue } from 'omni-sync-sdk';
|
|
422
|
+
|
|
423
|
+
// Access metafields on a product
|
|
424
|
+
const product = await omni.getProductBySlug('blue-shirt');
|
|
425
|
+
|
|
426
|
+
// Get all custom fields
|
|
427
|
+
product.metafields?.forEach((field) => {
|
|
428
|
+
console.log(`${field.definitionName}: ${field.value}`);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
// Get specific field by key
|
|
432
|
+
const material = getProductMetafieldValue(product, 'material');
|
|
433
|
+
const careInstructions = getProductMetafield(product, 'care_instructions');
|
|
434
|
+
|
|
435
|
+
// Get available metafield definitions (schema)
|
|
436
|
+
const { definitions } = await omni.getPublicMetafieldDefinitions();
|
|
437
|
+
// Use definitions to build dynamic UI (filters, forms, etc.)
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
> **Tip:** `metafields` may be empty if the store hasn't defined custom fields. Always use optional chaining.
|
|
441
|
+
|
|
442
|
+
---
|
|
443
|
+
|
|
444
|
+
## Customer Authentication
|
|
445
|
+
|
|
446
|
+
```typescript
|
|
447
|
+
// Register
|
|
448
|
+
const { customer, token } = await omni.registerCustomer({
|
|
449
|
+
email: 'john@example.com',
|
|
450
|
+
password: 'securepass123',
|
|
451
|
+
firstName: 'John',
|
|
452
|
+
lastName: 'Doe',
|
|
453
|
+
});
|
|
454
|
+
setCustomerToken(token);
|
|
455
|
+
|
|
456
|
+
// Login
|
|
457
|
+
const { customer, token } = await omni.loginCustomer('john@example.com', 'password');
|
|
458
|
+
setCustomerToken(token);
|
|
459
|
+
|
|
460
|
+
// Logout
|
|
461
|
+
setCustomerToken(null);
|
|
462
|
+
|
|
463
|
+
// Get profile (requires token)
|
|
464
|
+
const profile = await omni.getMyProfile();
|
|
465
|
+
|
|
466
|
+
// Get order history
|
|
467
|
+
const { data: orders, meta } = await omni.getMyOrders({ page: 1, limit: 10 });
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
---
|
|
471
|
+
|
|
472
|
+
## OAuth / Social Login
|
|
473
|
+
|
|
474
|
+
```typescript
|
|
475
|
+
// Get available providers for this store
|
|
476
|
+
const { providers } = await omni.getAvailableOAuthProviders();
|
|
477
|
+
// providers = ['GOOGLE', 'FACEBOOK', 'GITHUB']
|
|
478
|
+
|
|
479
|
+
// Redirect to OAuth provider
|
|
480
|
+
const { authorizationUrl } = await omni.getOAuthAuthorizeUrl('GOOGLE', {
|
|
481
|
+
redirectUrl: `${window.location.origin}/auth/callback`,
|
|
482
|
+
});
|
|
483
|
+
window.location.href = authorizationUrl;
|
|
484
|
+
|
|
485
|
+
// Handle callback (on /auth/callback page)
|
|
486
|
+
const code = new URLSearchParams(window.location.search).get('code');
|
|
487
|
+
const state = new URLSearchParams(window.location.search).get('state');
|
|
488
|
+
const { customer, token } = await omni.handleOAuthCallback('GOOGLE', code!, state!);
|
|
489
|
+
setCustomerToken(token);
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
---
|
|
493
|
+
|
|
494
|
+
## Required Pages Checklist
|
|
495
|
+
|
|
496
|
+
- [ ] **Home** (`/`) - Featured products grid
|
|
497
|
+
- [ ] **Products** (`/products`) - Product list with infinite scroll
|
|
498
|
+
- [ ] **Product Detail** (`/products/[slug]`) - Use `getProductBySlug(slug)`
|
|
499
|
+
- [ ] **Cart** (`/cart`) - Show items, quantities, totals
|
|
500
|
+
- [ ] **Checkout** (`/checkout`) - Address → Shipping → Payment
|
|
501
|
+
- [ ] **Success** (`/checkout/success`) - **Must call `completeGuestCheckout()`!**
|
|
502
|
+
- [ ] **Login** (`/login`) - Email/password + social buttons
|
|
503
|
+
- [ ] **Register** (`/register`) - Registration form
|
|
504
|
+
- [ ] **Account** (`/account`) - Profile + order history
|
|
505
|
+
- [ ] **Header** - Logo, nav, cart icon with count, search
|
|
506
|
+
|
|
507
|
+
---
|
|
508
|
+
|
|
509
|
+
## Common Type Gotchas
|
|
510
|
+
|
|
511
|
+
```typescript
|
|
512
|
+
// ❌ WRONG // ✅ CORRECT
|
|
513
|
+
address.state address.region
|
|
514
|
+
cart.total getCartTotals(cart).total
|
|
515
|
+
cart.discount cart.discountAmount
|
|
516
|
+
item.name (in cart) item.product.name
|
|
517
|
+
response.url (OAuth) response.authorizationUrl
|
|
518
|
+
providers.forEach (OAuth) response.providers.forEach
|
|
519
|
+
status === 'completed' status === 'succeeded'
|
|
520
|
+
product.metafields.name product.metafields[0].definitionName
|
|
521
|
+
product.metafields.key product.metafields[0].definitionKey
|
|
522
|
+
orderItem.unitPrice orderItem.price (OrderItem is FLAT, not nested!)
|
|
523
|
+
cartItem.price cartItem.unitPrice (Cart/Checkout items use unitPrice)
|
|
524
|
+
waitResult.orderNumber waitResult.status.orderNumber (nested in PaymentStatus)
|
|
525
|
+
variant.attributes.map(...) Object.entries(variant.attributes || {}) (it's an object!)
|
|
526
|
+
categorySuggestion.slug // ❌ doesn't exist! Only: id, name, productCount
|
|
527
|
+
order.status === 'COMPLETED' order.status === 'delivered' (OrderStatus is lowercase!)
|
|
528
|
+
getCartTotals(localCart) // ❌ LocalCart has no subtotal! Calculate manually
|
|
529
|
+
result.checkoutId (guest checkout) // ⚠️ Check result.tracked first! It's a union type
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
**Key distinctions:**
|
|
533
|
+
|
|
534
|
+
- **OrderItem** (from orders): Flat structure — `item.price`, `item.name`, `item.image`
|
|
535
|
+
- **CartItem / CheckoutLineItem**: Nested structure — `item.unitPrice`, `item.product.name`, `item.product.images`
|
|
536
|
+
- **`getCartTotals()`** only works with **server Cart** (has `subtotal`/`discountAmount`). For **LocalCart**, calculate manually:
|
|
537
|
+
```typescript
|
|
538
|
+
const subtotal = cart.items.reduce(
|
|
539
|
+
(sum, item) => sum + parseFloat(item.price || '0') * item.quantity,
|
|
540
|
+
0
|
|
541
|
+
);
|
|
542
|
+
```
|
|
543
|
+
- **`GuestCheckoutStartResponse`** is a union type — always check `result.tracked` before accessing `result.checkoutId`
|
|
544
|
+
- **`WaitForOrderResult`** has `result.status.orderNumber`, NOT `result.orderNumber`. But `completeGuestCheckout()` returns `GuestOrderResponse` which DOES have `result.orderNumber` directly.
|
|
545
|
+
- **⚠️ HYDRATION: Never use `useState(omni.getLocalCart())`** in Next.js — causes hydration mismatch! Server has no localStorage (empty cart) but client does (real cart). Always start with empty state and load in `useEffect`:
|
|
546
|
+
|
|
547
|
+
```typescript
|
|
548
|
+
// ❌ WRONG — hydration mismatch!
|
|
549
|
+
const [cart, setCart] = useState(omni.getLocalCart());
|
|
550
|
+
|
|
551
|
+
// ✅ CORRECT — load after hydration
|
|
552
|
+
const [cart, setCart] = useState<LocalCart>({ items: [], updatedAt: '' });
|
|
553
|
+
useEffect(() => {
|
|
554
|
+
setCart(omni.getLocalCart());
|
|
555
|
+
}, []);
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
---
|
|
559
|
+
|
|
560
|
+
## Full SDK Documentation
|
|
561
|
+
|
|
562
|
+
For complete API reference and working code examples:
|
|
563
|
+
**https://brainerce.com/docs/sdk**
|