Building a Complete Storefront

Learn how to build a production-ready e-commerce storefront from scratch using the Unified Commerce Platform. This guide covers everything from initial setup to deployment.

Prerequisites

Before starting, ensure you have:

  • Node.js 18+ installed
  • An active Unified Commerce account
  • API credentials (get them from Dashboard)
  • Basic knowledge of React/Next.js (or your preferred framework)

Table of Contents

  1. Project Setup
  2. Authentication
  3. Product Catalog
  4. Shopping Cart
  5. Checkout Flow
  6. User Accounts
  7. Order Management
  8. Search & Filtering
  9. Performance Optimization
  10. Deployment
  11. Troubleshooting

Project Setup

Step 1: Initialize Your Project

We'll use Next.js for this guide, but the concepts apply to any framework.

# Create a new Next.js app with TypeScript
npx create-next-app@latest storefront --typescript --tailwind --app

# Navigate to project directory
cd storefront

# Install Unified Commerce SDK and dependencies
npm install @unifiedcommerce/sdk graphql graphql-request swr zustand

Step 2: Configure Environment Variables

Create a .env.local file in your project root:

# Unified Commerce API
NEXT_PUBLIC_UC_ENDPOINT=https://gateway.unifiedcommerce.app/graphql
NEXT_PUBLIC_UC_API_KEY=your_api_key_here
NEXT_PUBLIC_UC_MERCHANT_ID=merchant_abc123

# Stripe (for payments)
NEXT_PUBLIC_STRIPE_PUBLIC_KEY=pk_test_...
STRIPE_SECRET_KEY=sk_test_...

# App Configuration
NEXT_PUBLIC_SITE_URL=http://localhost:3000
NEXT_PUBLIC_SITE_NAME=My Store

Step 3: Set Up API Client

Create lib/api-client.ts:

import { GraphQLClient } from 'graphql-request';
import { getSdk } from '@unifiedcommerce/sdk';

// Initialize GraphQL client
export const client = new GraphQLClient(
  process.env.NEXT_PUBLIC_UC_ENDPOINT!,
  {
    headers: {
      'Authorization': `Bearer ${process.env.NEXT_PUBLIC_UC_API_KEY}`,
      'X-Merchant-ID': process.env.NEXT_PUBLIC_UC_MERCHANT_ID!,
    },
  }
);

// Initialize SDK
export const sdk = getSdk(client);

// Helper for server-side requests
export const serverClient = new GraphQLClient(
  process.env.NEXT_PUBLIC_UC_ENDPOINT!,
  {
    headers: {
      'Authorization': `Bearer ${process.env.NEXT_PUBLIC_UC_API_KEY}`,
      'X-Merchant-ID': process.env.NEXT_PUBLIC_UC_MERCHANT_ID!,
    },
    cache: 'no-store', // Disable caching for server components
  }
);

Step 4: Create Type Definitions

Create types/commerce.ts:

// Product types
export interface Product {
  id: string;
  name: string;
  slug: string;
  description: string;
  price: number;
  currency: string;
  images: ProductImage[];
  variants: ProductVariant[];
  category: Category;
  inventory: InventoryLevel;
  metadata: Record<string, any>;
}

export interface ProductImage {
  id: string;
  url: string;
  alt: string;
  order: number;
}

export interface ProductVariant {
  id: string;
  name: string;
  sku: string;
  price: number;
  inventory: InventoryLevel;
  attributes: VariantAttribute[];
}

export interface VariantAttribute {
  name: string;
  value: string;
}

// Cart types
export interface Cart {
  id: string;
  items: CartItem[];
  subtotal: number;
  tax: number;
  shipping: number;
  total: number;
  currency: string;
  discounts: Discount[];
}

export interface CartItem {
  id: string;
  product: Product;
  variant?: ProductVariant;
  quantity: number;
  price: number;
  total: number;
}

// Order types
export interface Order {
  id: string;
  orderNumber: string;
  status: OrderStatus;
  items: OrderItem[];
  subtotal: number;
  tax: number;
  shipping: number;
  total: number;
  currency: string;
  customer: Customer;
  shippingAddress: Address;
  billingAddress: Address;
  createdAt: string;
  updatedAt: string;
}

export enum OrderStatus {
  PENDING = 'PENDING',
  PROCESSING = 'PROCESSING',
  SHIPPED = 'SHIPPED',
  DELIVERED = 'DELIVERED',
  CANCELLED = 'CANCELLED',
  REFUNDED = 'REFUNDED',
}

Authentication

Step 1: Create Auth Context

Create contexts/auth-context.tsx:

'use client';

import React, { createContext, useContext, useState, useEffect } from 'react';
import { sdk } from '@/lib/api-client';

