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

commerce-bondhu-sdk

v0.1.2

Published

Headless e-commerce SDK for Commerce Bondhu — React hooks and API client for building storefronts

Readme

commerce-bondhu-sdk

npm version license

Headless e-commerce SDK for Commerce Bondhu. React hooks + TypeScript API client for building storefronts powered by the Commerce Bondhu admin backend.

Features

  • React hooks — SWR-powered hooks for products, categories, cart, orders, auth
  • TypeScript — full type definitions for all data models
  • Customer auth — JWT-based login/register with automatic token persistence
  • Cart management — add, update, remove, clear with computed totals
  • Order lifecycle — place orders, track status, view history
  • Tree-shaking — ESM + CJS builds, import only what you use

Installation

npm install commerce-bondhu-sdk swr
# pnpm
pnpm add commerce-bondhu-sdk swr
# yarn
yarn add commerce-bondhu-sdk swr

swr is a peer dependency required for React hooks. If you only use the vanilla JS client, swr is optional.


Quick Start (Next.js App Router)

1. Wrap your layout with CommerceBondhuProvider

// app/layout.tsx
import { CommerceBondhuProvider } from 'commerce-bondhu-sdk/hooks';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <CommerceBondhuProvider
          baseUrl={process.env.NEXT_PUBLIC_CB_BASE_URL!}
          apiKey={process.env.NEXT_PUBLIC_CB_API_KEY!}
        >
          {children}
        </CommerceBondhuProvider>
      </body>
    </html>
  );
}

2. Add environment variables

NEXT_PUBLIC_CB_BASE_URL=https://your-admin-app.vercel.app
NEXT_PUBLIC_CB_API_KEY=cbk_live_...

Get your API key from Admin App → Settings → API Keys.

3. Use hooks in any client component

'use client';
import { useProducts } from 'commerce-bondhu-sdk/hooks';

export function ProductGrid() {
  const { products, isLoading } = useProducts({ limit: 12 });
  if (isLoading) return <div>Loading...</div>;
  return (
    <div>
      {products?.map(p => <div key={p.id}>{p.name} — ৳{p.price}</div>)}
    </div>
  );
}

Complete Examples

Product Listing Page

'use client';
import { useProducts, useCategories } from 'commerce-bondhu-sdk/hooks';
import { useRouter, useSearchParams } from 'next/navigation';
import Link from 'next/link';
import { useState, useCallback } from 'react';

function getPrimaryImage(images: { url: string; isPrimary: boolean }[]) {
  return (images.find(i => i.isPrimary) ?? images[0])?.url ?? null;
}

export function ProductsPage() {
  const router = useRouter();
  const searchParams = useSearchParams();
  const [searchInput, setSearchInput] = useState(searchParams.get('search') ?? '');

  const category = searchParams.get('category') ?? undefined;
  const search = searchParams.get('search') ?? undefined;
  const page = parseInt(searchParams.get('page') ?? '1');

  const { products, pagination, isLoading } = useProducts({ category, search, page, limit: 12 });
  const { categories } = useCategories();

  const setParam = useCallback((key: string, value: string | undefined) => {
    const params = new URLSearchParams(searchParams.toString());
    value ? params.set(key, value) : params.delete(key);
    if (key !== 'page') params.delete('page');
    router.push(`/products?${params.toString()}`);
  }, [router, searchParams]);

  return (
    <div style={{ display: 'flex', gap: 24 }}>
      {/* Sidebar */}
      <aside style={{ width: 200 }}>
        <button onClick={() => setParam('category', undefined)}>All Products</button>
        {categories?.map(cat => (
          <div key={cat.id}>
            <button onClick={() => setParam('category', cat.slug)}
              style={{ fontWeight: category === cat.slug ? 'bold' : 'normal' }}>
              {cat.name}
            </button>
            {category === cat.slug && cat.children?.map(child => (
              <Link key={child.id} href={`/products?category=${child.slug}`}
                style={{ display: 'block', paddingLeft: 16 }}>
                {child.name}
              </Link>
            ))}
          </div>
        ))}
      </aside>

      {/* Main */}
      <main style={{ flex: 1 }}>
        <form onSubmit={e => { e.preventDefault(); setParam('search', searchInput || undefined); }}>
          <input value={searchInput} onChange={e => setSearchInput(e.target.value)}
            placeholder="Search products..." />
          <button type="submit">Search</button>
        </form>

        {isLoading ? <p>Loading...</p> : (
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 16 }}>
            {products?.map(product => {
              const image = getPrimaryImage(product.images);
              return (
                <Link key={product.id} href={`/product/${product.slug}`}>
                  {image && <img src={image} alt={product.name} style={{ width: '100%' }} />}
                  <h3>{product.name}</h3>
                  <p>৳{product.price}</p>
                  {product.comparePrice && <s>৳{product.comparePrice}</s>}
                </Link>
              );
            })}
          </div>
        )}

        {pagination && pagination.totalPages > 1 && (
          <div>
            <button disabled={page <= 1} onClick={() => setParam('page', String(page - 1))}>Prev</button>
            <span> Page {page} of {pagination.totalPages} </span>
            <button disabled={page >= pagination.totalPages} onClick={() => setParam('page', String(page + 1))}>Next</button>
          </div>
        )}
      </main>
    </div>
  );
}

