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.