interface AuthContextType {
  user: User | null;
  isLoading: boolean;
  login: (email: string, password: string) => Promise<void>;
  register: (data: RegisterData) => Promise<void>;
  logout: () => Promise<void>;
  updateProfile: (data: UpdateProfileData) => Promise<void>;
}

const AuthContext = createContext<AuthContextType | undefined>(undefined);

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<User | null>(null);
  const [isLoading, setIsLoading] = useState(true);

  // Check for existing session on mount
  useEffect(() => {
    checkSession();
  }, []);

  const checkSession = async () => {
    try {
      const token = localStorage.getItem('auth_token');
      if (!token) {
        setIsLoading(false);
        return;
      }

      const { data } = await sdk.getCurrentUser();
      if (data?.me) {
        setUser(data.me);
      }
    } catch (error) {
      console.error('Session check failed:', error);
      localStorage.removeItem('auth_token');
    } finally {
      setIsLoading(false);
    }
  };

  const login = async (email: string, password: string) => {
    try {
      const { data } = await sdk.login({ email, password });

      if (data?.login?.token) {
        localStorage.setItem('auth_token', data.login.token);
        setUser(data.login.user);

        // Update API client headers
        sdk.setAuthToken(data.login.token);
      } else if (data?.login?.errors) {
        throw new Error(data.login.errors[0].message);
      }
    } catch (error) {
      console.error('Login failed:', error);
      throw error;
    }
  };

  const register = async (data: RegisterData) => {
    try {
      const { data: result } = await sdk.register({
        input: {
          email: data.email,
          password: data.password,
          firstName: data.firstName,
          lastName: data.lastName,
          phone: data.phone,
        }
      });

      if (result?.register?.token) {
        localStorage.setItem('auth_token', result.register.token);
        setUser(result.register.user);
        sdk.setAuthToken(result.register.token);
      } else if (result?.register?.errors) {
        throw new Error(result.register.errors[0].message);
      }
    } catch (error) {
      console.error('Registration failed:', error);
      throw error;
    }
  };

  const logout = async () => {
    try {
      await sdk.logout();
    } catch (error) {
      console.error('Logout error:', error);
    } finally {
      localStorage.removeItem('auth_token');
      setUser(null);
      sdk.clearAuthToken();
    }
  };

  const updateProfile = async (data: UpdateProfileData) => {
    try {
      const { data: result } = await sdk.updateProfile({ input: data });

      if (result?.updateProfile?.user) {
        setUser(result.updateProfile.user);
      } else if (result?.updateProfile?.errors) {
        throw new Error(result.updateProfile.errors[0].message);
      }
    } catch (error) {
      console.error('Profile update failed:', error);
      throw error;
    }
  };

  return (
    <AuthContext.Provider value={{
      user,
      isLoading,
      login,
      register,
      logout,
      updateProfile,
    }}>
      {children}
    </AuthContext.Provider>
  );
}

export const useAuth = () => {
  const context = useContext(AuthContext);
  if (context === undefined) {
    throw new Error('useAuth must be used within AuthProvider');
  }
  return context;
};

Step 2: Create Login Component

Create components/auth/login-form.tsx:

'use client';

import { useState } from 'react';
import { useAuth } from '@/contexts/auth-context';
import { useRouter } from 'next/navigation';

export function LoginForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');
  const [isLoading, setIsLoading] = useState(false);

  const { login } = useAuth();
  const router = useRouter();

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setError('');
    setIsLoading(true);

    try {
      await login(email, password);
      router.push('/account');
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Login failed');
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <form onSubmit={handleSubmit} className="space-y-4">
      {error && (
        <div className="bg-red-50 text-red-600 p-3 rounded">
          {error}
        </div>
      )}

      <div>
        <label htmlFor="email" className="block text-sm font-medium">
          Email
        </label>
        <input
          id="email"
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          required
          className="mt-1 block w-full rounded border-gray-300"
          disabled={isLoading}
        />
      </div>

      <div>
        <label htmlFor="password" className="block text-sm font-medium">
          Password
        </label>
        <input
          id="password"
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          required
          className="mt-1 block w-full rounded border-gray-300"
          disabled={isLoading}
        />
      </div>

      <button
        type="submit"
        disabled={isLoading}
        className="w-full bg-blue-600 text-white py-2 px-4 rounded hover:bg-blue-700 disabled:opacity-50"
      >
        {isLoading ? 'Signing in...' : 'Sign In'}
      </button>
    </form>
  );
}

Product Catalog

Step 1: Create Product List Page

Create app/products/page.tsx:

import { serverClient } from '@/lib/api-client';
import { ProductGrid } from '@/components/products/product-grid';
import { CategoryFilter } from '@/components/products/category-filter';
import { SortDropdown } from '@/components/products/sort-dropdown';
import { gql } from 'graphql-request';