Product Detail Page

'use client';
import { useProduct, useCart, useAuth } from 'commerce-bondhu-sdk/hooks';
import { useState } from 'react';

function getPrimaryImage(images: { url: string; isPrimary: boolean }[]) {
  return (images.find(i => i.isPrimary) ?? images[0])?.url ?? null;
}

export function ProductDetail({ slug }: { slug: string }) {
  const { product, isLoading, error } = useProduct(slug);
  const { addItem } = useCart();
  const { isAuthenticated } = useAuth();
  const [selectedVariantId, setSelectedVariantId] = useState<string | null>(null);
  const [quantity, setQuantity] = useState(1);
  const [adding, setAdding] = useState(false);

  if (isLoading) return <p>Loading...</p>;
  if (error || !product) return <p>Product not found</p>;

  const image = getPrimaryImage(product.images);
  const selectedVariant = product.variants.find(v => v.id === selectedVariantId);
  const price = selectedVariant?.price ?? product.price;
  const comparePrice = selectedVariant?.comparePrice ?? product.comparePrice;
  const inStock = product.availability !== 'OUT_OF_STOCK';
  const activeVariants = product.variants.filter(v => v.inventory > 0 || !product.trackInventory);

  async function handleAddToCart() {
    if (!isAuthenticated) { alert('Please log in to add items to cart'); return; }
    setAdding(true);
    try {
      await addItem({ productId: product!.id, variantId: selectedVariantId ?? undefined, quantity });
      alert('Added to cart!');
    } catch (e) {
      alert('Failed to add to cart');
    } finally {
      setAdding(false);
    }
  }

  return (
    <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 32 }}>
      {image && <img src={image} alt={product.name} style={{ width: '100%' }} />}

      <div>
        {product.category && <p>{product.category.name}</p>}
        <h1>{product.name}</h1>

        <div>
          <strong>৳{price}</strong>
          {comparePrice && <s style={{ marginLeft: 8 }}>৳{comparePrice}</s>}
        </div>

        <p>{product.availability === 'IN_STOCK' ? '✓ In Stock' : product.availability === 'PRE_ORDER' ? 'Pre-order' : '✗ Out of Stock'}</p>

        {product.description && <p>{product.description}</p>}

        {activeVariants.length > 0 && (
          <div>
            <p>Variant:</p>
            {activeVariants.map(v => (
              <button key={v.id}
                onClick={() => setSelectedVariantId(selectedVariantId === v.id ? null : v.id)}
                style={{ fontWeight: selectedVariantId === v.id ? 'bold' : 'normal', marginRight: 8 }}>
                {v.name}
              </button>
            ))}
          </div>
        )}

        <div>
          <button onClick={() => setQuantity(q => Math.max(1, q - 1))}>−</button>
          <span style={{ margin: '0 12px' }}>{quantity}</span>
          <button onClick={() => setQuantity(q => q + 1)}>+</button>
        </div>

        <button onClick={handleAddToCart} disabled={adding || !inStock} style={{ marginTop: 16 }}>
          {adding ? 'Adding...' : 'Add to Cart'}
        </button>

        {product.reviews && product.reviews.length > 0 && (
          <div>
            <h3>Reviews ({product.reviews.length})</h3>
            {product.reviews.map(r => (
              <div key={r.id}>
                <strong>{'★'.repeat(r.rating)}{'☆'.repeat(5 - r.rating)}</strong>
                <span style={{ marginLeft: 8 }}>{r.user.name ?? 'Anonymous'}</span>
                {r.title && <p><strong>{r.title}</strong></p>}
                {r.comment && <p>{r.comment}</p>}
              </div>
            ))}
          </div>
        )}
      </div>
    </div>
  );
}

