Skip to main content

Product Catalog Example

This guide demonstrates how to build a product catalog with pricing management using the Easy React SDK.

Overview

The Easy SDK provides comprehensive product and pricing management capabilities, allowing you to:

  • Create and manage products
  • Define multiple price points (one-time, recurring subscriptions)
  • Display product catalogs
  • Archive products and prices
  • Handle product metadata and tags

Complete Product Catalog

This example shows a full product catalog with pricing and purchase functionality.

import React, { useState, useEffect } from "react";
import { useEasy } from "@easylabs/react";
import type { ProductData, PriceData } from "@easylabs/react";

interface ProductWithPrices extends ProductData {
prices?: PriceData[];
}

function ProductCatalog() {
const { getProducts, getProductWithPrices, checkout } = useEasy();

const [products, setProducts] = useState<ProductWithPrices[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [selectedProduct, setSelectedProduct] =
useState<ProductWithPrices | null>(null);

// Load all products on mount
useEffect(() => {
loadProducts();
}, []);

const loadProducts = async () => {
setLoading(true);
setError(null);

try {
const result = await getProducts({ limit: 20, offset: 0 });

// Load prices for each product
const productsWithPrices = await Promise.all(
result.data.map(async (product) => {
try {
const productWithPrices = await getProductWithPrices(product.id);
return {
...product,
prices: productWithPrices.data.prices,
};
} catch (err) {
console.error(
`Failed to load prices for product ${product.id}`,
err,
);
return {
...product,
prices: [],
};
}
}),
);

setProducts(productsWithPrices);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to load products");
console.error("Error loading products:", err);
} finally {
setLoading(false);
}
};

const handlePurchase = async (productId: string, priceId: string) => {
setLoading(true);
setError(null);

try {
// In a real app, you'd collect customer and payment info
const result = await checkout({
customer_creation: true,
customer_details: {
first_name: "John",
last_name: "Doe",
email: "[email protected]",
},
source: "payment_instrument_id", // or new payment instrument
line_items: [
{
price_id: priceId,
quantity: 1,
},
],
});

console.log("Purchase successful:", result);
alert("Purchase successful!");
} catch (err) {
setError(err instanceof Error ? err.message : "Purchase failed");
console.error("Purchase error:", err);
} finally {
setLoading(false);
}
};

const formatPrice = (amount: number, currency: string) => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: currency.toUpperCase(),
}).format(amount / 100);
};

const getRecurringText = (price: PriceData) => {
if (!price.recurring) return null;

const { interval, interval_count } = price.recurring;
const period =
interval_count > 1 ? `${interval_count} ${interval}s` : interval;
return `per ${period}`;
};

if (loading && products.length === 0) {
return <div className="loading">Loading products...</div>;
}

if (error && products.length === 0) {
return <div className="error">Error: {error}</div>;
}

return (
<div className="product-catalog">
<h1>Product Catalog</h1>

{error && (
<div className="error-message" role="alert">
{error}
</div>
)}

<div className="product-grid">
{products.map((product) => (
<div key={product.id} className="product-card">
{product.images && product.images.length > 0 && (
<img
src={product.images[0]}
alt={product.name}
className="product-image"
/>
)}

<div className="product-info">
<h3>{product.name}</h3>
{product.description && (
<p className="product-description">{product.description}</p>
)}

{!product.active && (
<span className="badge inactive">Inactive</span>
)}

{product.prices && product.prices.length > 0 ? (
<div className="pricing-options">
{product.prices
.filter((price) => price.active)
.map((price) => (
<div key={price.id} className="price-option">
<div className="price-details">
<span className="price-amount">
{formatPrice(price.unit_amount, price.currency)}
</span>
{price.recurring && (
<span className="price-recurring">
{getRecurringText(price)}
</span>
)}
</div>
{price.description && (
<p className="price-description">
{price.description}
</p>
)}
<button
onClick={() => handlePurchase(product.id, price.id)}
disabled={loading || !product.active}
className="btn-purchase"
>
{loading ? "Processing..." : "Purchase"}
</button>
</div>
))}
</div>
) : (
<p className="no-pricing">No pricing available</p>
)}
</div>

<button
onClick={() => setSelectedProduct(product)}
className="btn-details"
>
View Details
</button>
</div>
))}
</div>

{/* Product Detail Modal */}
{selectedProduct && (
<div className="modal-overlay" onClick={() => setSelectedProduct(null)}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<button
className="modal-close"
onClick={() => setSelectedProduct(null)}
>
×
</button>
<h2>{selectedProduct.name}</h2>
<p>{selectedProduct.description}</p>
{selectedProduct.metadata && (
<div className="metadata">
<h4>Additional Information</h4>
<pre>{JSON.stringify(selectedProduct.metadata, null, 2)}</pre>
</div>
)}
</div>
</div>
)}
</div>
);
}

