E-commerce Store

Build a complete online store with products, cart, and checkout

This guide walks through building a complete e-commerce store with the Arky SDK.

Overview

You’ll learn how to:

  • Display products with categories
  • Build a shopping cart
  • Process checkout with Stripe
  • Handle order confirmation

Setup

Initialize the SDK with your business ID:

import { createArkyClient } from 'arky-sdk';

const sdk = createArkyClient({
  businessId: process.env.ARKY_BUSINESS_ID!,
  getToken: () => localStorage.getItem('arky_token'),
  setToken: (token) => localStorage.setItem('arky_token', token),
});

Product Listing

Fetch Products

async function getProducts(options?: {
  category?: string;
  search?: string;
  cursor?: string;
}) {
  const result = await sdk.eshop.getProducts({
    businessId: sdk.config.businessId,
    status: 'ACTIVE',
    category: options?.category,
    search: options?.search,
    sortBy: 'createdAt',
    sortOrder: 'desc',
    cursor: options?.cursor,
    limit: 24
  });

  if (result.ok) {
    return {
      products: result.val.items,
      nextCursor: result.val.cursor
    };
  }

  return { products: [], nextCursor: null };
}

Product Card Component

function ProductCard({ product }) {
  const formatPrice = (cents: number) =>
    new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: 'USD'
    }).format(cents / 100);

  return (
    <a href={`/products/${product.slug}`} className="product-card">
      <img
        src={`${product.images[0]}?w=400&h=400&fit=cover`}
        alt={product.name}
      />
      <h3>{product.name}</h3>
      <div className="price">
        {product.compareAtPrice && (
          <span className="original">
            {formatPrice(product.compareAtPrice)}
          </span>
        )}
        <span className="current">{formatPrice(product.price)}</span>
      </div>
      {product.inventory === 0 && (
        <span className="out-of-stock">Out of Stock</span>
      )}
    </a>
  );
}

Product Detail Page

async function getProduct(slug: string) {
  const result = await sdk.eshop.getProduct({
    businessId: sdk.config.businessId,
    slug
  });

  return result.ok ? result.val : null;
}
function ProductPage({ product }) {
  const [quantity, setQuantity] = useState(1);

  const addToCart = () => {
    cartStore.addItem({
      productId: product.id,
      name: product.name,
      price: product.price,
      image: product.images[0],
      quantity
    });
  };

  return (
    <div className="product-detail">
      <div className="gallery">
        {product.images.map((img, i) => (
          <img key={i} src={`${img}?w=800`} alt={product.name} />
        ))}
      </div>

      <div className="info">
        <h1>{product.name}</h1>
        <p className="price">{formatPrice(product.price)}</p>
        <div className="description">{product.description}</div>

        <div className="quantity">
          <button onClick={() => setQuantity(q => Math.max(1, q - 1))}>-</button>
          <span>{quantity}</span>
          <button onClick={() => setQuantity(q => q + 1)}>+</button>
        </div>

        <button
          onClick={addToCart}
          disabled={product.inventory === 0}
        >
          {product.inventory === 0 ? 'Out of Stock' : 'Add to Cart'}
        </button>
      </div>
    </div>
  );
}

Shopping Cart

Cart Store

import { writable, derived } from 'svelte/store';

interface CartItem {
  productId: string;
  name: string;
  price: number;
  image: string;
  quantity: number;
}

function createCartStore() {
  const items = writable<CartItem[]>([]);

  // Load from localStorage
  if (typeof window !== 'undefined') {
    const saved = localStorage.getItem('cart');
    if (saved) items.set(JSON.parse(saved));
  }

  // Save on change
  items.subscribe(value => {
    if (typeof window !== 'undefined') {
      localStorage.setItem('cart', JSON.stringify(value));
    }
  });

  return {
    subscribe: items.subscribe,

    addItem(item: CartItem) {
      items.update(current => {
        const existing = current.find(i => i.productId === item.productId);
        if (existing) {
          existing.quantity += item.quantity;
          return [...current];
        }
        return [...current, item];
      });
    },

    updateQuantity(productId: string, quantity: number) {
      items.update(current =>
        current.map(item =>
          item.productId === productId ? { ...item, quantity } : item
        )
      );
    },

    removeItem(productId: string) {
      items.update(current =>
        current.filter(item => item.productId !== productId)
      );
    },

    clear() {
      items.set([]);
    }
  };
}