Server page wrapper (Next.js App Router):

// app/product/[slug]/page.tsx
import { ProductDetail } from '@/components/ProductDetail';

export default async function ProductPage({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  return <ProductDetail slug={slug} />;
}

Category Navigator (with icons)

'use client';
import { useCategories } from 'commerce-bondhu-sdk/hooks';
import type { Category } from 'commerce-bondhu-sdk';
import { DynamicIcon, type IconName } from 'lucide-react/dynamic'; // requires lucide-react >=0.447
import Link from 'next/link';
import { useState } from 'react';

// category.icon is a lucide icon name string e.g. "shopping-bag"
function CategoryIcon({ category }: { category: Pick<Category, 'icon' | 'image' | 'name'> }) {
  if (category.icon) return <DynamicIcon name={category.icon as IconName} style={{ width: 16, height: 16 }} />;
  if (category.image) return <img src={category.image} alt={category.name} style={{ width: 16, height: 16 }} />;
  return <span>📁</span>;
}

export function CategoryNav() {
  const { categories, isLoading } = useCategories();
  const [selectedSlug, setSelectedSlug] = useState<string | null>(null);

  if (isLoading) return <p>Loading categories...</p>;

  const selected = categories?.find(c => c.slug === selectedSlug);

  return (
    <nav>
      {!selectedSlug ? (
        categories?.map(cat => (
          <button key={cat.id}
            onClick={() => cat.children?.length ? setSelectedSlug(cat.slug) : undefined}
            style={{ display: 'flex', alignItems: 'center', gap: 8, width: '100%' }}>
            <CategoryIcon category={cat} />
            {cat.children?.length ? (
              <span>{cat.name} ›</span>
            ) : (
              <Link href={`/products?category=${cat.slug}`}>{cat.name}</Link>
            )}
          </button>
        ))
      ) : (
        <div>
          <button onClick={() => setSelectedSlug(null)}>‹ {selected?.name}</button>
          <Link href={`/products?category=${selected?.slug}`} style={{ display: 'block' }}>
            All {selected?.name}
          </Link>
          {selected?.children?.map(child => (
            <Link key={child.id} href={`/products?category=${child.slug}`}
              style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
              <CategoryIcon category={child} />
              {child.name}
            </Link>
          ))}
        </div>
      )}
    </nav>
  );
}

Cart Sidebar

'use client';
import { useCart, useAuth } from 'commerce-bondhu-sdk/hooks';

export function CartSidebar() {
  const { isAuthenticated } = useAuth();
  const { items, itemCount, total, isLoading, updateItem, removeItem, clearCart } = useCart();

  if (!isAuthenticated) return <p>Log in to view your cart.</p>;
  if (isLoading) return <p>Loading cart...</p>;
  if (!items?.length) return <p>Your cart is empty.</p>;

  return (
    <div>
      <h2>Cart ({itemCount})</h2>

      {items.map(item => {
        const price = item.variant?.price ?? item.product.price;
        const image = (item.product.images.find(i => i.isPrimary) ?? item.product.images[0])?.url;

        return (
          <div key={item.id} style={{ display: 'flex', gap: 12, marginBottom: 12 }}>
            {image && <img src={image} alt={item.product.name} style={{ width: 60, height: 60, objectFit: 'cover' }} />}
            <div style={{ flex: 1 }}>
              <p>{item.product.name}</p>
              {item.variant && (
                <p style={{ fontSize: 12, color: '#666' }}>
                  {item.variant.options.map(o => `${o.name}: ${o.value}`).join(', ')}
                </p>
              )}
              <p>৳{price}</p>
            </div>
            <div>
              <button onClick={() => updateItem(item.id, Math.max(1, item.quantity - 1))}>−</button>
              <span style={{ margin: '0 8px' }}>{item.quantity}</span>
              <button onClick={() => updateItem(item.id, item.quantity + 1)}>+</button>
              <button onClick={() => removeItem(item.id)} style={{ marginLeft: 8 }}>✕</button>
            </div>
          </div>
        );
      })}

      <div style={{ borderTop: '1px solid #eee', paddingTop: 12 }}>
        <strong>Total: ৳{total}</strong>
      </div>
      <button onClick={() => clearCart()}>Clear Cart</button>
    </div>
  );
}

Authentication (Login / Register)

'use client';
import { useAuth } from 'commerce-bondhu-sdk/hooks';
import { useState } from 'react';

export function AuthPage() {
  const { login, register, isAuthenticated, isLoading, error } = useAuth();
  const [mode, setMode] = useState<'login' | 'register'>('login');
  const [form, setForm] = useState({ firstName: '', lastName: '', email: '', password: '', phone: '' });

  if (isAuthenticated) return <p>You are logged in. <a href="/account">View Account</a></p>;

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    if (mode === 'login') {
      await login({ email: form.email, password: form.password });
    } else {
      await register({ firstName: form.firstName, lastName: form.lastName, email: form.email, password: form.password, phone: form.phone || undefined });
    }
  }

  const set = (k: string) => (e: React.ChangeEvent<HTMLInputElement>) => setForm(f => ({ ...f, [k]: e.target.value }));

  return (
    <div>
      <div>
        <button onClick={() => setMode('login')} style={{ fontWeight: mode === 'login' ? 'bold' : 'normal' }}>Login</button>
        <button onClick={() => setMode('register')} style={{ fontWeight: mode === 'register' ? 'bold' : 'normal' }}>Register</button>
      </div>

      <form onSubmit={handleSubmit}>
        {mode === 'register' && (
          <>
            <input placeholder="First name" value={form.firstName} onChange={set('firstName')} required /><br />
            <input placeholder="Last name" value={form.lastName} onChange={set('lastName')} required /><br />
            <input placeholder="Phone (optional)" value={form.phone} onChange={set('phone')} /><br />
          </>
        )}
        <input type="email" placeholder="Email" value={form.email} onChange={set('email')} required /><br />
        <input type="password" placeholder="Password" value={form.password} onChange={set('password')} required /><br />
        {error && <p style={{ color: 'red' }}>{error.message}</p>}
        <button type="submit" disabled={isLoading}>
          {isLoading ? 'Please wait...' : mode === 'login' ? 'Login' : 'Create Account'}
        </button>
      </form>
    </div>
  );
}