export default ProductCatalog;

Product Management Dashboard

An admin interface for creating and managing products and prices.

import React, { useState, useEffect } from "react";
import { useEasy } from "@easylabs/react";
import type {
ProductData,
PriceData,
CreateProduct,
CreatePrice,
} from "@easylabs/react";

function ProductManagementDashboard() {
const {
getProducts,
createProduct,
updateProduct,
archiveProduct,
createProductPrice,
updateProductPrice,
archiveProductPrice,
} = useEasy();

const [products, setProducts] = useState<ProductData[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [showCreateForm, setShowCreateForm] = useState(false);

useEffect(() => {
loadProducts();
}, []);

const loadProducts = async () => {
setLoading(true);
try {
const result = await getProducts({ limit: 50 });
setProducts(result.data);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to load products");
} finally {
setLoading(false);
}
};

const handleCreateProduct = async (productData: CreateProduct) => {
setLoading(true);
setError(null);

try {
await createProduct(productData);
await loadProducts(); // Reload the list
setShowCreateForm(false);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to create product");
} finally {
setLoading(false);
}
};

const handleArchiveProduct = async (productId: string) => {
if (!confirm("Are you sure you want to archive this product?")) {
return;
}

setLoading(true);
try {
await archiveProduct(productId);
await loadProducts();
} catch (err) {
setError(
err instanceof Error ? err.message : "Failed to archive product",
);
} finally {
setLoading(false);
}
};

return (
<div className="product-dashboard">
<header>
<h1>Product Management</h1>
<button onClick={() => setShowCreateForm(true)}>
Create New Product
</button>
</header>

{error && <div className="error-message">{error}</div>}

{showCreateForm && (
<CreateProductForm
onSubmit={handleCreateProduct}
onCancel={() => setShowCreateForm(false)}
loading={loading}
/>
)}

<div className="product-list">
{products.map((product) => (
<div key={product.id} className="product-item">
<div>
<h3>{product.name}</h3>
<p>{product.description}</p>
<span
className={`status ${product.active ? "active" : "inactive"}`}
>
{product.active ? "Active" : "Inactive"}
</span>
</div>
<div className="actions">
<button onClick={() => handleArchiveProduct(product.id)}>
Archive
</button>
</div>
</div>
))}
</div>
</div>
);
}

// Create Product Form Component
interface CreateProductFormProps {
onSubmit: (data: CreateProduct) => void;
onCancel: () => void;
loading: boolean;
}

function CreateProductForm({
onSubmit,
onCancel,
loading,
}: CreateProductFormProps) {
const [formData, setFormData] = useState<CreateProduct>({
name: "",
description: "",
active: true,
metadata: {},
});

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSubmit(formData);
};

return (
<form onSubmit={handleSubmit} className="create-product-form">
<h2>Create New Product</h2>

<div className="form-group">
<label htmlFor="name">Product Name *</label>
<input
id="name"
type="text"
required
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
/>
</div>

<div className="form-group">
<label htmlFor="description">Description</label>
<textarea
id="description"
value={formData.description}
onChange={(e) =>
setFormData({ ...formData, description: e.target.value })
}
rows={4}
/>
</div>

<div className="form-group">
<label>
<input
type="checkbox"
checked={formData.active}
onChange={(e) =>
setFormData({ ...formData, active: e.target.checked })
}
/>
Active
</label>
</div>

<div className="form-actions">
<button type="button" onClick={onCancel} disabled={loading}>
Cancel
</button>
<button type="submit" disabled={loading}>
{loading ? "Creating..." : "Create Product"}
</button>
</div>
</form>
);
}

export default ProductManagementDashboard;

Pricing Management

Create and manage prices for products, including one-time and recurring pricing.

import React, { useState } from "react";
import { useEasy } from "@easylabs/react";
import type { CreatePrice } from "@easylabs/react";

interface PricingFormProps {
productId: string;
onSuccess: () => void;
}

function PricingForm({ productId, onSuccess }: PricingFormProps) {
const { createProductPrice } = useEasy();

const [priceType, setPriceType] = useState<"one_time" | "recurring">(
"one_time",
);
const [formData, setFormData] = useState({
unitAmount: "",
currency: "USD",
description: "",
active: true,
recurring: {
interval: "month" as "month" | "year" | "week" | "day",
intervalCount: 1,
},
});
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

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

try {
const priceData: CreatePrice = {
product_id: productId,
unit_amount: parseInt(formData.unitAmount) * 100, // Convert to cents
currency: formData.currency,
description: formData.description || undefined,
active: formData.active,
...(priceType === "recurring" && {
recurring: {
interval: formData.recurring.interval,
interval_count: formData.recurring.intervalCount,
},
}),
};

await createProductPrice(priceData);
onSuccess();
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to create price");
} finally {
setLoading(false);
}
};