const GET_PRODUCTS = gql`
  query GetProducts(
    $first: Int!
    $after: String
    $categoryId: ID
    $sortBy: ProductSortField
    $sortOrder: SortOrder
  ) {
    products(
      first: $first
      after: $after
      filters: { categoryId: $categoryId }
      sortBy: $sortBy
      sortOrder: $sortOrder
    ) {
      edges {
        cursor
        node {
          id
          name
          slug
          description
          price
          currency
          images(first: 1) {
            url
            alt
          }
          category {
            id
            name
          }
          inventory {
            available
          }
        }
      }
      pageInfo {
        hasNextPage
        endCursor
      }
      totalCount
    }

    categories {
      id
      name
      slug
      productCount
    }
  }
`;

export default async function ProductsPage({
  searchParams,
}: {
  searchParams: { category?: string; sort?: string; page?: string };
}) {
  const { data } = await serverClient.request(GET_PRODUCTS, {
    first: 12,
    after: searchParams.page,
    categoryId: searchParams.category,
    sortBy: searchParams.sort?.split('-')[0] || 'CREATED_AT',
    sortOrder: searchParams.sort?.split('-')[1] || 'DESC',
  });

  return (
    <div className="container mx-auto px-4 py-8">
      <h1 className="text-3xl font-bold mb-8">All Products</h1>

      <div className="flex gap-8">
        {/* Sidebar */}
        <aside className="w-64 shrink-0">
          <CategoryFilter
            categories={data.categories}
            selected={searchParams.category}
          />
        </aside>

        {/* Main content */}
        <main className="flex-1">
          <div className="flex justify-between items-center mb-6">
            <p className="text-gray-600">
              {data.products.totalCount} products
            </p>
            <SortDropdown selected={searchParams.sort} />
          </div>

          <ProductGrid products={data.products.edges.map(e => e.node)} />

          {/* Pagination */}
          {data.products.pageInfo.hasNextPage && (
            <div className="mt-8 text-center">
              <a
                href={`?page=${data.products.pageInfo.endCursor}`}
                className="inline-block bg-blue-600 text-white px-6 py-2 rounded"
              >
                Load More
              </a>
            </div>
          )}
        </main>
      </div>
    </div>
  );
}

Step 2: Create Product Detail Page

Create app/products/[slug]/page.tsx:

import { serverClient } from '@/lib/api-client';
import { ProductImages } from '@/components/products/product-images';
import { ProductInfo } from '@/components/products/product-info';
import { AddToCartButton } from '@/components/cart/add-to-cart-button';
import { gql } from 'graphql-request';
import { notFound } from 'next/navigation';

const GET_PRODUCT = gql`
  query GetProduct($slug: String!) {
    productBySlug(slug: $slug) {
      id
      name
      slug
      description
      longDescription
      price
      compareAtPrice
      currency
      images {
        id
        url
        alt
        order
      }
      variants {
        id
        name
        sku
        price
        inventory {
          available
          reserved
        }
        attributes {
          name
          value
        }
      }
      category {
        id
        name
        slug
      }
      inventory {
        available
        reserved
        warehouse {
          name
          location
        }
      }
      metadata
      seoTitle
      seoDescription

      # Federated fields
      reviews {
        rating
        count
      }
      relatedProducts(first: 4) {
        id
        name
        slug
        price
        images(first: 1) {
          url
          alt
        }
      }
    }
  }
`;

export async function generateMetadata({ params }: { params: { slug: string } }) {
  const { data } = await serverClient.request(GET_PRODUCT, {
    slug: params.slug,
  });

  if (!data?.productBySlug) {
    return {};
  }

  return {
    title: data.productBySlug.seoTitle || data.productBySlug.name,
    description: data.productBySlug.seoDescription || data.productBySlug.description,
    openGraph: {
      images: data.productBySlug.images[0]?.url,
    },
  };
}

