omni-sync-sdk 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +237 -15
- package/dist/index.d.mts +442 -2
- package/dist/index.d.ts +442 -2
- package/dist/index.js +49 -0
- package/dist/index.mjs +46 -0
- package/package.json +10 -9
- package/LICENSE +0 -0
package/README.md
CHANGED
|
@@ -260,6 +260,111 @@ export function isLoggedIn(): boolean {
|
|
|
260
260
|
|
|
261
261
|
---
|
|
262
262
|
|
|
263
|
+
## Important: Cart & Checkout Data Structures
|
|
264
|
+
|
|
265
|
+
### Nested Product/Variant Structure
|
|
266
|
+
|
|
267
|
+
Cart and Checkout items use a **nested structure** for product and variant data. This is a common pattern that prevents data duplication and ensures consistency.
|
|
268
|
+
|
|
269
|
+
**Common Mistake:**
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
// WRONG - product name is NOT at top level
|
|
273
|
+
const name = item.name; // undefined!
|
|
274
|
+
const sku = item.sku; // undefined!
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
**Correct Access Pattern:**
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
// CORRECT - access via nested objects
|
|
281
|
+
const name = item.product.name;
|
|
282
|
+
const sku = item.product.sku;
|
|
283
|
+
const variantName = item.variant?.name;
|
|
284
|
+
const variantSku = item.variant?.sku;
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Field Mapping Reference
|
|
288
|
+
|
|
289
|
+
| What You Want | CartItem | CheckoutLineItem |
|
|
290
|
+
| -------------- | ------------------------- | ------------------------- |
|
|
291
|
+
| Product Name | `item.product.name` | `item.product.name` |
|
|
292
|
+
| Product SKU | `item.product.sku` | `item.product.sku` |
|
|
293
|
+
| Product ID | `item.productId` | `item.productId` |
|
|
294
|
+
| Product Images | `item.product.images` | `item.product.images` |
|
|
295
|
+
| Variant Name | `item.variant?.name` | `item.variant?.name` |
|
|
296
|
+
| Variant SKU | `item.variant?.sku` | `item.variant?.sku` |
|
|
297
|
+
| Variant ID | `item.variantId` | `item.variantId` |
|
|
298
|
+
| Unit Price | `item.unitPrice` (string) | `item.unitPrice` (string) |
|
|
299
|
+
| Quantity | `item.quantity` | `item.quantity` |
|
|
300
|
+
|
|
301
|
+
### Price Fields Are Strings
|
|
302
|
+
|
|
303
|
+
All monetary values in Cart and Checkout are returned as **strings** (e.g., `"29.99"`) to preserve decimal precision across different systems. Use `parseFloat()` or the `formatPrice()` helper:
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
// Monetary fields that are strings:
|
|
307
|
+
// - CartItem: unitPrice, discountAmount
|
|
308
|
+
// - Cart: subtotal, discountAmount
|
|
309
|
+
// - CheckoutLineItem: unitPrice, discountAmount
|
|
310
|
+
// - Checkout: subtotal, discountAmount, shippingAmount, taxAmount, total
|
|
311
|
+
// - ShippingRate: price
|
|
312
|
+
|
|
313
|
+
import { formatPrice } from 'omni-sync-sdk';
|
|
314
|
+
|
|
315
|
+
// Option 1: Using formatPrice helper (recommended)
|
|
316
|
+
const cart = await omni.getCart(cartId);
|
|
317
|
+
const total = formatPrice(cart.subtotal); // "$59.98"
|
|
318
|
+
const totalNum = formatPrice(cart.subtotal, { asNumber: true }); // 59.98
|
|
319
|
+
|
|
320
|
+
// Option 2: Manual parseFloat
|
|
321
|
+
const subtotal = parseFloat(cart.subtotal);
|
|
322
|
+
const discount = parseFloat(cart.discountAmount);
|
|
323
|
+
const total = subtotal - discount;
|
|
324
|
+
|
|
325
|
+
// Line item total
|
|
326
|
+
cart.items.forEach((item) => {
|
|
327
|
+
const lineTotal = parseFloat(item.unitPrice) * item.quantity;
|
|
328
|
+
console.log(`${item.product.name}: $${lineTotal.toFixed(2)}`);
|
|
329
|
+
});
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Complete Cart Item Display Example
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
import type { CartItem } from 'omni-sync-sdk';
|
|
336
|
+
import { formatPrice } from 'omni-sync-sdk';
|
|
337
|
+
|
|
338
|
+
function CartItemRow({ item }: { item: CartItem }) {
|
|
339
|
+
// Access nested product data
|
|
340
|
+
const productName = item.product.name;
|
|
341
|
+
const productSku = item.product.sku;
|
|
342
|
+
const productImage = item.product.images?.[0]?.url;
|
|
343
|
+
|
|
344
|
+
// Access nested variant data (if exists)
|
|
345
|
+
const variantName = item.variant?.name;
|
|
346
|
+
const displayName = variantName ? `${productName} - ${variantName}` : productName;
|
|
347
|
+
|
|
348
|
+
// Format price using helper
|
|
349
|
+
const unitPrice = formatPrice(item.unitPrice);
|
|
350
|
+
const lineTotal = formatPrice(item.unitPrice, { asNumber: true }) * item.quantity;
|
|
351
|
+
|
|
352
|
+
return (
|
|
353
|
+
<div className="flex items-center gap-4">
|
|
354
|
+
<img src={productImage} alt={displayName} className="w-16 h-16 object-cover" />
|
|
355
|
+
<div className="flex-1">
|
|
356
|
+
<h3 className="font-medium">{displayName}</h3>
|
|
357
|
+
<p className="text-sm text-gray-500">SKU: {item.variant?.sku || productSku}</p>
|
|
358
|
+
</div>
|
|
359
|
+
<span className="text-gray-600">Qty: {item.quantity}</span>
|
|
360
|
+
<span className="font-medium">${lineTotal.toFixed(2)}</span>
|
|
361
|
+
</div>
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
263
368
|
## API Reference
|
|
264
369
|
|
|
265
370
|
### Products
|
|
@@ -473,19 +578,36 @@ function ProductPrice({ product }: { product: Product }) {
|
|
|
473
578
|
|
|
474
579
|
#### Rendering Product Descriptions
|
|
475
580
|
|
|
476
|
-
**
|
|
581
|
+
> **CRITICAL**: Product descriptions from Shopify/WooCommerce contain HTML tags. If you render them as plain text, users will see raw `<p>`, `<ul>`, `<li>` tags instead of formatted content!
|
|
582
|
+
|
|
583
|
+
Use the SDK helper functions to handle this automatically:
|
|
477
584
|
|
|
478
585
|
```tsx
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
586
|
+
import { isHtmlDescription, getDescriptionContent } from 'omni-sync-sdk';
|
|
587
|
+
|
|
588
|
+
// Option 1: Using isHtmlDescription helper (recommended)
|
|
589
|
+
function ProductDescription({ product }: { product: Product }) {
|
|
590
|
+
if (!product.description) return null;
|
|
591
|
+
|
|
592
|
+
if (isHtmlDescription(product)) {
|
|
593
|
+
// HTML from Shopify/WooCommerce - MUST use dangerouslySetInnerHTML
|
|
594
|
+
return <div dangerouslySetInnerHTML={{ __html: product.description }} />;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Plain text - render normally
|
|
598
|
+
return <p>{product.description}</p>;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Option 2: Using getDescriptionContent helper
|
|
602
|
+
function ProductDescription({ product }: { product: Product }) {
|
|
603
|
+
const content = getDescriptionContent(product);
|
|
604
|
+
if (!content) return null;
|
|
605
|
+
|
|
606
|
+
if ('html' in content) {
|
|
607
|
+
return <div dangerouslySetInnerHTML={{ __html: content.html }} />;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return <p>{content.text}</p>;
|
|
489
611
|
}
|
|
490
612
|
```
|
|
491
613
|
|
|
@@ -496,6 +618,13 @@ function ProductPrice({ product }: { product: Product }) {
|
|
|
496
618
|
| TikTok | `'text'` | Render as plain text |
|
|
497
619
|
| Manual entry | `'text'` | Render as plain text |
|
|
498
620
|
|
|
621
|
+
**Common Mistake** - DO NOT do this:
|
|
622
|
+
|
|
623
|
+
```tsx
|
|
624
|
+
// WRONG - HTML will show as raw tags like <p>Hello</p>
|
|
625
|
+
<p>{product.description}</p>
|
|
626
|
+
```
|
|
627
|
+
|
|
499
628
|
---
|
|
500
629
|
|
|
501
630
|
### Local Cart (Guest Users) - RECOMMENDED
|
|
@@ -988,6 +1117,98 @@ interface ShippingRate {
|
|
|
988
1117
|
}
|
|
989
1118
|
```
|
|
990
1119
|
|
|
1120
|
+
#### Shipping Rates: Complete Flow
|
|
1121
|
+
|
|
1122
|
+
The shipping flow involves setting an address and then selecting from available rates:
|
|
1123
|
+
|
|
1124
|
+
```typescript
|
|
1125
|
+
// Step 1: Set shipping address - this returns available rates
|
|
1126
|
+
const { checkout, rates } = await omni.setShippingAddress(checkoutId, {
|
|
1127
|
+
firstName: 'John',
|
|
1128
|
+
lastName: 'Doe',
|
|
1129
|
+
line1: '123 Main St',
|
|
1130
|
+
city: 'New York',
|
|
1131
|
+
region: 'NY',
|
|
1132
|
+
postalCode: '10001',
|
|
1133
|
+
country: 'US',
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
// Step 2: Handle empty rates (edge case)
|
|
1137
|
+
if (rates.length === 0) {
|
|
1138
|
+
// No shipping options available for this address
|
|
1139
|
+
// This can happen when:
|
|
1140
|
+
// - Store doesn't ship to this address/country
|
|
1141
|
+
// - All shipping methods have restrictions that exclude this address
|
|
1142
|
+
// - Shipping rates haven't been configured in the store
|
|
1143
|
+
|
|
1144
|
+
return (
|
|
1145
|
+
<div className="bg-yellow-50 p-4 rounded">
|
|
1146
|
+
<p className="font-medium">No shipping options available</p>
|
|
1147
|
+
<p className="text-sm text-gray-600">
|
|
1148
|
+
We currently cannot ship to this address. Please try a different address or contact us for
|
|
1149
|
+
assistance.
|
|
1150
|
+
</p>
|
|
1151
|
+
</div>
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// Step 3: Display available rates to customer
|
|
1156
|
+
<div className="space-y-2">
|
|
1157
|
+
<h3 className="font-medium">Select Shipping Method</h3>
|
|
1158
|
+
{rates.map((rate) => (
|
|
1159
|
+
<label key={rate.id} className="flex items-center gap-3 p-3 border rounded cursor-pointer">
|
|
1160
|
+
<input
|
|
1161
|
+
type="radio"
|
|
1162
|
+
name="shipping"
|
|
1163
|
+
value={rate.id}
|
|
1164
|
+
checked={selectedRateId === rate.id}
|
|
1165
|
+
onChange={() => setSelectedRateId(rate.id)}
|
|
1166
|
+
/>
|
|
1167
|
+
<div className="flex-1">
|
|
1168
|
+
<span className="font-medium">{rate.name}</span>
|
|
1169
|
+
{rate.description && <p className="text-sm text-gray-500">{rate.description}</p>}
|
|
1170
|
+
{rate.estimatedDays && (
|
|
1171
|
+
<p className="text-sm text-gray-500">Estimated delivery: {rate.estimatedDays} business days</p>
|
|
1172
|
+
)}
|
|
1173
|
+
</div>
|
|
1174
|
+
<span className="font-medium">${parseFloat(rate.price).toFixed(2)}</span>
|
|
1175
|
+
</label>
|
|
1176
|
+
))}
|
|
1177
|
+
</div>;
|
|
1178
|
+
|
|
1179
|
+
// Step 4: Select the shipping method
|
|
1180
|
+
await omni.selectShippingMethod(checkoutId, selectedRateId);
|
|
1181
|
+
```
|
|
1182
|
+
|
|
1183
|
+
**Handling Empty Shipping Rates:**
|
|
1184
|
+
|
|
1185
|
+
When no shipping rates are available, you have several options:
|
|
1186
|
+
|
|
1187
|
+
```typescript
|
|
1188
|
+
// Option 1: Show helpful message
|
|
1189
|
+
if (rates.length === 0) {
|
|
1190
|
+
return <NoShippingAvailable address={shippingAddress} />;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
// Option 2: Allow customer to contact store
|
|
1194
|
+
if (rates.length === 0) {
|
|
1195
|
+
return (
|
|
1196
|
+
<div>
|
|
1197
|
+
<p>Shipping not available to your location.</p>
|
|
1198
|
+
<a href="/contact">Request a shipping quote</a>
|
|
1199
|
+
</div>
|
|
1200
|
+
);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// Option 3: Validate before proceeding
|
|
1204
|
+
function canProceedToPayment(checkout: Checkout, rates: ShippingRate[]): boolean {
|
|
1205
|
+
if (rates.length === 0) return false;
|
|
1206
|
+
if (!checkout.shippingRateId) return false;
|
|
1207
|
+
if (!checkout.email) return false;
|
|
1208
|
+
return true;
|
|
1209
|
+
}
|
|
1210
|
+
```
|
|
1211
|
+
|
|
991
1212
|
---
|
|
992
1213
|
|
|
993
1214
|
### Customer Authentication
|
|
@@ -1726,6 +1947,7 @@ export default function ProductsPage() {
|
|
|
1726
1947
|
'use client';
|
|
1727
1948
|
import { useEffect, useState } from 'react';
|
|
1728
1949
|
import { omni } from '@/lib/omni-sync';
|
|
1950
|
+
import { isHtmlDescription } from 'omni-sync-sdk';
|
|
1729
1951
|
import type { Product } from 'omni-sync-sdk';
|
|
1730
1952
|
|
|
1731
1953
|
export default function ProductPage({ params }: { params: { id: string } }) {
|
|
@@ -1791,9 +2013,9 @@ export default function ProductPage({ params }: { params: { id: string } }) {
|
|
|
1791
2013
|
${product.salePrice || product.basePrice}
|
|
1792
2014
|
</p>
|
|
1793
2015
|
|
|
1794
|
-
{/*
|
|
2016
|
+
{/* IMPORTANT: Use isHtmlDescription() to render HTML descriptions correctly */}
|
|
1795
2017
|
{product.description && (
|
|
1796
|
-
product
|
|
2018
|
+
isHtmlDescription(product) ? (
|
|
1797
2019
|
<div className="mt-4 text-gray-600" dangerouslySetInnerHTML={{ __html: product.description }} />
|
|
1798
2020
|
) : (
|
|
1799
2021
|
<p className="mt-4 text-gray-600">{product.description}</p>
|
|
@@ -3010,7 +3232,7 @@ const handlePlaceOrder = () => {
|
|
|
3010
3232
|
- **Use toast notifications (Sonner) for user feedback on actions**
|
|
3011
3233
|
- Persist cart ID in localStorage
|
|
3012
3234
|
- Persist customer token after login
|
|
3013
|
-
- **
|
|
3235
|
+
- **Use `isHtmlDescription(product)` helper and render HTML with `dangerouslySetInnerHTML` when it returns true**
|
|
3014
3236
|
- **Wrap SDK calls in try/catch and show error toasts**
|
|
3015
3237
|
|
|
3016
3238
|
### DON'T:
|
|
@@ -3020,7 +3242,7 @@ const handlePlaceOrder = () => {
|
|
|
3020
3242
|
- Skip implementing required pages
|
|
3021
3243
|
- Write `const products = [...]` - use the API!
|
|
3022
3244
|
- Use `@apply group` in CSS - Tailwind doesn't allow 'group' in @apply. Use `className="group"` on the element instead
|
|
3023
|
-
- **Render `product.description` as plain text without
|
|
3245
|
+
- **Render `product.description` as plain text without using `isHtmlDescription()` - HTML will show as raw tags like `<p>`, `<ul>`, `<li>`!**
|
|
3024
3246
|
|
|
3025
3247
|
---
|
|
3026
3248
|
|