return (
<form onSubmit={handleSubmit} className="pricing-form">
<h3>Add Pricing</h3>

{error && <div className="error-message">{error}</div>}

<div className="form-group">
<label>Price Type</label>
<div className="radio-group">
<label>
<input
type="radio"
value="one_time"
checked={priceType === "one_time"}
onChange={(e) => setPriceType(e.target.value as "one_time")}
/>
One-time
</label>
<label>
<input
type="radio"
value="recurring"
checked={priceType === "recurring"}
onChange={(e) => setPriceType(e.target.value as "recurring")}
/>
Recurring (Subscription)
</label>
</div>
</div>

<div className="form-row">
<div className="form-group">
<label htmlFor="amount">Amount *</label>
<input
id="amount"
type="number"
required
min="0"
step="0.01"
placeholder="0.00"
value={formData.unitAmount}
onChange={(e) =>
setFormData({ ...formData, unitAmount: e.target.value })
}
/>
</div>

<div className="form-group">
<label htmlFor="currency">Currency</label>
<select
id="currency"
value={formData.currency}
onChange={(e) =>
setFormData({ ...formData, currency: e.target.value })
}
>
<option value="USD">USD</option>
<option value="EUR">EUR</option>
<option value="GBP">GBP</option>
<option value="CAD">CAD</option>
</select>
</div>
</div>

{priceType === "recurring" && (
<div className="form-row">
<div className="form-group">
<label htmlFor="interval">Billing Interval</label>
<select
id="interval"
value={formData.recurring.interval}
onChange={(e) =>
setFormData({
...formData,
recurring: {
...formData.recurring,
interval: e.target.value as
| "month"
| "year"
| "week"
| "day",
},
})
}
>
<option value="day">Day</option>
<option value="week">Week</option>
<option value="month">Month</option>
<option value="year">Year</option>
</select>
</div>

<div className="form-group">
<label htmlFor="intervalCount">Every</label>
<input
id="intervalCount"
type="number"
min="1"
value={formData.recurring.intervalCount}
onChange={(e) =>
setFormData({
...formData,
recurring: {
...formData.recurring,
intervalCount: parseInt(e.target.value),
},
})
}
/>
</div>
</div>
)}

<div className="form-group">
<label htmlFor="description">Description</label>
<input
id="description"
type="text"
placeholder="E.g., 'Basic monthly plan'"
value={formData.description}
onChange={(e) =>
setFormData({ ...formData, description: e.target.value })
}
/>
</div>

<div className="form-group">
<label>
<input
type="checkbox"
checked={formData.active}
onChange={(e) =>
setFormData({ ...formData, active: e.target.checked })
}
/>
Active (available for purchase)
</label>
</div>

<button type="submit" disabled={loading}>
{loading ? "Creating..." : "Create Price"}
</button>
</form>
);
}

export default PricingForm;

Subscription Product Example

A specialized component for subscription-based products.

import React, { useState, useEffect } from "react";
import { useEasy } from "@easylabs/react";
import type { ProductData, PriceData } from "@easylabs/react";

