commerce-bondhu-sdk
v0.1.2
Published
Headless e-commerce SDK for Commerce Bondhu — React hooks and API client for building storefronts
Maintainers
Readme
commerce-bondhu-sdk
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
swris a peer dependency required for React hooks. If you only use the vanilla JS client,swris 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