export const cart = createCartStore();

export const cartTotal = derived(cart, $cart =>
  $cart.reduce((sum, item) => sum + item.price * item.quantity, 0)
);

export const cartCount = derived(cart, $cart =>
  $cart.reduce((sum, item) => sum + item.quantity, 0)
);

Cart Page

function CartPage() {
  const items = useStore(cart);
  const total = useStore(cartTotal);

  if (items.length === 0) {
    return (
      <div className="empty-cart">
        <h2>Your cart is empty</h2>
        <a href="/products">Continue Shopping</a>
      </div>
    );
  }

  return (
    <div className="cart-page">
      <h1>Shopping Cart</h1>

      <div className="cart-items">
        {items.map(item => (
          <div key={item.productId} className="cart-item">
            <img src={`${item.image}?w=100`} alt={item.name} />
            <div className="details">
              <h3>{item.name}</h3>
              <p>{formatPrice(item.price)}</p>
            </div>
            <div className="quantity">
              <button onClick={() => {
                if (item.quantity === 1) {
                  cart.removeItem(item.productId);
                } else {
                  cart.updateQuantity(item.productId, item.quantity - 1);
                }
              }}>-</button>
              <span>{item.quantity}</span>
              <button onClick={() =>
                cart.updateQuantity(item.productId, item.quantity + 1)
              }>+</button>
            </div>
            <p className="subtotal">
              {formatPrice(item.price * item.quantity)}
            </p>
            <button onClick={() => cart.removeItem(item.productId)}>
              Remove
            </button>
          </div>
        ))}
      </div>

      <div className="cart-summary">
        <p className="total">Total: {formatPrice(total)}</p>
        <a href="/checkout" className="checkout-btn">
          Proceed to Checkout
        </a>
      </div>
    </div>
  );
}

Checkout

Get Quote with Promo Code

async function getQuote(promoCode?: string) {
  const items = get(cart);

  const result = await sdk.eshop.getQuote({
    businessId: sdk.config.businessId,
    items: items.map(item => ({
      productId: item.productId,
      quantity: item.quantity
    })),
    promoCode
  });

  if (result.ok) {
    return result.val;
  }

  throw new Error(result.val.message);
}

Checkout Form

import { loadStripe } from '@stripe/stripe-js';
import { Elements, CardElement, useStripe, useElements } from '@stripe/react-stripe-js';

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

function CheckoutPage() {
  return (
    <Elements stripe={stripePromise}>
      <CheckoutForm />
    </Elements>
  );
}