function SubscriptionPlans() {
const { getProducts, getProductWithPrices, checkout } = useEasy();

const [plans, setPlans] = useState<
Array<ProductData & { prices: PriceData[] }>
>([]);
const [selectedPlan, setSelectedPlan] = useState<string | null>(null);
const [loading, setLoading] = useState(false);

useEffect(() => {
loadSubscriptionPlans();
}, []);

const loadSubscriptionPlans = async () => {
setLoading(true);
try {
const productsResult = await getProducts({ limit: 20 });

// Filter for subscription products and load their prices
const subscriptionPlans = await Promise.all(
productsResult.data
.filter((product) => product.active)
.map(async (product) => {
const withPrices = await getProductWithPrices(product.id);
return {
...product,
prices: withPrices.data.prices.filter(
(price) => price.recurring && price.active,
),
};
}),
);

setPlans(subscriptionPlans.filter((plan) => plan.prices.length > 0));
} catch (err) {
console.error("Failed to load subscription plans:", err);
} finally {
setLoading(false);
}
};

const handleSubscribe = async (priceId: string) => {
setLoading(true);
try {
await checkout({
customer_creation: true,
customer_details: {
first_name: "John",
last_name: "Doe",
email: "[email protected]",
},
source: "payment_instrument_id",
line_items: [{ price_id: priceId, quantity: 1 }],
});
alert("Subscription created successfully!");
} catch (err) {
console.error("Subscription failed:", err);
alert("Failed to create subscription");
} finally {
setLoading(false);
}
};

const formatPrice = (price: PriceData) => {
const amount = new Intl.NumberFormat("en-US", {
style: "currency",
currency: price.currency,
}).format(price.unit_amount / 100);

const interval =
price.recurring?.interval_count === 1
? price.recurring.interval
: `${price.recurring?.interval_count} ${price.recurring?.interval}s`;

return `${amount}/${interval}`;
};

return (
<div className="subscription-plans">
<h1>Choose Your Plan</h1>

<div className="plans-grid">
{plans.map((plan) => (
<div key={plan.id} className="plan-card">
<h2>{plan.name}</h2>
<p>{plan.description}</p>

<div className="pricing-tiers">
{plan.prices.map((price) => (
<div key={price.id} className="price-tier">
<div className="price">{formatPrice(price)}</div>
{price.description && <p>{price.description}</p>}
<button
onClick={() => handleSubscribe(price.id)}
disabled={loading}
className={selectedPlan === price.id ? "selected" : ""}
>
{loading && selectedPlan === price.id
? "Processing..."
: "Subscribe"}
</button>
</div>
))}
</div>

{plan.metadata && (
<div className="features">
<h4>Features:</h4>
<ul>
{Object.entries(plan.metadata).map(([key, value]) => (
<li key={key}>
{key}: {String(value)}
</li>
))}
</ul>
</div>
)}
</div>
))}
</div>
</div>
);
}

export default SubscriptionPlans;

API Methods Reference

Product Methods

getProducts

Retrieve all products with pagination.

const result = await getProducts({
limit?: number; // Default: 20
offset?: number; // Default: 0
});

getProduct

Get a single product by ID.

const result = await getProduct(productId);

getProductWithPrices

Get a product with all its associated prices.

const result = await getProductWithPrices(productId);

getProductWithPrice

Get a product with a specific price.

const result = await getProductWithPrice(productId, priceId);

createProduct

Create a new product.

const result = await createProduct({
name: string;
description?: string;
active?: boolean;
images?: string[];
metadata?: Record<string, unknown>;
});

updateProduct

Update an existing product.

const result = await updateProduct(productId, {
name?: string;
description?: string;
active?: boolean;
metadata?: Record<string, unknown>;
});

archiveProduct

Archive a product (soft delete).

const result = await archiveProduct(productId);

Price Methods

getProductPrices

Get all prices with pagination.

const result = await getProductPrices({
limit?: number;
offset?: number;
});

getProductPrice

Get a specific price by ID.

const result = await getProductPrice(priceId);

createProductPrice

Create a new price for a product.

const result = await createProductPrice({
product_id: string;
unit_amount: number; // in cents
currency: string;
recurring?: {
interval: "day" | "week" | "month" | "year";
interval_count: number;
};
active?: boolean;
description?: string;
metadata?: Record<string, unknown>;
});

updateProductPrice

Update a price (limited fields).

const result = await updateProductPrice(priceId, {
active?: boolean;
description?: string;
metadata?: Record<string, unknown>;
});

archiveProductPrice

Archive a price.

const result = await archiveProductPrice(priceId);

Best Practices

1. Use Metadata for Features

Store product features in metadata for easy access:

await createProduct({
name: "Premium Plan",
metadata: {
max_users: 10,
storage_gb: 100,
support: "priority",
features: ["analytics", "api_access", "sso"],
},
});

2. Active Status Management

Use the active flag to control visibility:

// Hide a product without deleting it
await updateProduct(productId, { active: false });

// Archive permanently
await archiveProduct(productId);

3. Price Versioning

Create new prices instead of updating amounts:

// DON'T update unit_amount
// DO create a new price and archive the old one
await createProductPrice({
product_id: productId,
unit_amount: 2999, // new price
currency: "USD",
});

await archiveProductPrice(oldPriceId);

4. Display Recurring Intervals Properly

Format recurring intervals for better UX:

const formatInterval = (price: PriceData) => {
if (!price.recurring) return "";

const { interval, interval_count } = price.recurring;

if (interval_count === 1) {
return interval; // "month"
}

return `${interval_count} ${interval}s`; // "3 months"
};

5. Handle Currency Properly

Always work in cents and format for display:

// Store: 2999 (cents)
const amount = 2999;

// Display: $29.99
const formatted = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(amount / 100);

Next Steps