Customer Account & Orders

'use client';
import { useCustomer, useOrders, useAuth } from 'commerce-bondhu-sdk/hooks';
import { useState } from 'react';

export function AccountPage() {
  const { isAuthenticated, logout } = useAuth();
  const { customer, isLoading: loadingCustomer, updateProfile } = useCustomer();
  const { orders, pagination, isLoading: loadingOrders } = useOrders({ limit: 10 });
  const [editing, setEditing] = useState(false);
  const [form, setForm] = useState({ firstName: '', lastName: '', phone: '' });

  if (!isAuthenticated) return <p>Please <a href="/auth">log in</a>.</p>;
  if (loadingCustomer) return <p>Loading...</p>;

  const statusColors: Record<string, string> = {
    PENDING: '#f59e0b', CONFIRMED: '#3b82f6', PROCESSING: '#8b5cf6',
    SHIPPED: '#06b6d4', DELIVERED: '#10b981', CANCELLED: '#ef4444', REFUNDED: '#6b7280',
  };

  async function handleUpdate(e: React.FormEvent) {
    e.preventDefault();
    await updateProfile({ firstName: form.firstName || undefined, lastName: form.lastName || undefined, phone: form.phone || undefined });
    setEditing(false);
  }

  return (
    <div>
      <div>
        <h2>Account</h2>
        <button onClick={logout}>Logout</button>
      </div>

      {customer && (
        <div>
          {!editing ? (
            <div>
              <p>{customer.firstName} {customer.lastName}</p>
              <p>{customer.email}</p>
              {customer.phone && <p>{customer.phone}</p>}
              <button onClick={() => { setForm({ firstName: customer.firstName, lastName: customer.lastName, phone: customer.phone ?? '' }); setEditing(true); }}>
                Edit Profile
              </button>
            </div>
          ) : (
            <form onSubmit={handleUpdate}>
              <input value={form.firstName} onChange={e => setForm(f => ({ ...f, firstName: e.target.value }))} placeholder="First name" /><br />
              <input value={form.lastName} onChange={e => setForm(f => ({ ...f, lastName: e.target.value }))} placeholder="Last name" /><br />
              <input value={form.phone} onChange={e => setForm(f => ({ ...f, phone: e.target.value }))} placeholder="Phone" /><br />
              <button type="submit">Save</button>
              <button type="button" onClick={() => setEditing(false)}>Cancel</button>
            </form>
          )}
        </div>
      )}

      <div>
        <h3>Order History</h3>
        {loadingOrders ? <p>Loading orders...</p> : orders?.length === 0 ? <p>No orders yet.</p> : (
          orders?.map(order => (
            <div key={order.id} style={{ border: '1px solid #eee', padding: 12, marginBottom: 8 }}>
              <div style={{ display: 'flex', justifyContent: 'space-between' }}>
                <strong>#{order.orderNumber}</strong>
                <span style={{ color: statusColors[order.status] ?? '#000' }}>{order.status}</span>
              </div>
              <p>{new Date(order.createdAt).toLocaleDateString()} — ৳{order.total}</p>
              {order.items.map(item => (
                <div key={item.id} style={{ fontSize: 14, color: '#666' }}>
                  {item.product.name} × {item.quantity}
                  {item.variant && ` (${item.variant.options.map(o => o.value).join(', ')})`}
                </div>
              ))}
            </div>
          ))
        )}
      </div>
    </div>
  );
}