export default async function ProductPage({
  params,
}: {
  params: { slug: string };
}) {
  const { data } = await serverClient.request(GET_PRODUCT, {
    slug: params.slug,
  });

  if (!data?.productBySlug) {
    notFound();
  }

  const product = data.productBySlug;

  return (
    <div className="container mx-auto px-4 py-8">
      <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
        {/* Images */}
        <ProductImages images={product.images} />

        {/* Product Info */}
        <div className="space-y-6">
          <ProductInfo product={product} />

          {/* Add to Cart */}
          <AddToCartButton
            productId={product.id}
            variants={product.variants}
            inventory={product.inventory}
          />

          {/* Additional Info */}
          <div className="border-t pt-6">
            <h3 className="font-semibold mb-2">Description</h3>
            <div
              className="prose prose-sm"
              dangerouslySetInnerHTML={{
                __html: product.longDescription || product.description,
              }}
            />
          </div>
        </div>
      </div>

      {/* Related Products */}
      {product.relatedProducts.length > 0 && (
        <div className="mt-16">
          <h2 className="text-2xl font-bold mb-6">Related Products</h2>
          <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
            {product.relatedProducts.map((related) => (
              <a
                key={related.id}
                href={`/products/${related.slug}`}
                className="group"
              >
                <img
                  src={related.images[0]?.url}
                  alt={related.images[0]?.alt}
                  className="w-full h-64 object-cover rounded"
                />
                <h3 className="mt-2 font-medium group-hover:text-blue-600">
                  {related.name}
                </h3>
                <p className="text-gray-600">
                  ${(related.price / 100).toFixed(2)}
                </p>
              </a>
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

Shopping Cart

Step 1: Create Cart Store

Create stores/cart-store.ts:

import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { sdk } from '@/lib/api-client';

interface CartItem {
  productId: string;
  variantId?: string;
  quantity: number;
  product?: any; // Product details
}

interface CartStore {
  items: CartItem[];
  isLoading: boolean;
  cartId: string | null;

  // Actions
  addItem: (productId: string, variantId?: string, quantity?: number) => Promise<void>;
  updateQuantity: (productId: string, quantity: number) => Promise<void>;
  removeItem: (productId: string) => Promise<void>;
  clearCart: () => Promise<void>;
  syncCart: () => Promise<void>;

  // Computed
  get itemCount(): number;
  get subtotal(): number;
}

export const useCartStore = create<CartStore>()(
  persist(
    (set, get) => ({
      items: [],
      isLoading: false,
      cartId: null,

      addItem: async (productId, variantId, quantity = 1) => {
        set({ isLoading: true });

        try {
          // Check if item already exists
          const existingItem = get().items.find(
            item => item.productId === productId && item.variantId === variantId
          );

          if (existingItem) {
            // Update quantity
            await get().updateQuantity(productId, existingItem.quantity + quantity);
            return;
          }

          // Create or update cart
          const { data } = await sdk.addToCart({
            input: {
              cartId: get().cartId,
              productId,
              variantId,
              quantity,
            }
          });

          if (data?.addToCart?.cart) {
            set({
              cartId: data.addToCart.cart.id,
              items: data.addToCart.cart.items,
            });
          }
        } catch (error) {
          console.error('Failed to add item to cart:', error);
          throw error;
        } finally {
          set({ isLoading: false });
        }
      },

      updateQuantity: async (productId, quantity) => {
        if (quantity <= 0) {
          return get().removeItem(productId);
        }

        set({ isLoading: true });

        try {
          const { data } = await sdk.updateCartItem({
            cartId: get().cartId!,
            productId,
            quantity,
          });

          if (data?.updateCartItem?.cart) {
            set({ items: data.updateCartItem.cart.items });
          }
        } catch (error) {
          console.error('Failed to update quantity:', error);
          throw error;
        } finally {
          set({ isLoading: false });
        }
      },

      removeItem: async (productId) => {
        set({ isLoading: true });

        try {
          const { data } = await sdk.removeFromCart({
            cartId: get().cartId!,
            productId,
          });

          if (data?.removeFromCart?.cart) {
            set({ items: data.removeFromCart.cart.items });
          }
        } catch (error) {
          console.error('Failed to remove item:', error);
          throw error;
        } finally {
          set({ isLoading: false });
        }
      },

      clearCart: async () => {
        set({ isLoading: true });

        try {
          await sdk.clearCart({ cartId: get().cartId! });
          set({ items: [], cartId: null });
        } catch (error) {
          console.error('Failed to clear cart:', error);
          throw error;
        } finally {
          set({ isLoading: false });
        }
      },

      syncCart: async () => {
        const cartId = get().cartId;
        if (!cartId) return;

        try {
          const { data } = await sdk.getCart({ id: cartId });

          if (data?.cart) {
            set({ items: data.cart.items });
          } else {
            // Cart no longer exists
            set({ items: [], cartId: null });
          }
        } catch (error) {
          console.error('Failed to sync cart:', error);
        }
      },

      get itemCount() {
        return get().items.reduce((sum, item) => sum + item.quantity, 0);
      },

      get subtotal() {
        return get().items.reduce(
          (sum, item) => sum + (item.product?.price || 0) * item.quantity,
          0
        );
      },
    }),
    {
      name: 'cart-storage',
      partialize: (state) => ({ cartId: state.cartId }),
    }
  )
);

Step 2: Create Cart Components

Create components/cart/cart-drawer.tsx:

'use client';

import { Fragment } from 'react';
import { Dialog, Transition } from '@headlessui/react';
import { XMarkIcon } from '@heroicons/react/24/outline';
import { useCartStore } from '@/stores/cart-store';
import { CartItem } from './cart-item';

interface CartDrawerProps {
  isOpen: boolean;
  onClose: () => void;
}

export function CartDrawer({ isOpen, onClose }: CartDrawerProps) {
  const { items, itemCount, subtotal, clearCart } = useCartStore();

  return (
    <Transition.Root show={isOpen} as={Fragment}>
      <Dialog as="div" className="relative z-50" onClose={onClose}>
        <Transition.Child
          as={Fragment}
          enter="ease-in-out duration-300"
          enterFrom="opacity-0"
          enterTo="opacity-100"
          leave="ease-in-out duration-300"
          leaveFrom="opacity-100"
          leaveTo="opacity-0"
        >
          <div className="fixed inset-0 bg-gray-500 bg-opacity-75" />
        </Transition.Child>

        <div className="fixed inset-0 overflow-hidden">
          <div className="absolute inset-0 overflow-hidden">
            <div className="pointer-events-none fixed inset-y-0 right-0 flex max-w-full pl-10">
              <Transition.Child
                as={Fragment}
                enter="transform transition ease-in-out duration-300"
                enterFrom="translate-x-full"
                enterTo="translate-x-0"
                leave="transform transition ease-in-out duration-300"
                leaveFrom="translate-x-0"
                leaveTo="translate-x-full"
              >
                <Dialog.Panel className="pointer-events-auto w-screen max-w-md">
                  <div className="flex h-full flex-col bg-white shadow-xl">
                    {/* Header */}
                    <div className="flex-1 overflow-y-auto px-4 py-6 sm:px-6">
                      <div className="flex items-start justify-between">
                        <Dialog.Title className="text-lg font-medium">
                          Shopping Cart ({itemCount})
                        </Dialog.Title>
                        <button
                          type="button"
                          className="ml-3 flex h-7 w-7 items-center justify-center"
                          onClick={onClose}
                        >
                          <XMarkIcon className="h-6 w-6" />
                        </button>
                      </div>

                      {/* Cart Items */}
                      <div className="mt-8">
                        {items.length === 0 ? (
                          <p className="text-center text-gray-500">
                            Your cart is empty
                          </p>
                        ) : (
                          <div className="flow-root">
                            <ul className="-my-6 divide-y divide-gray-200">
                              {items.map((item) => (
                                <CartItem key={item.productId} item={item} />
                              ))}
                            </ul>
                          </div>
                        )}
                      </div>
                    </div>

                    {/* Footer */}
                    {items.length > 0 && (
                      <div className="border-t border-gray-200 px-4 py-6 sm:px-6">
                        <div className="flex justify-between text-base font-medium">
                          <p>Subtotal</p>
                          <p>${(subtotal / 100).toFixed(2)}</p>
                        </div>
                        <p className="mt-0.5 text-sm text-gray-500">
                          Shipping and taxes calculated at checkout.
                        </p>
                        <div className="mt-6">
                          <a
                            href="/checkout"
                            className="flex items-center justify-center rounded-md bg-blue-600 px-6 py-3 text-white hover:bg-blue-700"
                          >
                            Checkout
                          </a>
                        </div>
                        <div className="mt-6 flex justify-center text-center text-sm">
                          <p>
                            or{' '}
                            <button
                              type="button"
                              className="font-medium text-blue-600 hover:text-blue-500"
                              onClick={onClose}
                            >
                              Continue Shopping
                            </button>
                          </p>
                        </div>
                      </div>
                    )}
                  </div>
                </Dialog.Panel>
              </Transition.Child>
            </div>
          </div>
        </div>
      </Dialog>
    </Transition.Root>
  );
}

Checkout Flow

Step 1: Create Checkout Page

Create app/checkout/page.tsx:

'use client';

import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { useCartStore } from '@/stores/cart-store';
import { useAuth } from '@/contexts/auth-context';
import { CheckoutForm } from '@/components/checkout/checkout-form';
import { OrderSummary } from '@/components/checkout/order-summary';
import { PaymentForm } from '@/components/checkout/payment-form';
import { sdk } from '@/lib/api-client';

export default function CheckoutPage() {
  const router = useRouter();
  const { items, cartId, clearCart } = useCartStore();
  const { user } = useAuth();

  const [step, setStep] = useState(1);
  const [isProcessing, setIsProcessing] = useState(false);
  const [shippingData, setShippingData] = useState<any>(null);
  const [error, setError] = useState('');

  // Redirect if cart is empty
  if (items.length === 0) {
    router.push('/cart');
    return null;
  }

  const handleShippingSubmit = async (data: any) => {
    setShippingData(data);
    setStep(2);
  };

  const handlePaymentSubmit = async (paymentMethod: any) => {
    setIsProcessing(true);
    setError('');

    try {
      // Create order
      const { data: orderData } = await sdk.createOrder({
        input: {
          cartId,
          shippingAddress: shippingData.shippingAddress,
          billingAddress: shippingData.billingAddress || shippingData.shippingAddress,
          shippingMethodId: shippingData.shippingMethodId,
          paymentMethodId: paymentMethod.id,
          notes: shippingData.notes,
        }
      });

      if (orderData?.createOrder?.order) {
        // Process payment
        const { data: paymentData } = await sdk.processPayment({
          orderId: orderData.createOrder.order.id,
          paymentMethodId: paymentMethod.id,
        });

        if (paymentData?.processPayment?.success) {
          // Clear cart and redirect to success page
          await clearCart();
          router.push(`/orders/${orderData.createOrder.order.id}/success`);
        } else {
          throw new Error(paymentData?.processPayment?.error || 'Payment failed');
        }
      } else {
        throw new Error(orderData?.createOrder?.errors?.[0]?.message || 'Order creation failed');
      }
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Checkout failed');
      setIsProcessing(false);
    }
  };

  return (
    <div className="container mx-auto px-4 py-8">
      <h1 className="text-3xl font-bold mb-8">Checkout</h1>

      {error && (
        <div className="bg-red-50 text-red-600 p-4 rounded mb-6">
          {error}
        </div>
      )}

      <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
        <div className="lg:col-span-2">
          {/* Progress Steps */}
          <div className="flex items-center mb-8">
            <div className={`flex items-center ${step >= 1 ? 'text-blue-600' : 'text-gray-400'}`}>
              <span className="rounded-full h-8 w-8 flex items-center justify-center border-2 border-current">
                1
              </span>
              <span className="ml-2">Shipping</span>
            </div>
            <div className="flex-1 h-0.5 bg-gray-300 mx-4" />
            <div className={`flex items-center ${step >= 2 ? 'text-blue-600' : 'text-gray-400'}`}>
              <span className="rounded-full h-8 w-8 flex items-center justify-center border-2 border-current">
                2
              </span>
              <span className="ml-2">Payment</span>
            </div>
          </div>

          {/* Step Content */}
          {step === 1 && (
            <CheckoutForm
              user={user}
              onSubmit={handleShippingSubmit}
              isGuest={!user}
            />
          )}

          {step === 2 && (
            <PaymentForm
              amount={useCartStore.getState().subtotal}
              onSubmit={handlePaymentSubmit}
              onBack={() => setStep(1)}
              isProcessing={isProcessing}
            />
          )}
        </div>

        {/* Order Summary */}
        <div className="lg:col-span-1">
          <OrderSummary items={items} />
        </div>
      </div>
    </div>
  );
}

Step 2: Create Payment Integration

Create components/checkout/payment-form.tsx:

'use client';

import { useState, useEffect } from 'react';
import { loadStripe } from '@stripe/stripe-js';
import {
  Elements,
  CardElement,
  useStripe,
  useElements,
} from '@stripe/react-stripe-js';
import { sdk } from '@/lib/api-client';

const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY!);

interface PaymentFormProps {
  amount: number;
  onSubmit: (paymentMethod: any) => void;
  onBack: () => void;
  isProcessing: boolean;
}

function PaymentFormContent({ amount, onSubmit, onBack, isProcessing }: PaymentFormProps) {
  const stripe = useStripe();
  const elements = useElements();
  const [error, setError] = useState('');
  const [clientSecret, setClientSecret] = useState('');

  useEffect(() => {
    // Create payment intent
    createPaymentIntent();
  }, [amount]);

  const createPaymentIntent = async () => {
    try {
      const { data } = await sdk.createPaymentIntent({
        amount,
        currency: 'USD',
      });

      if (data?.createPaymentIntent?.clientSecret) {
        setClientSecret(data.createPaymentIntent.clientSecret);
      }
    } catch (err) {
      setError('Failed to initialize payment');
    }
  };

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    if (!stripe || !elements || !clientSecret) {
      return;
    }

    setError('');

    const card = elements.getElement(CardElement);
    if (!card) return;

    const { error, paymentMethod } = await stripe.createPaymentMethod({
      type: 'card',
      card,
    });

    if (error) {
      setError(error.message || 'Payment failed');
      return;
    }

    // Confirm payment
    const { error: confirmError } = await stripe.confirmCardPayment(
      clientSecret,
      {
        payment_method: paymentMethod.id,
      }
    );

    if (confirmError) {
      setError(confirmError.message || 'Payment confirmation failed');
      return;
    }

    onSubmit(paymentMethod);
  };

  return (
    <form onSubmit={handleSubmit} className="space-y-6">
      <div>
        <h2 className="text-xl font-semibold mb-4">Payment Information</h2>

        {error && (
          <div className="bg-red-50 text-red-600 p-3 rounded mb-4">
            {error}
          </div>
        )}

        <div className="border rounded p-4">
          <CardElement
            options={{
              style: {
                base: {
                  fontSize: '16px',
                  color: '#424770',
                  '::placeholder': {
                    color: '#aab7c4',
                  },
                },
              },
            }}
          />
        </div>
      </div>

      <div className="flex gap-4">
        <button
          type="button"
          onClick={onBack}
          className="flex-1 border border-gray-300 py-2 px-4 rounded"
          disabled={isProcessing}
        >
          Back
        </button>
        <button
          type="submit"
          disabled={!stripe || isProcessing}
          className="flex-1 bg-blue-600 text-white py-2 px-4 rounded hover:bg-blue-700 disabled:opacity-50"
        >
          {isProcessing ? 'Processing...' : `Pay $${(amount / 100).toFixed(2)}`}
        </button>
      </div>
    </form>
  );
}

export function PaymentForm(props: PaymentFormProps) {
  return (
    <Elements stripe={stripePromise}>
      <PaymentFormContent {...props} />
    </Elements>
  );
}

Performance Optimization

Step 1: Implement Caching Strategy

Create lib/cache.ts:

import { unstable_cache } from 'next/cache';
import { serverClient } from './api-client';

// Cache product queries for 5 minutes
export const getCachedProducts = unstable_cache(
  async (params: any) => {
    return serverClient.request(GET_PRODUCTS_QUERY, params);
  },
  ['products'],
  {
    revalidate: 300, // 5 minutes
    tags: ['products'],
  }
);

// Cache individual product for 10 minutes
export const getCachedProduct = unstable_cache(
  async (slug: string) => {
    return serverClient.request(GET_PRODUCT_QUERY, { slug });
  },
  ['product'],
  {
    revalidate: 600, // 10 minutes
    tags: ['product'],
  }
);

// Invalidate cache on mutation
export async function revalidateProducts() {
  revalidateTag('products');
}

export async function revalidateProduct(slug: string) {
  revalidateTag(`product-${slug}`);
}

Step 2: Optimize Images

Create components/optimized-image.tsx:

'use client';

import Image from 'next/image';
import { useState } from 'react';

interface OptimizedImageProps {
  src: string;
  alt: string;
  width?: number;
  height?: number;
  priority?: boolean;
  className?: string;
  sizes?: string;
}

export function OptimizedImage({
  src,
  alt,
  width = 800,
  height = 600,
  priority = false,
  className = '',
  sizes = '100vw',
}: OptimizedImageProps) {
  const [isLoading, setIsLoading] = useState(true);

  // Use image optimization service
  const optimizedSrc = src.includes('unifiedcommerce.app')
    ? `${src}?w=${width}&h=${height}&q=75&fm=webp`
    : src;

  return (
    <div className={`relative overflow-hidden ${className}`}>
      <Image
        src={optimizedSrc}
        alt={alt}
        width={width}
        height={height}
        priority={priority}
        sizes={sizes}
        className={`
          duration-700 ease-in-out
          ${isLoading ? 'scale-110 blur-lg' : 'scale-100 blur-0'}
        `}
        onLoadingComplete={() => setIsLoading(false)}
        placeholder="blur"
        blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQ..."
      />
    </div>
  );
}

Step 3: Implement Lazy Loading

Create hooks/use-intersection-observer.ts:

import { useEffect, useRef, useState } from 'react';

interface UseIntersectionObserverProps {
  threshold?: number;
  root?: Element | null;
  rootMargin?: string;
}

export function useIntersectionObserver({
  threshold = 0.1,
  root = null,
  rootMargin = '0px',
}: UseIntersectionObserverProps = {}) {
  const [entry, setEntry] = useState<IntersectionObserverEntry>();
  const [node, setNode] = useState<Element | null>(null);

  const observer = useRef<IntersectionObserver | null>(null);

  useEffect(() => {
    if (observer.current) observer.current.disconnect();

    observer.current = new IntersectionObserver(
      ([entry]) => setEntry(entry),
      { threshold, root, rootMargin }
    );

    if (node) observer.current.observe(node);

    return () => {
      if (observer.current) observer.current.disconnect();
    };
  }, [node, threshold, root, rootMargin]);

  return { setNode, entry, isIntersecting: entry?.isIntersecting };
}

// Usage in component
export function LazyProductGrid({ products }: { products: Product[] }) {
  const { setNode, isIntersecting } = useIntersectionObserver();
  const [isLoaded, setIsLoaded] = useState(false);

  useEffect(() => {
    if (isIntersecting && !isLoaded) {
      setIsLoaded(true);
    }
  }, [isIntersecting, isLoaded]);

  return (
    <div ref={setNode}>
      {isLoaded ? (
        <ProductGrid products={products} />
      ) : (
        <div className="h-96 bg-gray-100 animate-pulse" />
      )}
    </div>
  );
}

Troubleshooting

Common Issues and Solutions

1. Authentication Errors

Problem: Getting 401 Unauthorized errors Solution:

// Check if token is expired
const checkTokenExpiry = (token: string) => {
  try {
    const payload = JSON.parse(atob(token.split('.')[1]));
    return payload.exp * 1000 > Date.now();
  } catch {
    return false;
  }
};

// Refresh token if expired
if (!checkTokenExpiry(token)) {
  await refreshToken();
}

2. Cart Persistence Issues

Problem: Cart items lost on refresh Solution:

// Sync cart on app mount
useEffect(() => {
  const syncCartOnMount = async () => {
    const cartId = localStorage.getItem('cartId');
    if (cartId) {
      await cartStore.syncCart();
    }
  };

  syncCartOnMount();
}, []);

3. Slow Product Loading

Problem: Product pages loading slowly Solution:

// Implement parallel data fetching
const [products, categories, featured] = await Promise.all([
  getCachedProducts({ first: 20 }),
  getCachedCategories(),
  getFeaturedProducts(),
]);

// Use React Suspense
<Suspense fallback={<ProductSkeleton />}>
  <ProductGrid products={products} />
</Suspense>

4. Payment Failures

Problem: Stripe payment failing Solution:

// Add comprehensive error handling
try {
  const result = await stripe.confirmCardPayment(clientSecret);

  if (result.error) {
    switch (result.error.code) {
      case 'card_declined':
        showError('Your card was declined. Please try another card.');
        break;
      case 'insufficient_funds':
        showError('Insufficient funds. Please try another payment method.');
        break;
      default:
        showError(result.error.message || 'Payment failed');
    }
  }
} catch (error) {
  console.error('Payment error:', error);
  showError('An unexpected error occurred. Please try again.');
}

5. SEO Issues

Problem: Poor SEO performance Solution:

// Add structured data
export function generateJsonLd(product: Product) {
  return {
    '@context': 'https://schema.org',
    '@type': 'Product',
    name: product.name,
    description: product.description,
    image: product.images[0]?.url,
    offers: {
      '@type': 'Offer',
      price: (product.price / 100).toFixed(2),
      priceCurrency: product.currency,
      availability: product.inventory.available > 0
        ? 'https://schema.org/InStock'
        : 'https://schema.org/OutOfStock',
    },
  };
}

// Add to page
<script
  type="application/ld+json"
  dangerouslySetInnerHTML={{
    __html: JSON.stringify(generateJsonLd(product)),
  }}
/>

Deployment

Step 1: Environment Setup

Create .env.production:

NEXT_PUBLIC_UC_ENDPOINT=https://gateway.unifiedcommerce.app/graphql
NEXT_PUBLIC_UC_API_KEY=your_production_api_key
NEXT_PUBLIC_UC_MERCHANT_ID=merchant_prod_123
NEXT_PUBLIC_STRIPE_PUBLIC_KEY=pk_live_...
STRIPE_SECRET_KEY=sk_live_...
NEXT_PUBLIC_SITE_URL=https://yourdomain.com

Step 2: Build Optimization

Update next.config.js:

/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    domains: ['cdn.unifiedcommerce.app'],
    formats: ['image/avif', 'image/webp'],
  },
  experimental: {
    optimizeCss: true,
  },
  compress: true,
  poweredByHeader: false,

  // Enable SWC minification
  swcMinify: true,

  // Bundle analyzer
  webpack: (config, { isServer }) => {
    if (!isServer) {
      config.resolve.alias = {
        ...config.resolve.alias,
        '@sentry/node': '@sentry/browser',
      };
    }
    return config;
  },
};

module.exports = nextConfig;

Step 3: Deploy to Vercel

# Install Vercel CLI
npm i -g vercel

# Deploy to production
vercel --prod

# Set environment variables
vercel env add NEXT_PUBLIC_UC_API_KEY production
vercel env add STRIPE_SECRET_KEY production

Best Practices

  1. Error Boundaries: Implement error boundaries for graceful failures
  2. Loading States: Always show loading indicators for async operations
  3. Optimistic Updates: Update UI immediately for better UX
  4. Image Optimization: Use Next.js Image component with proper sizes
  5. Code Splitting: Lazy load components and routes
  6. API Caching: Cache API responses appropriately
  7. Security: Never expose sensitive keys in client code
  8. Analytics: Implement tracking for user behavior
  9. A/B Testing: Use feature flags for gradual rollouts
  10. Monitoring: Set up error tracking and performance monitoring

Next Steps

  • Implement advanced features (wishlists, reviews, recommendations)
  • Add internationalization (i18n) support
  • Implement Progressive Web App (PWA) features
  • Add real-time inventory updates via WebSockets
  • Implement advanced search with Algolia/Elasticsearch
  • Add social commerce features
  • Implement subscription/recurring payments
  • Add multi-vendor marketplace capabilities

Resources

Was this page helpful?