npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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.

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-sdk

Quick 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 pages

Get 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 type is '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 type is '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 variant

Clear 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

  1. Go to OmniSync Admin → Integrations → Vibe-Coded Sites
  2. Click on your connection → Settings
  3. Enable "Track Guest Checkouts"
  4. 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 localStorage

Get 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 home

Get 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 sent

Note: 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 configuration

OAuth 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