Checkout Page

'use client';
import { useCart, useCreateOrder, useAuth } from 'commerce-bondhu-sdk/hooks';
import type { Address } from 'commerce-bondhu-sdk';
import { useState } from 'react';
import { useRouter } from 'next/navigation';

export function CheckoutPage() {
  const router = useRouter();
  const { isAuthenticated } = useAuth();
  const { items, total, isLoading: cartLoading } = useCart();
  const { createOrder, isLoading: placing } = useCreateOrder();
  const [address, setAddress] = useState<Omit<Address, 'id' | 'type' | 'isDefault'>>({
    firstName: '', lastName: '', address1: '', city: '', state: '', postalCode: '', country: 'BD',
  });

  if (!isAuthenticated) return <p>Please <a href="/auth">log in</a> to checkout.</p>;
  if (cartLoading) return <p>Loading cart...</p>;
  if (!items?.length) return <p>Your cart is empty.</p>;

  const set = (k: string) => (e: React.ChangeEvent<HTMLInputElement>) => setAddress(a => ({ ...a, [k]: e.target.value }));

  async function handlePlaceOrder(e: React.FormEvent) {
    e.preventDefault();
    try {
      const order = await createOrder({
        items: items!.map(item => ({
          productId: item.product.id,
          variantId: item.variant?.id,
          quantity: item.quantity,
        })),
        shippingAddress: address,
      });
      router.push(`/orders/${order.id}?success=1`);
    } catch (err) {
      alert('Failed to place order. Please try again.');
    }
  }

  return (
    <div style={{ display: 'grid', gridTemplateColumns: '1fr 400px', gap: 32 }}>
      <form onSubmit={handlePlaceOrder}>
        <h2>Shipping Address</h2>
        <input placeholder="First name" value={address.firstName} onChange={set('firstName')} required /><br />
        <input placeholder="Last name" value={address.lastName} onChange={set('lastName')} required /><br />
        <input placeholder="Address line 1" value={address.address1} onChange={set('address1')} required /><br />
        <input placeholder="Address line 2 (optional)" value={address.address2 ?? ''} onChange={set('address2')} /><br />
        <input placeholder="City" value={address.city} onChange={set('city')} required /><br />
        <input placeholder="State / Division" value={address.state} onChange={set('state')} required /><br />
        <input placeholder="Postal code" value={address.postalCode} onChange={set('postalCode')} required /><br />
        <input placeholder="Country" value={address.country} onChange={set('country')} required /><br />
        <input placeholder="Phone (optional)" value={address.phone ?? ''} onChange={set('phone')} /><br />
        <button type="submit" disabled={placing} style={{ marginTop: 16 }}>
          {placing ? 'Placing order...' : `Place Order — ৳${total}`}
        </button>
      </form>

      <div>
        <h2>Order Summary</h2>
        {items.map(item => {
          const price = item.variant?.price ?? item.product.price;
          return (
            <div key={item.id} style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 8 }}>
              <span>{item.product.name} × {item.quantity}</span>
              <span>৳{price * item.quantity}</span>
            </div>
          );
        })}
        <div style={{ borderTop: '1px solid #eee', paddingTop: 8 }}>
          <strong>Total: ৳{total}</strong>
        </div>
      </div>
    </div>
  );
}

