Commerce Engine is now in beta. We're working hard to make it better for you.
Complete guide to integrating the SDK with React applications
npm install @commercengine/storefront-sdk
# or
yarn add @commercengine/storefront-sdk
// 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'
});
// 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;
}
// 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;
}
// 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
};
}
// 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>
);
}
// 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>
);
}
// 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.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;