The SDK integrates seamlessly with React applications, providing type-safe hooks, context providers, and component patterns for e-commerce functionality.

Installation and Setup

Install the SDK and set up your React application:

npm install @commercengine/storefront-sdk
# or
yarn add @commercengine/storefront-sdk

Basic Configuration

Create a shared SDK instance:

// 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:

// 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:

// 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 () => {
    try {
      const userInfo = await client.getUserInfo();
      setUser(userInfo);
    } catch (error) {
      setUser(null);
    }
  };

  const login = async (email: string): Promise<boolean> => {
    setIsLoading(true);
    try {
      const result = await client.auth.loginWithEmail({
        email,
        register_if_not_exists: true
      });

      if (result.success) {
        await refreshUser();
        return true;
      }
      return false;
    } 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:

// 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 result = await client.cart.retrieveCartUsingUserId(user.sub);
      if (result.success) {
        setCart(result.data);
      } else {
        setError(result.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> => {
    setLoading(true);
    setError(null);
    
    try {
      const result = await client.cart.addDeleteCartItem({
        product_id: item.product_id,
        variant_id: item.variant_id,
        quantity: item.quantity
      });

      if (result.success) {
        // Cart APIs are stateless and return the full updated cart
        setCart(result.data);
        return true;
      } else {
        setError(result.error.message);
        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 result = await client.cart.deleteCart(cart.id);
      if (result.success) {
        setCart(null);
        return true;
      } else {
        setError(result.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

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

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

// 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:

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

Stateless Cart Operations

Cart APIs are stateless and return the full updated cart - no need to manage complex state

Product & Variant IDs

Always provide both product_id and variant_id (can be null) when working with cart items

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

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.