Installation and Setup
Install the SDK and set up your React application:Copy
npm install @commercengine/storefront-sdk
# or
yarn add @commercengine/storefront-sdk
Basic Configuration
Create a shared SDK instance:Copy
// lib/storefront.ts
import { StorefrontSDK, Environment, BrowserTokenStorage } from '@commercengine/storefront-sdk';
export const storefront = new StorefrontSDK({
storeId: process.env.REACT_APP_STORE_ID!,
environment: process.env.NODE_ENV === 'production' ? Environment.Production : Environment.Staging,
apiKey: process.env.REACT_APP_API_KEY!,
tokenStorage: new BrowserTokenStorage('myapp_'),
debug: process.env.NODE_ENV === 'development'
});
Context Providers
Commerce Context
Create a context to share the SDK instance across your app:Copy
// contexts/CommerceContext.tsx
import React, { createContext, useContext, ReactNode } from 'react';
import { StorefrontSDK } from '@commercengine/storefront-sdk';
import { storefront } from '../lib/storefront';
interface CommerceContextType {
client: StorefrontSDK;
}
const CommerceContext = createContext<CommerceContextType | undefined>(undefined);
export function CommerceProvider({ children }: { children: ReactNode }) {
return (
<CommerceContext.Provider value={{ client: storefront }}>
{children}
</CommerceContext.Provider>
);
}
export function useCommerce() {
const context = useContext(CommerceContext);
if (context === undefined) {
throw new Error('useCommerce must be used within a CommerceProvider');
}
return context;
}
Authentication Context
Manage authentication state across your application:Copy
// contexts/AuthContext.tsx
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { useCommerce } from './CommerceContext';
import type { UserInfo } from '@commercengine/storefront-sdk';
interface AuthContextType {
user: UserInfo | null;
isLoading: boolean;
isLoggedIn: boolean;
login: (email: string) => Promise<boolean>;
logout: () => Promise<void>;
refreshUser: () => Promise<void>;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: ReactNode }) {
const { client } = useCommerce();
const [user, setUser] = useState<UserInfo | null>(null);
const [isLoading, setIsLoading] = useState(true);
const isLoggedIn = user !== null;
const refreshUser = async () => {
const { data: userData, error } = await client.auth.retrieveUser();
if (userData) {
setUser(userData.content);
} else {
setUser(null);
}
};
const login = async (email: string): Promise<boolean> => {
setIsLoading(true);
try {
// This only initiates the login flow by sending an OTP.
// A full implementation would require another step to verify the OTP.
const { data, error } = await client.auth.loginWithEmail({
email,
register_if_not_exists: true
});
// For this example, we'll assume success means OTP was sent.
return !!data;
} catch (error) {
return false;
} finally {
setIsLoading(false);
}
};
const logout = async () => {
setIsLoading(true);
try {
await client.auth.logoutUser();
await client.clearTokens();
setUser(null);
} catch (error) {
// Force clear even if API call fails
await client.clearTokens();
setUser(null);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
refreshUser().finally(() => setIsLoading(false));
}, []);
return (
<AuthContext.Provider value={{
user,
isLoading,
isLoggedIn,
login,
logout,
refreshUser
}}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
Custom Hooks
useCart Hook
Manage shopping cart state with stateless cart operations:Copy
// hooks/useCart.ts
import { useState, useEffect } from 'react';
import { useCommerce } from '../contexts/CommerceContext';
import { useAuth } from '../contexts/AuthContext';
import type { Cart } from '@commercengine/storefront-sdk';
interface CartItem {
product_id: string;
variant_id: string | null; // Always required, can be null
quantity: number;
}
interface UseCartReturn {
cart: Cart | null;
loading: boolean;
error: string | null;
addItem: (item: CartItem) => Promise<boolean>;
updateItem: (item: CartItem) => Promise<boolean>;
removeItem: (productId: string, variantId?: string | null) => Promise<boolean>;
clearCart: () => Promise<boolean>;
refresh: () => Promise<void>;
itemCount: number;
}
export function useCart(): UseCartReturn {
const { client } = useCommerce();
const { user, isLoggedIn } = useAuth();
const [cart, setCart] = useState<Cart | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchCart = async () => {
if (!isLoggedIn || !user) {
setCart(null);
return;
}
setLoading(true);
setError(null);
try {
const { data, error } = await client.cart.retrieveCartUsingUserId(user.id);
if (data) {
setCart(data.content);
} else if (error) {
setError(error.message);
setCart(null);
}
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
setCart(null);
} finally {
setLoading(false);
}
};
const addItem = async (item: CartItem): Promise<boolean> => {
if (!cart) return false;
setLoading(true);
setError(null);
try {
const { data, error } = await client.cart.addDeleteCartItem(cart.id, {
product_id: item.product_id,
variant_id: item.variant_id,
quantity: item.quantity
});
if (data) {
// Cart APIs are stateless and return the full updated cart
setCart(data.content);
return true;
} else if (error) {
setError(error.message);
return false;
}
return false;
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
return false;
} finally {
setLoading(false);
}
};
const updateItem = async (item: CartItem): Promise<boolean> => {
// Same as addItem since cart APIs handle updates by product_id + variant_id
return addItem(item);
};
const removeItem = async (productId: string, variantId: string | null = null): Promise<boolean> => {
return addItem({
product_id: productId,
variant_id: variantId,
quantity: 0 // Setting quantity to 0 removes the item
});
};
const clearCart = async (): Promise<boolean> => {
if (!cart?.id) return false;
setLoading(true);
setError(null);
try {
const { data, error } = await client.cart.deleteCart(cart.id);
if (data) {
setCart(null);
return true;
} else if (error) {
setError(error.message);
return false;
}
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
return false;
} finally {
setLoading(false);
}
};
// Calculate total items in cart
const itemCount = cart?.items?.reduce((total, item) => total + item.quantity, 0) || 0;
useEffect(() => {
fetchCart();
}, [isLoggedIn, user]);
return {
cart,
loading,
error,
addItem,
updateItem,
removeItem,
clearCart,
refresh: fetchCart,
itemCount
};
}
Component Examples
Add to Cart Example
Copy
// components/AddToCartButton.tsx
import React, { useState } from 'react';
import { useCart } from '../hooks/useCart';
interface AddToCartButtonProps {
productId: string;
variantId?: string | null;
quantity?: number;
}
export function AddToCartButton({
productId,
variantId = null,
quantity = 1
}: AddToCartButtonProps) {
const { addItem, loading } = useCart();
const [isAdding, setIsAdding] = useState(false);
const handleAddToCart = async () => {
setIsAdding(true);
const success = await addItem({
product_id: productId,
variant_id: variantId, // Always required, can be null
quantity
});
if (success) {
// Optional: Show success message
console.log('Item added to cart successfully');
}
setIsAdding(false);
};
return (
<button
onClick={handleAddToCart}
disabled={loading || isAdding}
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 disabled:opacity-50"
>
{isAdding ? 'Adding...' : 'Add to Cart'}
</button>
);
}
Authentication Component
Copy
// components/AuthButton.tsx
import React, { useState } from 'react';
import { useAuth } from '../contexts/AuthContext';
export function AuthButton() {
const { isLoggedIn, user, login, logout, isLoading } = useAuth();
const [email, setEmail] = useState('');
const [showLogin, setShowLogin] = useState(false);
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
const success = await login(email);
if (success) {
setShowLogin(false);
setEmail('');
}
};
if (isLoading) {
return <div>Loading...</div>;
}
if (isLoggedIn) {
return (
<div className="flex items-center gap-4">
<span>Welcome, {user?.email}</span>
<button
onClick={logout}
className="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600"
>
Logout
</button>
</div>
);
}
return (
<div>
{showLogin ? (
<form onSubmit={handleLogin} className="flex gap-2">
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Enter your email"
className="border rounded px-3 py-2"
required
/>
<button
type="submit"
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
>
Login
</button>
<button
type="button"
onClick={() => setShowLogin(false)}
className="bg-gray-500 text-white px-4 py-2 rounded hover:bg-gray-600"
>
Cancel
</button>
</form>
) : (
<button
onClick={() => setShowLogin(true)}
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
>
Login
</button>
)}
</div>
);
}
Cart Component
Copy
// components/Cart.tsx
import React from 'react';
import { useCart } from '../hooks/useCart';
export function Cart() {
const { cart, loading, updateItem, removeItem, itemCount } = useCart();
if (loading) return <div>Loading cart...</div>;
if (!cart || !cart.items?.length) return <div>Your cart is empty</div>;
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h2 className="text-2xl font-bold">Shopping Cart</h2>
<span className="text-sm text-gray-600">{itemCount} items</span>
</div>
{cart.items.map(item => (
<div key={`${item.product_id}-${item.variant_id || 'default'}`} className="flex items-center justify-between border-b pb-4">
<div>
<h3 className="font-semibold">{item.product_name}</h3>
{item.variant_name && (
<p className="text-sm text-gray-500">{item.variant_name}</p>
)}
<p className="text-gray-600">${item.selling_price}</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => updateItem({
product_id: item.product_id,
variant_id: item.variant_id,
quantity: Math.max(0, item.quantity - 1)
})}
className="bg-gray-200 px-2 py-1 rounded hover:bg-gray-300"
disabled={loading}
>
-
</button>
<span className="mx-2">{item.quantity}</span>
<button
onClick={() => updateItem({
product_id: item.product_id,
variant_id: item.variant_id,
quantity: item.quantity + 1
})}
className="bg-gray-200 px-2 py-1 rounded hover:bg-gray-300"
disabled={loading}
>
+
</button>
<button
onClick={() => removeItem(item.product_id, item.variant_id)}
className="bg-red-500 text-white px-3 py-1 rounded ml-4 hover:bg-red-600"
disabled={loading}
>
Remove
</button>
</div>
</div>
))}
<div className="pt-4 space-y-2">
<div className="flex justify-between">
<span>Subtotal:</span>
<span>${cart.sub_total}</span>
</div>
{cart.tax_amount > 0 && (
<div className="flex justify-between">
<span>Tax:</span>
<span>${cart.tax_amount}</span>
</div>
)}
{cart.shipping_amount > 0 && (
<div className="flex justify-between">
<span>Shipping:</span>
<span>${cart.shipping_amount}</span>
</div>
)}
<div className="flex justify-between text-xl font-bold border-t pt-2">
<span>Total:</span>
<span>${cart.grand_total}</span>
</div>
</div>
</div>
);
}
App Integration
Wrap your app with the providers:Copy
// App.tsx
import React from 'react';
import { CommerceProvider } from './contexts/CommerceContext';
import { AuthProvider } from './contexts/AuthContext';
import { AddToCartButton } from './components/AddToCartButton';
import { AuthButton } from './components/AuthButton';
import { Cart } from './components/Cart';
function App() {
return (
<CommerceProvider>
<AuthProvider>
<div className="container mx-auto p-4">
<header className="flex justify-between items-center mb-8">
<h1 className="text-3xl font-bold">My Store</h1>
<AuthButton />
</header>
<main className="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div className="lg:col-span-2">
{/* Your product catalog components */}
<div className="space-y-4">
<h2 className="text-xl font-semibold">Products</h2>
{/* Example product cards with add to cart */}
<div className="border rounded-lg p-4">
<h3 className="font-semibold">Sample Product 1</h3>
<p className="text-gray-600">$29.99</p>
<AddToCartButton
productId="product-123"
variantId={null}
quantity={1}
/>
</div>
<div className="border rounded-lg p-4">
<h3 className="font-semibold">Sample Product 2</h3>
<p className="text-gray-600">$39.99</p>
<AddToCartButton
productId="product-456"
variantId="variant-789"
quantity={1}
/>
</div>
</div>
</div>
<div>
<Cart />
</div>
</main>
</div>
</AuthProvider>
</CommerceProvider>
);
}
export default App;
Best Practices
Context Providers
Use context providers to share SDK instances and authentication state across your application
Type Safety
Leverage TypeScript and the SDKβs type definitions for better development experience
Cross-References
- Token Management: Token Management Guide for authentication patterns
- Error Handling: Error Handling Guide for comprehensive error management
- API Reference: API Reference for endpoint documentation
The SDKβs React integration provides type-safe hooks and context patterns that make it easy to build robust e-commerce applications with proper state management and error handling.