API Reference

CommerceBondhuProvider

| Prop | Type | Required | Description | |------|------|----------|-------------| | baseUrl | string | ✓ | Your admin app URL (no trailing slash) | | apiKey | string | ✓ | Store API key (cbk_live_...) | | disablePersistence | boolean | — | Disable localStorage token persistence |

Hooks

| Hook | Auth Required | Returns | |------|:---:|---------| | useStore() | — | { store, isLoading, error } | | useProducts(params?) | — | { products, pagination, isLoading, error, mutate } | | useProduct(slug) | — | { product, isLoading, error } | | useCategories() | — | { categories, isLoading, error } | | useCollections() | — | { collections, isLoading, error } | | useAuth() | — | { login, register, logout, isAuthenticated, isLoading, error } | | useCustomer() | ✓ | { customer, isLoading, error, updateProfile, mutate } | | useCart() | ✓ | { items, itemCount, total, isLoading, error, addItem, updateItem, removeItem, clearCart, mutate } | | useOrders(params?) | ✓ | { orders, pagination, isLoading, error, mutate } | | useOrder(id) | ✓ | { order, isLoading, error } | | useCreateOrder() | ✓ | { createOrder, isLoading, error } |

useProducts params:

| Param | Type | Description | |-------|------|-------------| | page | number | Page number (default: 1) | | limit | number | Items per page (default: 10) | | search | string | Full-text search | | category | string | Filter by category slug | | collection | string | Filter by collection slug | | featured | boolean | Featured products only |

Pass null to useProduct or useOrder to skip fetching (useful for conditional/deferred loading).


Type Definitions

type Store = {
  id: string; name: string; slug: string; description: string | null;
  logo: string | null; banner: string | null; domain: string | null;
  isActive: boolean; settings: StoreSettings | null;
};

type StoreSettings = {
  currency: string; timezone: string; dateFormat: string; weightUnit: string;
  taxIncluded: boolean; defaultTaxRate: number; enableReviews: boolean;
  enableWishlist: boolean; enableCompare: boolean; enableCoupons: boolean;
  address: string | null; city: string | null; country: string | null;
  email: string | null; phone: string | null; state: string | null; zip: string | null;
};

// Images are objects — NOT plain strings
type ProductImage = {
  url: string; alt: string; isPrimary: boolean; sortOrder: number;
};

type ProductVariantOption = { name: string; value: string };

type ProductVariant = {
  id: string; name: string; price: number | null; comparePrice: number | null;
  inventory: number; image: string | null; sku: string | null;
  options: ProductVariantOption[];
};

type ProductReview = {
  id: string; rating: number; title: string | null; comment: string | null;
  isVerified: boolean; createdAt: string;
  user: { name: string | null; image: string | null };
};

type Product = {
  id: string; name: string; slug: string; description: string | null;
  price: number; comparePrice: number | null;
  images: ProductImage[];       // objects with url/alt/isPrimary/sortOrder
  inventory: number;
  availability: 'IN_STOCK' | 'OUT_OF_STOCK' | 'PRE_ORDER';
  isFeatured: boolean; isDigital: boolean; sku: string | null;
  weight: number | null; trackInventory?: boolean; allowBackorders?: boolean;
  category?: { id: string; name: string; slug: string } | null;
  collections?: { id: string; name: string; slug: string }[];
  variants: ProductVariant[];
  reviews?: ProductReview[];
};

type Pagination = { page: number; limit: number; total: number; totalPages: number };

