omni-sync-sdk
v0.13.0
Published
Official SDK for building e-commerce storefronts with OmniSync Platform. Perfect for vibe-coded sites, AI-built stores (Cursor, Lovable, v0), and custom storefronts.
Maintainers
Readme
omni-sync-sdk
Official SDK for building e-commerce storefronts with OmniSync Platform.
This SDK provides a complete solution for vibe-coded sites, AI-built stores (Cursor, Lovable, v0), and custom storefronts to connect to OmniSync's unified commerce API.
Installation
npm install omni-sync-sdk
# or
pnpm add omni-sync-sdk
# or
yarn add omni-sync-sdkQuick Start
For Vibe-Coded Sites (Recommended)
import { OmniSyncClient } from 'omni-sync-sdk';
// Initialize with your Connection ID
const omni = new OmniSyncClient({
connectionId: 'vc_YOUR_CONNECTION_ID',
});
// Fetch products
const { data: products } = await omni.getProducts();Checkout: Guest vs Logged-In Customer
IMPORTANT: There are TWO different checkout flows. You MUST use the correct one based on whether the customer is logged in or not.
| Customer Type | Cart Type | Checkout Method | Orders Linked to Account? |
| ------------- | ------------------------- | -------------------- | ------------------------- |
| Guest | Local Cart (localStorage) | submitGuestOrder() | No |
| Logged In | Server Cart | completeCheckout() | Yes |
Decision Flow
// ALWAYS check this at checkout!
if (isLoggedIn()) {
// ✅ Logged-in customer → Server Cart + Checkout flow
// Orders will be linked to their account
const order = await completeServerCheckout();
} else {
// ✅ Guest → Local Cart + submitGuestOrder
// Orders are standalone (not linked to any account)
const order = await omni.submitGuestOrder();
}Guest Checkout (for visitors without account)
// Cart stored locally - NO API calls until checkout!
// Add to local cart (stored in localStorage)
omni.addToLocalCart({
productId: products[0].id,
quantity: 1,
name: products[0].name,
price: String(products[0].basePrice),
});
// Set customer info
omni.setLocalCartCustomer({ email: '[email protected]' });
omni.setLocalCartShippingAddress({
firstName: 'John',
lastName: 'Doe',
line1: '123 Main St',
city: 'New York',
postalCode: '10001',
country: 'US',
});
// Submit order (single API call!)
const order = await omni.submitGuestOrder();
console.log('Order created:', order.orderId);Logged-In Customer Checkout (orders linked to account)
// 1. Make sure customer token is set (after login)
omni.setCustomerToken(authResponse.token);
// 2. Create server cart (auto-linked to customer!)
const cart = await omni.createCart();
localStorage.setItem('cartId', cart.id);
// 3. Add items to server cart
await omni.addToCart(cart.id, {
productId: products[0].id,
quantity: 1,
});
// 4. Create checkout from cart
const checkout = await omni.createCheckout({ cartId: cart.id });
// 5. Set customer info (REQUIRED - email is needed for order!)
await omni.setCheckoutCustomer(checkout.id, {
email: '[email protected]',
firstName: 'John',
lastName: 'Doe',
});
// 6. Set shipping address
await omni.setShippingAddress(checkout.id, {
firstName: 'John',
lastName: 'Doe',
line1: '123 Main St',
city: 'New York',
postalCode: '10001',
country: 'US',
});
// 7. Get shipping rates and select one
const rates = await omni.getShippingRates(checkout.id);
await omni.selectShippingMethod(checkout.id, rates[0].id);
// 8. Complete checkout - order is linked to customer!
const { orderId } = await omni.completeCheckout(checkout.id);
console.log('Order created:', orderId);
// Customer can now see this order in omni.getMyOrders()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.
Two Ways to Handle Cart
Option 1: Local Cart (Guest Users)
For guest users, the cart is stored in localStorage - exactly like Amazon, Shopify, and other major platforms do. This means:
- ✅ No API calls when browsing/adding to cart
- ✅ Cart persists across page refreshes
- ✅ Single API call at checkout
- ✅ No server load for window shoppers
// Add product to local cart
omni.addToLocalCart({ productId: 'prod_123', quantity: 2 });
// View cart
const cart = omni.getLocalCart();
console.log('Items:', cart.items.length);
// Update quantity
omni.updateLocalCartItem('prod_123', 5);
// Remove item
omni.removeFromLocalCart('prod_123');
// At checkout - submit everything in ONE API call
const order = await omni.submitGuestOrder();Option 2: Server Cart (Logged-In Customers)
For logged-in customers, you MUST use server-side cart to link orders to their account:
- ✅ Cart syncs across devices
- ✅ Abandoned cart recovery
- ✅ Orders linked to customer account
- ✅ Customer can see orders in "My Orders"
// 1. Set customer token (after login)
omni.setCustomerToken(token);
// 2. Create cart (auto-linked to customer)
const cart = await omni.createCart();
localStorage.setItem('cartId', cart.id);
// 3. Add items
await omni.addToCart(cart.id, { productId: 'prod_123', quantity: 2 });
// 4. At checkout - create checkout and complete
const checkout = await omni.createCheckout({ cartId: cart.id });
// ... set shipping address, select shipping method ...
const { orderId } = await omni.completeCheckout(checkout.id);⚠️ CRITICAL: If you use
submitGuestOrder()for a logged-in customer, their order will NOT be linked to their account!
Complete Store Setup
Step 1: Create the OmniSync Client
Create a file lib/omni-sync.ts:
import { OmniSyncClient } from 'omni-sync-sdk';
export const omni = new OmniSyncClient({
connectionId: 'vc_YOUR_CONNECTION_ID', // Your Connection ID from OmniSync
});
// ----- Guest Cart Helpers (localStorage) -----
export function getCartItemCount(): number {
return omni.getLocalCartItemCount();
}
export function getCart() {
return omni.getLocalCart();
}
// ----- For Registered Users (server cart) -----
export function getServerCartId(): string | null {
if (typeof window === 'undefined') return null;
return localStorage.getItem('cartId');
}
export function setServerCartId(id: string): void {
localStorage.setItem('cartId', id);
}
export function clearServerCartId(): void {
localStorage.removeItem('cartId');
}
// ----- Customer Token Helpers -----
export function setCustomerToken(token: string | null): void {
if (token) {
localStorage.setItem('customerToken', token);
omni.setCustomerToken(token);
} else {
localStorage.removeItem('customerToken');
omni.clearCustomerToken();
}
}
export function restoreCustomerToken(): string | null {
const token = localStorage.getItem('customerToken');
if (token) omni.setCustomerToken(token);
return token;
}
export function isLoggedIn(): boolean {
return !!localStorage.getItem('customerToken');
}API Reference
Products
Get Products (with pagination)
import { omni } from '@/lib/omni-sync';
import type { Product, PaginatedResponse } from 'omni-sync-sdk';
const response: PaginatedResponse<Product> = await omni.getProducts({
page: 1,
limit: 12,
search: 'shirt', // Optional: search by name
status: 'active', // Optional: 'active' | 'draft' | 'archived'
type: 'SIMPLE', // Optional: 'SIMPLE' | 'VARIABLE'
sortBy: 'createdAt', // Optional: 'name' | 'createdAt' | 'updatedAt' | 'basePrice'
sortOrder: 'desc', // Optional: 'asc' | 'desc'
});
console.log(response.items); // Product[]
console.log(response.total); // Total number of products
console.log(response.totalPages); // Total pagesGet Single Product
const product: Product = await omni.getProduct('product_id');
console.log(product.name);
console.log(product.basePrice);
console.log(product.salePrice); // null if no sale
console.log(product.images); // ProductImage[]
console.log(product.variants); // ProductVariant[] (for VARIABLE products)
console.log(product.inventory); // { total, reserved, available }Search Suggestions (Autocomplete)
Get search suggestions for building autocomplete/search-as-you-type UI:
import type { SearchSuggestions } from 'omni-sync-sdk';
// Basic autocomplete
const suggestions: SearchSuggestions = await omni.getSearchSuggestions('shirt');
console.log(suggestions.products);
// [{ id, name, image, basePrice, salePrice, type }]
console.log(suggestions.categories);
// [{ id, name, productCount }]
// With custom limit (default: 5, max: 10)
const suggestions = await omni.getSearchSuggestions('dress', 3);Search covers: name, sku, description, categories, tags, and brands.
Example: Search Input with Suggestions
function SearchInput() {
const [query, setQuery] = useState('');
const [suggestions, setSuggestions] = useState<SearchSuggestions | null>(null);
// Debounce search requests
useEffect(() => {
if (query.length < 2) {
setSuggestions(null);
return;
}
const timer = setTimeout(async () => {
const results = await omni.getSearchSuggestions(query, 5);
setSuggestions(results);
}, 300);
return () => clearTimeout(timer);
}, [query]);
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search products..."
/>
{suggestions && (
<div className="suggestions">
{suggestions.products.map((product) => (
<a key={product.id} href={`/products/${product.id}`}>
<img src={product.image || '/placeholder.png'} alt={product.name} />
<span>{product.name}</span>
<span>${product.basePrice}</span>
</a>
))}
{suggestions.categories.map((category) => (
<a key={category.id} href={`/category/${category.id}`}>
{category.name} ({category.productCount} products)
</a>
))}
</div>
)}
</div>
);
}Product Type Definition
interface Product {
id: string;
name: string;
description?: string | null;
descriptionFormat?: 'text' | 'html' | 'markdown'; // Format of description content
sku: string;
basePrice: number;
salePrice?: number | null;
status: 'active' | 'draft' | 'archived';
type: 'SIMPLE' | 'VARIABLE';
images?: ProductImage[];
inventory?: InventoryInfo | null;
variants?: ProductVariant[];
categories?: string[];
tags?: string[];
createdAt: string;
updatedAt: string;
}
interface ProductImage {
url: string;
position?: number;
isMain?: boolean;
}
interface ProductVariant {
id: string;
sku?: string | null;
name?: string | null;
price?: number | null;
salePrice?: number | null;
attributes?: Record<string, string>;
inventory?: InventoryInfo | null;
}
interface InventoryInfo {
total: number;
reserved: number;
available: number;
}Displaying Price Range for Variable Products
For products with type: 'VARIABLE' and multiple variants with different prices, display a price range instead of a single price:
// Helper function to get price range from variants
function getPriceRange(product: Product): { min: number; max: number } | null {
if (product.type !== 'VARIABLE' || !product.variants?.length) {
return null;
}
const prices = product.variants
.map(v => v.price ?? product.basePrice)
.filter((p): p is number => p !== null);
if (prices.length === 0) return null;
const min = Math.min(...prices);
const max = Math.max(...prices);
// Return null if all variants have the same price
return min !== max ? { min, max } : null;
}
// Usage in component
function ProductPrice({ product }: { product: Product }) {
const priceRange = getPriceRange(product);
if (priceRange) {
// Variable product with different variant prices - show range
return <span>${priceRange.min} - ${priceRange.max}</span>;
}
// Simple product or all variants same price - show single price
return product.salePrice ? (
<>
<span className="text-red-600">${product.salePrice}</span>
<span className="line-through text-gray-400 ml-2">${product.basePrice}</span>
</>
) : (
<span>${product.basePrice}</span>
);
}When to show price range:
- Product
typeis'VARIABLE' - Has 2+ variants with different prices
- Example: T-shirt sizes S/M/L at $29, XL/XXL at $34 → Display "$29 - $34"
When to show single price:
- Product
typeis'SIMPLE' - Variable product where all variants have the same price
Rendering Product Descriptions
IMPORTANT: Product descriptions may contain HTML (from Shopify/WooCommerce) or plain text. Always check descriptionFormat before rendering:
// Correct way to render product descriptions
{
product.description &&
(product.descriptionFormat === 'html' ? (
// HTML content from Shopify/WooCommerce - render as HTML
<div dangerouslySetInnerHTML={{ __html: product.description }} />
) : (
// Plain text - render normally
<p>{product.description}</p>
));
}| Source Platform | descriptionFormat | Rendering |
| --------------- | ----------------- | ----------------------------- |
| Shopify | 'html' | Use dangerouslySetInnerHTML |
| WooCommerce | 'html' | Use dangerouslySetInnerHTML |
| TikTok | 'text' | Render as plain text |
| Manual entry | 'text' | Render as plain text |
Local Cart (Guest Users) - RECOMMENDED
The local cart stores everything in localStorage until checkout. This is the recommended approach for most storefronts.
Add to Local Cart
// Add item with product info (for display)
omni.addToLocalCart({
productId: 'prod_123',
variantId: 'var_456', // Optional: for products with variants
quantity: 2,
name: 'Cool T-Shirt', // Optional: for cart display
price: '29.99', // Optional: for cart display
image: 'https://...', // Optional: for cart display
});Get Local Cart
const cart = omni.getLocalCart();
console.log(cart.items); // Array of cart items
console.log(cart.customer); // Customer info (if set)
console.log(cart.shippingAddress); // Shipping address (if set)
console.log(cart.couponCode); // Applied coupon (if any)Update Item Quantity
// Set quantity to 5
omni.updateLocalCartItem('prod_123', 5);
// For variant products
omni.updateLocalCartItem('prod_123', 3, 'var_456');
// Set to 0 to remove
omni.updateLocalCartItem('prod_123', 0);Remove Item
omni.removeFromLocalCart('prod_123');
omni.removeFromLocalCart('prod_123', 'var_456'); // With variantClear Cart
omni.clearLocalCart();Set Customer Info
omni.setLocalCartCustomer({
email: '[email protected]', // Required
firstName: 'John', // Optional
lastName: 'Doe', // Optional
phone: '+1234567890', // Optional
});Set Shipping Address
omni.setLocalCartShippingAddress({
firstName: 'John',
lastName: 'Doe',
line1: '123 Main St',
line2: 'Apt 4B', // Optional
city: 'New York',
region: 'NY', // Optional: State/Province
postalCode: '10001',
country: 'US',
phone: '+1234567890', // Optional
});Set Billing Address (Optional)
omni.setLocalCartBillingAddress({
firstName: 'John',
lastName: 'Doe',
line1: '456 Business Ave',
city: 'New York',
postalCode: '10002',
country: 'US',
});Apply Coupon
omni.setLocalCartCoupon('SAVE20');
// Remove coupon
omni.setLocalCartCoupon(undefined);Get Cart Item Count
const count = omni.getLocalCartItemCount();
console.log(`${count} items in cart`);Local Cart Type Definition
interface LocalCart {
items: LocalCartItem[];
couponCode?: string;
customer?: {
email: string;
firstName?: string;
lastName?: string;
phone?: string;
};
shippingAddress?: {
firstName: string;
lastName: string;
line1: string;
line2?: string;
city: string;
region?: string;
postalCode: string;
country: string;
phone?: string;
};
billingAddress?: {
/* same as shipping */
};
notes?: string;
updatedAt: string;
}
interface LocalCartItem {
productId: string;
variantId?: string;
quantity: number;
name?: string;
sku?: string;
price?: string;
image?: string;
addedAt: string;
}Guest Checkout (Submit Order)
Submit the local cart as an order with a single API call:
// Make sure cart has items, customer email, and shipping address
const order = await omni.submitGuestOrder();
console.log(order.orderId); // 'order_abc123...'
console.log(order.orderNumber); // 'ORD-12345'
console.log(order.status); // 'pending'
console.log(order.total); // 59.98
console.log(order.message); // 'Order created successfully'
// Cart is automatically cleared after successful order🔄 Automatic Tracking: If "Track Guest Checkouts" is enabled in your connection settings (OmniSync Admin),
submitGuestOrder()will automatically create a tracked checkout session before placing the order. This allows you to see abandoned carts and checkout sessions in your admin dashboard - no code changes needed!
Keep Cart After Order
// If you want to keep the cart data (e.g., for order review page)
const order = await omni.submitGuestOrder({ clearCartOnSuccess: false });Create Order with Custom Data
If you manage cart state yourself instead of using local cart:
const order = await omni.createGuestOrder({
items: [
{ productId: 'prod_123', quantity: 2 },
{ productId: 'prod_456', variantId: 'var_789', quantity: 1 },
],
customer: {
email: '[email protected]',
firstName: 'John',
lastName: 'Doe',
},
shippingAddress: {
firstName: 'John',
lastName: 'Doe',
line1: '123 Main St',
city: 'New York',
postalCode: '10001',
country: 'US',
},
couponCode: 'SAVE20', // Optional
notes: 'Please gift wrap', // Optional
});Guest Order Response Type
interface GuestOrderResponse {
orderId: string;
orderNumber: string;
status: string;
total: number;
message: string;
}Tracked Guest Checkout (Automatic)
Note: As of SDK v0.7.1,
submitGuestOrder()automatically handles tracking. You don't need to use these methods unless you want explicit control over the checkout flow.
When "Track Guest Checkouts" is enabled in your connection settings, checkout sessions are automatically created on the server, allowing:
- Visibility of checkout sessions in admin dashboard
- Abandoned cart tracking
- Future: abandoned cart recovery emails
How to Enable
- Go to OmniSync Admin → Integrations → Vibe-Coded Sites
- Click on your connection → Settings
- Enable "Track Guest Checkouts"
- Save - that's it! No code changes needed.
Advanced: Manual Tracking Control
If you need explicit control over the tracking flow (e.g., to track checkout steps before the user places an order):
// 1. Start tracked checkout (sends cart items to server)
const checkout = await omni.startGuestCheckout();
if (checkout.tracked) {
// 2. Update with shipping address
await omni.updateGuestCheckoutAddress(checkout.checkoutId, {
shippingAddress: {
firstName: 'John',
lastName: 'Doe',
line1: '123 Main St',
city: 'New York',
postalCode: '10001',
country: 'US',
},
});
// 3. Complete the checkout
const order = await omni.completeGuestCheckout(checkout.checkoutId);
console.log('Order created:', order.orderId);
} else {
// Fallback to regular guest checkout
const order = await omni.submitGuestOrder();
}Response Types
type GuestCheckoutStartResponse =
| {
tracked: true;
checkoutId: string;
cartId: string;
message: string;
}
| {
tracked: false;
message: string;
};Server Cart (Registered Users)
For logged-in customers who want cart sync across devices.
Create Cart
const cart = await omni.createCart();
setServerCartId(cart.id); // Save to localStorageGet Cart
const cartId = getCartId();
if (cartId) {
const cart = await omni.getCart(cartId);
console.log(cart.items); // CartItem[]
console.log(cart.itemCount); // Total items
console.log(cart.subtotal); // Subtotal amount
}Add to Cart
const cart = await omni.addToCart(cartId, {
productId: 'product_id',
variantId: 'variant_id', // Optional: for VARIABLE products
quantity: 2,
notes: 'Gift wrap please', // Optional
});Update Cart Item
const cart = await omni.updateCartItem(cartId, itemId, {
quantity: 3,
});Remove Cart Item
const cart = await omni.removeCartItem(cartId, itemId);Apply Coupon
const cart = await omni.applyCoupon(cartId, 'SAVE20');
console.log(cart.discountAmount); // Discount applied
console.log(cart.couponCode); // 'SAVE20'Remove Coupon
const cart = await omni.removeCoupon(cartId);Cart Type Definition
interface Cart {
id: string;
sessionToken?: string | null;
customerId?: string | null;
status: 'ACTIVE' | 'MERGED' | 'CONVERTED' | 'ABANDONED';
currency: string;
subtotal: string;
discountAmount: string;
couponCode?: string | null;
items: CartItem[];
itemCount: number;
createdAt: string;
updatedAt: string;
}
interface CartItem {
id: string;
productId: string;
variantId?: string | null;
quantity: number;
unitPrice: string;
discountAmount: string;
notes?: string | null;
product: {
id: string;
name: string;
sku: string;
images?: unknown[];
};
variant?: {
id: string;
name?: string | null;
sku?: string | null;
} | null;
}Checkout
Create Checkout from Cart
const checkout = await omni.createCheckout({
cartId: cartId,
});Set Customer Information
const checkout = await omni.setCheckoutCustomer(checkoutId, {
email: '[email protected]',
firstName: 'John',
lastName: 'Doe',
phone: '+1234567890', // Optional
});Set Shipping Address
const { checkout, rates } = await omni.setShippingAddress(checkoutId, {
firstName: 'John',
lastName: 'Doe',
line1: '123 Main St',
line2: 'Apt 4B', // Optional
city: 'New York',
region: 'NY', // State/Province
postalCode: '10001',
country: 'US',
phone: '+1234567890', // Optional
});
// rates contains available shipping options
console.log(rates); // ShippingRate[]Select Shipping Method
const checkout = await omni.selectShippingMethod(checkoutId, rates[0].id);Set Billing Address
// Same as shipping
const checkout = await omni.setBillingAddress(checkoutId, {
...shippingAddress,
sameAsShipping: true, // Optional shortcut
});Complete Checkout
const { orderId } = await omni.completeCheckout(checkoutId);
clearCartId(); // Clear cart from localStorage
console.log('Order created:', orderId);Checkout Type Definition
interface Checkout {
id: string;
status: CheckoutStatus;
email?: string | null;
shippingAddress?: CheckoutAddress | null;
billingAddress?: CheckoutAddress | null;
shippingMethod?: ShippingRate | null;
currency: string;
subtotal: string;
discountAmount: string;
shippingAmount: string;
taxAmount: string;
total: string;
couponCode?: string | null;
items: CheckoutLineItem[];
itemCount: number;
availableShippingRates?: ShippingRate[];
}
type CheckoutStatus = 'PENDING' | 'SHIPPING_SET' | 'PAYMENT_PENDING' | 'COMPLETED' | 'FAILED';
interface ShippingRate {
id: string;
name: string;
description?: string | null;
price: string;
currency: string;
estimatedDays?: number | null;
}Customer Authentication
Register Customer
const auth = await omni.registerCustomer({
email: '[email protected]',
password: 'securepassword123',
firstName: 'John',
lastName: 'Doe',
});
// Check if email verification is required
if (auth.requiresVerification) {
localStorage.setItem('verificationToken', auth.token);
window.location.href = '/verify-email';
} else {
setCustomerToken(auth.token);
// Redirect back to store, not /account
window.location.href = '/';
}Login Customer
const auth = await omni.loginCustomer('[email protected]', 'password123');
setCustomerToken(auth.token);
// Best practice: redirect back to previous page or home
const returnUrl = localStorage.getItem('returnUrl') || '/';
localStorage.removeItem('returnUrl');
window.location.href = returnUrl;Best Practice: Before showing login page, save the current URL with
localStorage.setItem('returnUrl', window.location.pathname). After login, redirect back to that URL. This is how Amazon, Shopify, and most e-commerce sites work.
Logout Customer
setCustomerToken(null);
window.location.href = '/'; // Return to store homeGet Customer Profile
restoreCustomerToken(); // Restore from localStorage
const profile = await omni.getMyProfile();
console.log(profile.firstName);
console.log(profile.email);
console.log(profile.addresses);Get Customer Orders
const { data: orders, meta } = await omni.getMyOrders({
page: 1,
limit: 10,
});Auth Response Type
interface CustomerAuthResponse {
customer: {
id: string;
email: string;
firstName?: string;
lastName?: string;
emailVerified: boolean;
};
token: string;
expiresAt: string;
requiresVerification?: boolean; // true if email verification is required
}Email Verification
If the store has email verification enabled, customers must verify their email after registration before they can fully use their account.
Registration with Email Verification
When requiresVerification is true in the registration response, the customer needs to verify their email:
const auth = await omni.registerCustomer({
email: '[email protected]',
password: 'securepassword123',
firstName: 'John',
});
if (auth.requiresVerification) {
// Save token for verification step
localStorage.setItem('verificationToken', auth.token);
// Redirect to verification page
window.location.href = '/verify-email';
} else {
// No verification needed - redirect back to store
setCustomerToken(auth.token);
window.location.href = '/';
}Verify Email with Code
After the customer receives the 6-digit code via email:
// Get the token saved from registration
const token = localStorage.getItem('verificationToken');
// Verify email - pass the token directly (no need to call setCustomerToken first!)
const result = await omni.verifyEmail(code, token);
if (result.verified) {
// Email verified! Now set the token for normal use
setCustomerToken(token);
localStorage.removeItem('verificationToken');
// Redirect back to store (or returnUrl if saved)
const returnUrl = localStorage.getItem('returnUrl') || '/';
localStorage.removeItem('returnUrl');
window.location.href = returnUrl;
}Resend Verification Email
If the customer didn't receive the email or the code expired:
const token = localStorage.getItem('verificationToken');
await omni.resendVerificationEmail(token);
// Show success message - new code sentNote: Resend is rate-limited to 3 requests per hour.
Complete Email Verification Page Example
'use client';
import { useState } from 'react';
import { omni, setCustomerToken } from '@/lib/omni-sync';
import { toast } from 'sonner';
export default function VerifyEmailPage() {
const [code, setCode] = useState('');
const [loading, setLoading] = useState(false);
const handleVerify = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
try {
const token = localStorage.getItem('verificationToken');
if (!token) {
toast.error('No verification token found. Please register again.');
return;
}
const result = await omni.verifyEmail(code, token);
if (result.verified) {
toast.success('Email verified!');
setCustomerToken(token);
localStorage.removeItem('verificationToken');
// Redirect back to store
const returnUrl = localStorage.getItem('returnUrl') || '/';
localStorage.removeItem('returnUrl');
window.location.href = returnUrl;
}
} catch (error) {
toast.error(error instanceof Error ? error.message : 'Verification failed');
} finally {
setLoading(false);
}
};
const handleResend = async () => {
try {
const token = localStorage.getItem('verificationToken');
if (!token) {
toast.error('No verification token found');
return;
}
await omni.resendVerificationEmail(token);
toast.success('Verification code sent!');
} catch (error) {
toast.error(error instanceof Error ? error.message : 'Failed to resend');
}
};
return (
<div className="max-w-md mx-auto mt-12">
<h1 className="text-2xl font-bold mb-4">Verify Your Email</h1>
<p className="text-gray-600 mb-6">
We sent a 6-digit code to your email. Enter it below to verify your account.
</p>
<form onSubmit={handleVerify} className="space-y-4">
<input
type="text"
placeholder="Enter 6-digit code"
value={code}
onChange={(e) => setCode(e.target.value)}
maxLength={6}
className="w-full border p-3 rounded text-center text-2xl tracking-widest"
required
/>
<button
type="submit"
disabled={loading || code.length !== 6}
className="w-full bg-black text-white py-3 rounded disabled:opacity-50"
>
{loading ? 'Verifying...' : 'Verify Email'}
</button>
</form>
<button onClick={handleResend} className="mt-4 text-blue-600 text-sm">
Didn't receive the code? Resend
</button>
</div>
);
}Social Login (OAuth)
Allow customers to sign in with Google, Facebook, or GitHub. The store owner configures which providers are available in their OmniSync admin panel.
Check Available Providers
// Returns only the providers the store owner has enabled
const { providers } = await omni.getAvailableOAuthProviders();
// providers = ['GOOGLE', 'FACEBOOK'] - varies by store configurationOAuth Login Flow
Step 1: User clicks "Sign in with Google"
// Save cart ID before redirect (user will leave your site!)
const cartId = localStorage.getItem('cartId');
if (cartId) sessionStorage.setItem('pendingCartId', cartId);
// Get authorization URL
const { authorizationUrl, state } = await omni.getOAuthAuthorizeUrl('GOOGLE', {
redirectUrl: window.location.origin + '/auth/callback', // Where Google sends them back
});
// Save state for verification (CSRF protection)
sessionStorage.setItem('oauthState', state);
// Redirect to Google
window.location.href = authorizationUrl;Step 2: Create callback page (/auth/callback)
// pages/auth/callback.tsx or app/auth/callback/page.tsx
'use client';
import { useEffect, useState } from 'react';
import { useSearchParams } from 'next/navigation';
import { omni, setCustomerToken } from '@/lib/omni-sync';
export default function AuthCallback() {
const searchParams = useSearchParams();
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function handleCallback() {
const code = searchParams.get('code');
const state = searchParams.get('state');
const errorParam = searchParams.get('error');
// Check for OAuth errors (user cancelled, etc.)
if (errorParam) {
window.location.href = '/login?error=cancelled';
return;
}
if (!code || !state) {
setError('Missing OAuth parameters');
return;
}
// Verify state matches (CSRF protection)
const savedState = sessionStorage.getItem('oauthState');
if (state !== savedState) {
setError('Invalid state - please try again');
return;
}
try {
// Exchange code for customer token
const { customer, token, isNewCustomer } = await omni.handleOAuthCallback(
'GOOGLE',
code,
state
);
// Save the customer token
setCustomerToken(token);
sessionStorage.removeItem('oauthState');
// Link any pending cart to the now-logged-in customer
const pendingCartId = sessionStorage.getItem('pendingCartId');
if (pendingCartId) {
try {
await omni.linkCart(pendingCartId);
} catch {
// Cart may have expired - that's ok
}
sessionStorage.removeItem('pendingCartId');
}
// Redirect to return URL or home
const returnUrl = localStorage.getItem('returnUrl') || '/';
localStorage.removeItem('returnUrl');
window.location.href = returnUrl;
} catch (err) {
setError(err instanceof Error ? err.message : 'Login failed');
}
}
handleCallback();
}, [searchParams]);
if (error) {
return (
<div className="max-w-md mx-auto mt-12 text-center">
<h1 className="text-xl font-bold text-red-600">Login Failed</h1>
<p className="mt-2 text-gray-600">{error}</p>
<a href="/login" className="mt-4 inline-block text-blue-600">
Try again
</a>
</div>
);
}
return (
<div className="max-w-md mx-auto mt-12 text-center">
<div className="animate-spin h-8 w-8 border-4 border-blue-600 border-t-transparent rounded-full mx-auto"></div>
<p className="mt-4 text-gray-600">Completing login...</p>
</div>
);
}Login Page with Social Buttons
'use client';
import { useState, useEffect } from 'react';
import { omni, setCustomerToken } from '@/lib/omni-sync';
export default function LoginPage() {
const [providers, setProviders] = useState<string[]>([]);
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
// Load available OAuth providers
useEffect(() => {
omni.getAvailableOAuthProviders()
.then(({ providers }) => setProviders(providers))
.catch(() => {}); // OAuth not configured - that's ok
}, []);
// Social login handler
const handleSocialLogin = async (provider: string) => {
try {
// Save cart ID before redirect
const cartId = localStorage.getItem('cartId');
if (cartId) sessionStorage.setItem('pendingCartId', cartId);
const { authorizationUrl, state } = await omni.getOAuthAuthorizeUrl(
provider as 'GOOGLE' | 'FACEBOOK' | 'GITHUB',
{ redirectUrl: window.location.origin + '/auth/callback' }
);
sessionStorage.setItem('oauthState', state);
window.location.href = authorizationUrl;
} catch (err) {
setError('Failed to start login');
}
};
// Regular email/password login
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError('');
try {
const auth = await omni.loginCustomer(email, password);
setCustomerToken(auth.token);
const returnUrl = localStorage.getItem('returnUrl') || '/';
localStorage.removeItem('returnUrl');
window.location.href = returnUrl;
} catch (err) {
setError('Invalid email or password');
} finally {
setLoading(false);
}
};
return (
<div className="max-w-md mx-auto mt-12">
<h1 className="text-2xl font-bold mb-6">Login</h1>
{/* Social Login Buttons */}
{providers.length > 0 && (
<div className="space-y-3 mb-6">
{providers.includes('GOOGLE') && (
<button
onClick={() => handleSocialLogin('GOOGLE')}
className="w-full flex items-center justify-center gap-2 border py-3 rounded hover:bg-gray-50"
>
<GoogleIcon />
Continue with Google
</button>
)}
{providers.includes('FACEBOOK') && (
<button
onClick={() => handleSocialLogin('FACEBOOK')}
className="w-full flex items-center justify-center gap-2 bg-[#1877F2] text-white py-3 rounded"
>
<FacebookIcon />
Continue with Facebook
</button>
)}
{providers.includes('GITHUB') && (
<button
onClick={() => handleSocialLogin('GITHUB')}
className="w-full flex items-center justify-center gap-2 bg-[#24292F] text-white py-3 rounded"
>
<GithubIcon />
Continue with GitHub
</button>
)}
<div className="relative my-4">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t" />
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-white text-gray-500">or</span>
</div>
</div>
</div>
)}
{/* Email/Password Form */}
{error && <div className="bg-red-100 text-red-600 p-3 rounded mb-4">{error}</div>}
<form onSubmit={handleSubmit} className="space-y-4">
<input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="w-full border p-2 rounded"
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className="w-full border p-2 rounded"
/>
<button
type="submit"
disabled={loading}
className="w-full bg-black text-white py-3 rounded"
>
{loading ? 'Logging in...' : 'Login'}
</button>
</form>
<p className="mt-4 text-center">
Don't have an account? <a href="/register" className="text-blue-600">Register</a>
</p>
</div>
);
}
// Simple SVG icons (or use lucide-react)
const GoogleIcon = () => (
<svg className="w-5 h-5" viewBox="0 0 24 24">
<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"/>
<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"/>
<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"/>
<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"/>
</svg>
);
const FacebookIcon = () => (
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<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"/>
</svg>
);
const GithubIcon = () => (
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<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"/>
</svg>
);Account Linking (Add Social Account to Existing User)
Logged-in customers can link additional social accounts to their profile:
// Get currently linked accounts
const connections = await omni.getOAuthConnections();
// [{ provider: 'GOOGLE', email: '[email protected]', linkedAt: '...' }]
// Link a new provider (redirects to OAuth flow)
const { authorizationUrl } = await omni.linkOAuthProvider('GITHUB');
window.location.href = authorizationUrl;
// Unlink a provider
await omni.unlinkOAuthProvider('GOOGLE');Cart Linking After OAuth Login
When a customer logs in via OAuth, their guest cart should be linked to their account:
// omni.linkCart() associates a guest cart with the logged-in customer
await omni.linkCart(cartId);This is automatically handled in the callback example above.
OAuth Method Reference
| Method | Description |
| -------------------------------------------- | -------------------------------------------- |
| getAvailableOAuthProviders() | Get list of enabled providers for this store |
| getOAuthAuthorizeUrl(provider, options?) | Get URL to redirect user to OAuth provider |
| handleOAuthCallback(provider, code, state) | Exchange OAuth code for customer token |
| linkOAuthProvider(provider) | Link social account to current customer |
| unlinkOAuthProvider(provider) | Remove linked social account |
| getOAuthConnections() | Get list of linked social accounts |
| linkCart(cartId) | Link guest cart to logged-in customer |
Customer Addresses
Get Addresses
const addresses = await omni.getMyAddresses();Add Address
const address = await omni.addMyAddress({
firstName: 'John',
lastName: 'Doe',
line1: '123 Main St',
city: 'New York',
region: 'NY',
postalCode: '10001',
country: 'US',
isDefault: true,
});Update Address
const updated = await omni.updateMyAddress(addressId, {
line1: '456 New Street',
});Delete Address
await omni.deleteMyAddress(addressId);Store Info
const store = await omni.getStoreInfo();
console.log(store.name); // Store name
console.log(store.currency); // 'USD', 'ILS', etc.
console.log(store.language); // 'en', 'he', etc.Complete Page Examples
Home Page
'use client';
import { useEffect, useState } from 'react';
import { omni } from '@/lib/omni-sync';
import type { Product } from 'omni-sync-sdk';
export default function HomePage() {
const [products, setProducts] = useState<Product[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function loadProducts() {
try {
const { data } = await omni.getProducts({ limit: 8 });
setProducts(data);
} catch (err) {
setError('Failed to load products');
} finally {
setLoading(false);
}
}
loadProducts();
}, []);
if (loading) return <div>Loading...</div>;
if (error) return <div>{error}</div>;
return (
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
{products.map((product) => (
<a key={product.id} href={`/products/${product.id}`} className="group">
<img
src={product.images?.[0]?.url || '/placeholder.jpg'}
alt={product.name}
className="w-full aspect-square object-cover"
/>
<h3 className="mt-2 font-medium">{product.name}</h3>
<p className="text-lg">
{product.salePrice ? (
<>
<span className="text-red-600">${product.salePrice}</span>
<span className="line-through text-gray-400 ml-2">${product.basePrice}</span>
</>
) : (
<span>${product.basePrice}</span>
)}
</p>
</a>
))}
</div>
);
}Products List with Pagination
'use client';
import { useEffect, useState } from 'react';
import { omni } from '@/lib/omni-sync';
import type { Product, PaginatedResponse } from 'omni-sync-sdk';
export default function ProductsPage() {
const [data, setData] = useState<PaginatedResponse<Product> | null>(null);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function load() {
setLoading(true);
try {
const result = await omni.getProducts({ page, limit: 12 });
setData(result);
} finally {
setLoading(false);
}
}
load();
}, [page]);
if (loading) return <div>Loading...</div>;
if (!data) return <div>No products found</div>;
return (
<div>
<div className="grid grid-cols-3 gap-6">
{data.data.map((product) => (
<a key={product.id} href={`/products/${product.id}`}>
<img src={product.images?.[0]?.url} alt={product.name} />
<h3>{product.name}</h3>
<p>${product.salePrice || product.basePrice}</p>
</a>
))}
</div>
{/* Pagination */}
<div className="flex gap-2 mt-8">
<button onClick={() => setPage(p => Math.max(1, p - 1))} disabled={page === 1}>
Previous
</button>
<span>Page {data.page} of {data.totalPages}</span>
<button onClick={() => setPage(p => p + 1)} disabled={page >= data.totalPages}>
Next
</button>
</div>
</div>
);
}Product Detail with Add to Cart (Local Cart)
'use client';
import { useEffect, useState } from 'react';
import { omni } from '@/lib/omni-sync';
import type { Product } from 'omni-sync-sdk';
export default function ProductPage({ params }: { params: { id: string } }) {
const [product, setProduct] = useState<Product | null>(null);
const [selectedVariant, setSelectedVariant] = useState<string | null>(null);
const [quantity, setQuantity] = useState(1);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function load() {
try {
const p = await omni.getProduct(params.id);
setProduct(p);
if (p.variants && p.variants.length > 0) {
setSelectedVariant(p.variants[0].id);
}
} finally {
setLoading(false);
}
}
load();
}, [params.id]);
const handleAddToCart = () => {
if (!product) return;
// Get variant if selected
const variant = selectedVariant
? product.variants?.find(v => v.id === selectedVariant)
: null;
// Add to local cart (NO API call!)
omni.addToLocalCart({
productId: product.id,
variantId: selectedVariant || undefined,
quantity,
name: variant?.name || product.name,
price: String(variant?.price || product.salePrice || product.basePrice),
image: product.images?.[0]?.url,
});
alert('Added to cart!');
};
if (loading) return <div>Loading...</div>;
if (!product) return <div>Product not found</div>;
return (
<div className="grid grid-cols-2 gap-8">
{/* Images */}
<div>
<img
src={product.images?.[0]?.url || '/placeholder.jpg'}
alt={product.name}
className="w-full"
/>
</div>
{/* Details */}
<div>
<h1 className="text-3xl font-bold">{product.name}</h1>
<p className="text-2xl mt-4">
${product.salePrice || product.basePrice}
</p>
{/* Render description based on format (HTML from Shopify/WooCommerce, text otherwise) */}
{product.description && (
product.descriptionFormat === 'html' ? (
<div className="mt-4 text-gray-600" dangerouslySetInnerHTML={{ __html: product.description }} />
) : (
<p className="mt-4 text-gray-600">{product.description}</p>
)
)}
{/* Variant Selection */}
{product.variants && product.variants.length > 0 && (
<div className="mt-6">
<label className="block font-medium mb-2">Select Option</label>
<select
value={selectedVariant || ''}
onChange={(e) => setSelectedVariant(e.target.value)}
className="border rounded p-2 w-full"
>
{product.variants.map((v) => (
<option key={v.id} value={v.id}>
{v.name || v.sku} - ${v.price || product.basePrice}
</option>
))}
</select>
</div>
)}
{/* Quantity */}
<div className="mt-4">
<label className="block font-medium mb-2">Quantity</label>
<input
type="number"
min="1"
value={quantity}
onChange={(e) => setQuantity(Number(e.target.value))}
className="border rounded p-2 w-20"
/>
</div>
{/* Add to Cart Button */}
<button
onClick={handleAddToCart}
disabled={adding}
className="mt-6 w-full bg-black text-white py-3 rounded disabled:opacity-50"
>
{adding ? 'Adding...' : 'Add to Cart'}
</button>
{/* Stock Status */}
{product.inventory && (
<p className="mt-4 text-sm">
{product.inventory.available > 0
? `${product.inventory.available} in stock`
: 'Out of stock'}
</p>
)}
</div>
</div>
);
}Cart Page (Local Cart)
'use client';
import { useState } from 'react';
import { omni } from '@/lib/omni-sync';
import type { LocalCart } from 'omni-sync-sdk';
export default function CartPage() {
const [cart, setCart] = useState<LocalCart>(omni.getLocalCart());
const updateQuantity = (productId: string, quantity: number, variantId?: string) => {
const updated = omni.updateLocalCartItem(productId, quantity, variantId);
setCart(updated);
};
const removeItem = (productId: string, variantId?: string) => {
const updated = omni.removeFromLocalCart(productId, variantId);
setCart(updated);
};
// Calculate subtotal from local cart
const subtotal = cart.items.reduce((sum, item) => {
return sum + (parseFloat(item.price || '0') * item.quantity);
}, 0);
if (cart.items.length === 0) {
return (
<div className="text-center py-12">
<h1 className="text-2xl font-bold">Your cart is empty</h1>
<a href="/products" className="text-blue-600 mt-4 inline-block">Continue Shopping</a>
</div>
);
}
return (
<div>
<h1 className="text-2xl font-bold mb-6">Shopping Cart</h1>
{cart.items.map((item) => (
<div key={`${item.productId}-${item.variantId || ''}`} className="flex items-center gap-4 py-4 border-b">
<img
src={item.image || '/placeholder.jpg'}
alt={item.name || 'Product'}
className="w-20 h-20 object-cover"
/>
<div className="flex-1">
<h3 className="font-medium">{item.name || 'Product'}</h3>
<p className="font-bold">${item.price}</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => updateQuantity(item.productId, item.quantity - 1, item.variantId)}
className="w-8 h-8 border rounded"
>-</button>
<span className="w-8 text-center">{item.quantity}</span>
<button
onClick={() => updateQuantity(item.productId, item.quantity + 1, item.variantId)}
className="w-8 h-8 border rounded"
>+</button>
</div>
<button
onClick={() => removeItem(item.productId, item.variantId)}
className="text-red-600"
>Remove</button>
</div>
))}
<div className="mt-6 text-right">
<p className="text-xl">Subtotal: <strong>${subtotal.toFixed(2)}</strong></p>
{cart.couponCode && (
<p className="text-green-600">Coupon applied: {cart.couponCode}</p>
)}
<a
href="/checkout"
className="mt-4 inline-block bg-black text-white px-8 py-3 rounded"
>
Proceed to Checkout
</a>
</div>
</div>
);
}Universal Checkout (Handles Both Guest & Logged-In)
RECOMMENDED: Use this pattern to properly handle both guest and logged-in customers in a single checkout page.
'use client';
import { useState, useEffect } from 'react';
import { omni, isLoggedIn, getServerCartId, setServerCartId, restoreCustomerToken } from '@/lib/omni-sync';
export default function CheckoutPage() {
const [loading, setLoading] = useState(true);
const [submitting, setSubmitting] = useState(false);
const [customerLoggedIn, setCustomerLoggedIn] = useState(false);
// Form state
const [email, setEmail] = useState('');
const [shippingAddress, setShippingAddress] = useState({
firstName: '', lastName: '', line1: '', city: '', postalCode: '', country: 'US'
});
// Server checkout state (for logged-in customers)
const [checkoutId, setCheckoutId] = useState<string | null>(null);
const [shippingRates, setShippingRates] = useState<any[]>([]);
const [selectedRate, setSelectedRate] = useState<string | null>(null);
useEffect(() => {
restoreCustomerToken();
const loggedIn = isLoggedIn();
setCustomerLoggedIn(loggedIn);
async function initCheckout() {
if (loggedIn) {
// Logged-in customer: Create server cart + checkout
let cartId = getServerCartId();
if (!cartId) {
// Create new cart (auto-linked to customer)
const cart = await omni.createCart();
cartId = cart.id;
setServerCartId(cartId);
// Migrate local cart items to server cart
const localCart = omni.getLocalCart();
for (const item of localCart.items) {
await omni.addToCart(cartId, {
productId: item.productId,
variantId: item.variantId,
quantity: item.quantity,
});
}
omni.clearLocalCart(); // Clear local cart after migration
}
// Create checkout from server cart
const checkout = await omni.createCheckout({ cartId });
setCheckoutId(checkout.id);
// Pre-fill from customer profile if available
try {
const profile = await omni.getMyOrders({ limit: 1 }); // Just to check auth works
} catch (e) {
console.log('Could not fetch profile');
}
}
setLoading(false);
}
initCheckout();
}, []);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setSubmitting(true);
try {
if (customerLoggedIn && checkoutId) {
// ===== LOGGED-IN CUSTOMER: Server Checkout =====
// 1. Set customer info (REQUIRED - even for logged-in customers!)
await omni.setCheckoutCustomer(checkoutId, {
email: email, // Get from form or customer profile
firstName: shippingAddress.firstName,
lastName: shippingAddress.lastName,
});
// 2. Set shipping address
await omni.setShippingAddress(checkoutId, shippingAddress);
// 3. Get and select shipping rate
const rates = await omni.getShippingRates(checkoutId);
if (rates.length > 0) {
await omni.selectShippingMethod(checkoutId, selectedRate || rates[0].id);
}
// 4. Complete checkout - ORDER IS LINKED TO CUSTOMER!
const { orderId } = await omni.completeCheckout(checkoutId);
// Clear cart ID
localStorage.removeItem('cartId');
// Redirect to success page
window.location.href = `/order-success?orderId=${orderId}`;
} else {
// ===== GUEST: Local Cart + submitGuestOrder =====
// Set customer and shipping info on local cart
omni.setLocalCartCustomer({ email });
omni.setLocalCartShippingAddress(shippingAddress);
// Submit guest order (single API call)
const order = await omni.submitGuestOrder();
// Redirect to success page
window.location.href = `/order-success?orderId=${order.orderId}`;
}
} catch (error) {
console.error('Checkout failed:', error);
alert('Checkout failed. Please try again.');
} finally {
setSubmitting(false);
}
};
if (loading) return <div>Loading checkout...</div>;
return (
<form onSubmit={handleSubmit}>
{/* Show email field only for guests */}
{!customerLoggedIn && (
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
required
/>
)}
{/* Shipping address fields */}
<input value={shippingAddress.firstName} onChange={(e) => setShippingAddress({...shippingAddress, firstName: e.target.value})} placeholder="First Name" required />
<input value={shippingAddress.lastName} onChange={(e) => setShippingAddress({...shippingAddress, lastName: e.target.value})} placeholder="Last Name" required />
<input value={shippingAddress.line1} onChange={(e) => setShippingAddress({...shippingAddress, line1: e.target.value})} placeholder="Address" required />
<input value={shippingAddress.city} onChange={(e) => setShippingAddress({...shippingAddress, city: e.target.value})} placeholder="City" required />
<input value={shippingAddress.postalCode} onChange={(e) => setShippingAddress({...shippingAddress, postalCode: e.target.value})} placeholder="Postal Code" required />
{/* Shipping rates (for logged-in customers) */}
{customerLoggedIn && shippingRates.length > 0 && (
<select value={selectedRate || ''} onChange={(e) => setSelectedRate(e.target.value)}>
{shippingRates.map((rate) => (
<option key={rate.id} value={rate.id}>
{rate.name} - ${rate.price}
</option>
))}
</select>
)}
<button type="submit" disabled={submitting}>
{submitting ? 'Processing...' : 'Place Order'}
</button>
{customerLoggedIn && (
<p className="text-sm text-green-600">
✓ Logged in - Order will be saved to your account
</p>
)}
</form>
);
}Key Points:
isLoggedIn()determines which flow to use- Logged-in customers use
createCart()→createCheckout()→completeCheckout()- Guests use local cart +
submitGuestOrder()- Local cart items are migrated to server cart when customer logs in
Guest Checkout (Single API Call)
This checkout is for guest users only. All cart data is in localStorage, and we submit it in one API call.
⚠️ WARNING: Do NOT use this for logged-in customers! Use the Universal Checkout pattern above instead.
'use client';
import { useState, useEffect } from 'react';
import { omni } from '@/lib/omni-sync';
import type { LocalCart, GuestOrderResponse } from 'omni-sync-sdk';
type Step = 'info' | 'review' | 'complete';
export default function CheckoutPage() {
const [cart, setCart] = useState<LocalCart>(omni.getLocalCart());
const [step, setStep] = useState<Step>('info');
const [order, setOrder] = useState<GuestOrderResponse | null>(null);
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState('');
// Form state
const [email, setEmail] = useState(cart.customer?.email || '');
const [firstName, setFirstName] = useState(cart.customer?.firstName || '');
const [lastName, setLastName] = useState(cart.customer?.lastName || '');
const [address, setAddress] = useState(cart.shippingAddress?.line1 || '');
const [city, setCity] = useState(cart.shippingAddress?.city || '');
const [postalCode, setPostalCode] = useState(cart.shippingAddress?.postalCode || '');
const [country, setCountry] = useState(cart.shippingAddress?.country || 'US');
// Calculate subtotal
const subtotal = cart.items.reduce((sum, item) => {
return sum + (parseFloat(item.price || '0') * item.quantity);
}, 0);
// Redirect if cart is empty
useEffect(() => {
if (cart.items.length === 0 && step !== 'complete') {
window.location.href = '/cart';
}
}, [cart.items.length, step]);
const handleInfoSubmit = (e: React.FormEvent) => {
e.preventDefault();
// Save to local cart
omni.setLocalCartCustomer({ email, firstName, lastName });
omni.setLocalCartShippingAddress({
firstName,
lastName,
line1: address,
city,
postalCode,
country,
});
setStep('review');
};
const handlePlaceOrder = async () => {
setSubmitting(true);
setError('');
try {
// Single API call to create order!
const result = await omni.submitGuestOrder();
setOrder(result);
setStep('complete');
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to place order');
} finally {
setSubmitting(false);
}
};
if (step === 'complete' && order) {
return (
<div className="text-center py-12">
<h1 className="text-3xl font-bold text-green-600">Order Complete!</h1>
<p className="mt-4">Order Number: <strong>{order.orderNumber}</strong></p>
<p className="mt-2">Total: <strong>${order.total.toFixed(2)}</strong></p>
<p className="mt-4 text-gray-600">A confirmation email will be sent to {email}</p>
<a href="/" className="mt-6 inline-block text-blue-600">Continue Shopping</a>
</div>
);
}
return (
<div className="max-w-2xl mx-auto">
<h1 className="text-2xl font-bold mb-6">Checkout</h1>
{error && (
<div className="bg-red-100 text-red-600 p-3 rounded mb-4">{error}</div>
)}
{step === 'info' && (
<form onSubmit={handleInfoSubmit} className="space-y-4">
<h2 className="text-lg font-bold">Contact Information</h2>
<input