function CheckoutForm() {
  const stripe = useStripe();
  const elements = useElements();
  const items = useStore(cart);

  const [loading, setLoading] = useState(false);
  const [promoCode, setPromoCode] = useState('');
  const [quote, setQuote] = useState(null);
  const [customer, setCustomer] = useState({
    email: '',
    firstName: '',
    lastName: ''
  });
  const [address, setAddress] = useState({
    line1: '',
    city: '',
    state: '',
    postalCode: '',
    country: 'US'
  });

  // Fetch quote on load and promo code change
  useEffect(() => {
    getQuote(promoCode).then(setQuote).catch(console.error);
  }, [promoCode]);

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    if (!stripe || !elements) return;

    setLoading(true);

    try {
      // 1. Create order
      const orderResult = await sdk.eshop.createOrder({
        businessId: sdk.config.businessId,
        items: items.map(item => ({
          productId: item.productId,
          quantity: item.quantity
        })),
        customer,
        shippingAddress: address,
        promoCode: promoCode || undefined
      });

      if (!orderResult.ok) {
        throw new Error(orderResult.val.message);
      }

      // 2. Create payment method
      const card = elements.getElement(CardElement)!;
      const { paymentMethod, error } = await stripe.createPaymentMethod({
        type: 'card',
        card,
        billing_details: {
          email: customer.email,
          name: `${customer.firstName} ${customer.lastName}`
        }
      });

      if (error) throw new Error(error.message);

      // 3. Process checkout
      const checkoutResult = await sdk.eshop.checkout({
        businessId: sdk.config.businessId,
        orderId: orderResult.val.id,
        paymentMethod: 'stripe',
        paymentMethodId: paymentMethod.id,
        returnUrl: `${window.location.origin}/order/complete`
      });

      if (!checkoutResult.ok) {
        throw new Error(checkoutResult.val.message);
      }

      // 4. Handle 3D Secure if needed
      if (checkoutResult.val.requiresAction) {
        const { error } = await stripe.confirmCardPayment(
          checkoutResult.val.clientSecret
        );
        if (error) throw new Error(error.message);
      }

      // 5. Success! Clear cart and redirect
      cart.clear();
      window.location.href = `/order/success?id=${orderResult.val.id}`;

    } catch (err) {
      alert(err.message);
    } finally {
      setLoading(false);
    }
  }

  return (
    <form onSubmit={handleSubmit} className="checkout-form">
      <section className="customer-info">
        <h2>Contact Information</h2>
        <input
          type="email"
          placeholder="Email"
          value={customer.email}
          onChange={e => setCustomer({ ...customer, email: e.target.value })}
          required
        />
        <div className="row">
          <input
            placeholder="First Name"
            value={customer.firstName}
            onChange={e => setCustomer({ ...customer, firstName: e.target.value })}
            required
          />
          <input
            placeholder="Last Name"
            value={customer.lastName}
            onChange={e => setCustomer({ ...customer, lastName: e.target.value })}
            required
          />
        </div>
      </section>

      <section className="shipping">
        <h2>Shipping Address</h2>
        <input
          placeholder="Address"
          value={address.line1}
          onChange={e => setAddress({ ...address, line1: e.target.value })}
          required
        />
        <div className="row">
          <input
            placeholder="City"
            value={address.city}
            onChange={e => setAddress({ ...address, city: e.target.value })}
            required
          />
          <input
            placeholder="State"
            value={address.state}
            onChange={e => setAddress({ ...address, state: e.target.value })}
            required
          />
          <input
            placeholder="ZIP"
            value={address.postalCode}
            onChange={e => setAddress({ ...address, postalCode: e.target.value })}
            required
          />
        </div>
      </section>

      <section className="payment">
        <h2>Payment</h2>
        <CardElement options={{
          style: {
            base: {
              fontSize: '16px',
              color: '#424770'
            }
          }
        }} />
      </section>

      <section className="promo">
        <input
          placeholder="Promo code"
          value={promoCode}
          onChange={e => setPromoCode(e.target.value)}
        />
      </section>

      {quote && (
        <section className="summary">
          <div className="line">
            <span>Subtotal</span>
            <span>{formatPrice(quote.subtotal)}</span>
          </div>
          {quote.discount > 0 && (
            <div className="line discount">
              <span>Discount</span>
              <span>-{formatPrice(quote.discount)}</span>
            </div>
          )}
          <div className="line">
            <span>Shipping</span>
            <span>{formatPrice(quote.shipping)}</span>
          </div>
          <div className="line">
            <span>Tax</span>
            <span>{formatPrice(quote.tax)}</span>
          </div>
          <div className="line total">
            <span>Total</span>
            <span>{formatPrice(quote.total)}</span>
          </div>
        </section>
      )}

      <button type="submit" disabled={loading || !stripe}>
        {loading ? 'Processing...' : `Pay ${formatPrice(quote?.total || 0)}`}
      </button>
    </form>
  );
}

Order Confirmation

async function OrderSuccessPage({ orderId }) {
  const result = await sdk.eshop.getOrder({
    businessId: sdk.config.businessId,
    id: orderId
  });

  if (!result.ok) {
    return <div>Order not found</div>;
  }

  const order = result.val;

  return (
    <div className="order-success">
      <h1>Thank you for your order!</h1>
      <p>Order #{order.id}</p>

      <div className="order-items">
        {order.items.map(item => (
          <div key={item.id} className="item">
            <span>{item.name} x {item.quantity}</span>
            <span>{formatPrice(item.price * item.quantity)}</span>
          </div>
        ))}
      </div>

      <div className="order-total">
        <span>Total Paid</span>
        <span>{formatPrice(order.total)}</span>
      </div>

      <p>A confirmation email has been sent to {order.customer.email}</p>
    </div>
  );
}
Tip

Always use getQuote to show accurate pricing before checkout—it handles promo codes, taxes, and shipping calculations.