type ProductListResponse = { products: Product[]; pagination: Pagination };

// category.icon is a lucide-react icon name string (e.g. "shopping-bag")
// children is one level deep — Omit<Category, 'children'>[]
type Category = {
  id: string; name: string; slug: string; description: string | null;
  image: string | null; icon: string | null; sortOrder: number;
  children?: Omit<Category, 'children'>[];
};

type Collection = {
  id: string; name: string; slug: string; description: string | null;
  image: string | null; sortOrder: number;
};

type Address = {
  id?: string; type?: 'SHIPPING' | 'BILLING';
  firstName: string; lastName: string; company?: string | null;
  address1: string; address2?: string | null;
  city: string; state: string; postalCode: string; country: string;
  phone?: string | null; isDefault?: boolean;
};

type Customer = {
  id: string; firstName: string; lastName: string;
  email: string; phone: string | null; createdAt?: string;
  addresses?: Address[];
};

type AuthResponse = { token: string; customer: Customer };

type CartItem = {
  id: string; quantity: number;
  product: Pick<Product, 'id' | 'name' | 'slug' | 'price' | 'comparePrice' | 'images' | 'inventory' | 'availability' | 'trackInventory' | 'allowBackorders'>;
  variant: Pick<ProductVariant, 'id' | 'name' | 'price' | 'inventory' | 'image' | 'options'> | null;
};

type OrderStatus = 'PENDING' | 'CONFIRMED' | 'PROCESSING' | 'SHIPPED' | 'DELIVERED' | 'CANCELLED' | 'REFUNDED';

type OrderItem = {
  id: string; quantity: number; price: number; total: number;
  product: { id: string; name: string; slug: string; images: ProductImage[] };
  variant: { id: string; name: string; options: ProductVariantOption[] } | null;
};

type Order = {
  id: string; orderNumber: string; status: OrderStatus;
  subtotal: number; taxAmount: number; shippingAmount: number;
  discountAmount: number; total: number; currency: string;
  notes: string | null; createdAt: string; updatedAt?: string;
  items: OrderItem[];
  shippingAddress: Omit<Address, 'id' | 'type' | 'isDefault'> | null;
  payments?: { id: string; amount: number; status: string; method: string; createdAt: string }[];
};

type CreateOrderInput = {
  items: { productId: string; variantId?: string; quantity: number }[];
  shippingAddress?: Address;
  shippingAmount?: number; discountAmount?: number; notes?: string;
};

type ContactInput = {
  firstName: string; lastName: string; email: string; subject: string; message: string;
};

type ApiError = { error: string; status: number };

Advanced Usage

Vanilla JS / Node.js (no React)

import { CommerceBondhuClient } from 'commerce-bondhu-sdk';

const client = new CommerceBondhuClient({
  baseUrl: 'https://your-admin-app.com',
  apiKey: 'cbk_live_...',
});

const products = await client.products.list({ limit: 10, featured: true });
const product = await client.products.get('my-product-slug');
const categories = await client.categories.list();

// Auth
const { token, customer } = await client.auth.login({ email: '[email protected]', password: 'password' });
client.setCustomerToken(token);

// Cart (requires auth token)
await client.cart.add({ productId: 'prod_id', quantity: 1 });
const cartItems = await client.cart.get();

Disable localStorage Persistence

<CommerceBondhuProvider
  baseUrl={process.env.NEXT_PUBLIC_CB_BASE_URL!}
  apiKey={process.env.NEXT_PUBLIC_CB_API_KEY!}
  disablePersistence={true}   // token not saved to localStorage
>
  {children}
</CommerceBondhuProvider>

Error Handling

import { CommerceBondhuError } from 'commerce-bondhu-sdk';

try {
  await client.auth.login({ email, password });
} catch (err) {
  if (err instanceof CommerceBondhuError) {
    console.log(err.status);   // HTTP status code e.g. 401
    console.log(err.message);  // error message from server
  }
}

Next.js Monorepo (source transpilation)

If using this SDK within a Next.js monorepo with workspace dependencies, add to next.config:

const nextConfig = {
  transpilePackages: ['commerce-bondhu-sdk'],
};

This compiles the SDK TypeScript source directly, preserving "use client" directives.


License

MIT