🎟️Presales
Presale Feature Guide
Overview
The LiquidCommerce SDK provides comprehensive support for presale products, allowing merchants to offer products for purchase before general availability. This guide covers how to integrate presale functionality into your application.
What are Presales?
Presales enable customers to reserve and purchase products before they become widely available. Key features include:
Advanced Purchase: Customers can buy products before stock arrival
Limited Quantities: Presale products often have purchase limits
Scheduled Availability: Products have defined presale windows
Exclusive Access: First-come, first-served inventory reservation
Working with Presale Products
Identifying Presale Products
Use the catalog search to find presale products:
const presaleProducts = await liquidCommerce.catalog.search({
filters: [{
key: ENUM_FILTER_KEYS.PRESALE,
values: ENUM_BINARY_FILTER.YES
}],
loc: {
address: {
one: '123 Main St',
city: 'New York',
state: 'NY',
zip: '10001'
}
}
});
// Extract presale information from products
presaleProducts.products.forEach(product => {
product.sizes.forEach(size => {
const presale = size.attributes?.presale;
if (presale?.isActive) {
console.log({
product: product.name,
availableFrom: presale.canPurchaseOn,
estimatedShipping: presale.estimatedShipBy,
status: 'Active Presale'
});
}
});
});
Cart Response Structure
When working with presale items, the cart response includes additional fields:
interface ICart {
id: string;
// ... standard cart fields
// Presale-specific fields
isPresaleLocked: boolean; // Indicates cart contains presale items
presaleExpiresAt: string | null; // Reservation expiration time
events: ICartEvent[]; // Contains presale-related events
}
Adding Presale Items to Cart
Basic Implementation
const cartResponse = await liquidCommerce.cart.update({
id: cartId, // Use existing cart ID or create new
items: [{
partNumber: 'WHISKEY_PRESALE_2024_retailer_abc',
quantity: 1,
fulfillmentId: 'shipping_fulfillment_123'
}],
loc: {
address: {
one: '456 Oak Avenue',
city: 'Los Angeles',
state: 'CA',
zip: '90001'
}
}
});
// Check if presale was successfully added
if (cartResponse.isPresaleLocked) {
console.log('Presale item reserved successfully');
console.log(`Complete checkout by: ${cartResponse.presaleExpiresAt}`);
}
Handling Presale Events
The SDK provides specific event types for presale scenarios:
// Available presale events
enum CART_EVENT_ENUM {
PRESALE_ITEMS_NOT_ALLOWED = 'PresaleItemsNotAllowed',
PRESALE_LIMIT_EXCEEDED = 'PresaleLimitExceeded',
PRESALE_NOT_STARTED = 'PresaleNotStarted',
PRESALE_EXPIRED = 'PresaleExpired',
PRESALE_MIXED_CART = 'PresaleMixedCart',
}
Example event handling:
function handlePresaleEvents(cart: ICart) {
cart.events.forEach(event => {
switch(event.type) {
case CART_EVENT_ENUM.PRESALE_NOT_STARTED:
// Presale hasn't begun yet
alert('This presale has not started. Please check back later.');
break;
case CART_EVENT_ENUM.PRESALE_LIMIT_EXCEEDED:
// Requested quantity exceeds available inventory
alert('Sorry, not enough inventory available. Please try a smaller quantity.');
break;
case CART_EVENT_ENUM.PRESALE_EXPIRED:
// Presale period has ended
alert('This presale has ended.');
break;
case CART_EVENT_ENUM.PRESALE_MIXED_CART:
// Cannot mix presale with other items
alert('Presale items must be purchased separately.');
break;
}
});
}
Checkout Process
Preparing Checkout
Complete the checkout process promptly when isPresaleLocked
is true:
async function checkoutPresaleCart(cartId: string) {
// Step 1: Prepare checkout
const checkoutPrep = await liquidCommerce.checkout.prepare({
cartId: cartId,
customer: {
firstName: "Sarah",
lastName: "Johnson",
email: "[email protected]",
phone: "3105551234",
birthDate: "1985-06-15"
},
billingAddress: {
firstName: "Sarah",
lastName: "Johnson",
email: "[email protected]",
phone: "3105551234",
one: "789 Pine Street",
two: "Suite 100",
city: "San Francisco",
state: "CA",
zip: "94102"
},
hasSubstitutionPolicy: true,
marketingPreferences: {
canEmail: true,
canSms: false
}
});
// Step 2: Process payment
// Assuming payment element is already mounted
const paymentToken = await liquidCommerce.payment.generateToken();
if ('id' in paymentToken) {
// Step 3: Complete checkout
const order = await liquidCommerce.checkout.complete({
token: checkoutPrep.token,
payment: paymentToken.id
});
return order;
}
}
Time-Sensitive Checkout
Monitor the reservation time when dealing with presales:
class PresaleCheckoutManager {
private expirationTimer?: NodeJS.Timeout;
startCheckout(cart: ICart) {
if (!cart.isPresaleLocked || !cart.presaleExpiresAt) {
return;
}
const expiresAt = new Date(cart.presaleExpiresAt);
const now = new Date();
const timeRemaining = expiresAt.getTime() - now.getTime();
// Show countdown to user
this.displayCountdown(timeRemaining);
// Warn before expiration
if (timeRemaining > 60000) { // More than 1 minute
this.expirationTimer = setTimeout(() => {
this.warnUserAboutExpiration();
}, timeRemaining - 60000);
}
}
private displayCountdown(milliseconds: number) {
const minutes = Math.floor(milliseconds / 60000);
const seconds = Math.floor((milliseconds % 60000) / 1000);
console.log(`Time remaining: ${minutes}:${seconds.toString().padStart(2, '0')}`);
}
private warnUserAboutExpiration() {
alert('Your presale reservation expires in 1 minute!');
}
cleanup() {
if (this.expirationTimer) {
clearTimeout(this.expirationTimer);
}
}
}
Best Practices
1. Clear User Communication
// Display presale information prominently
function displayPresaleInfo(product: IProduct) {
const presaleInfo = product.sizes[0]?.attributes?.presale;
if (presaleInfo?.isActive) {
return {
status: 'PRESALE',
availableDate: presaleInfo.canPurchaseOn,
shippingDate: presaleInfo.estimatedShipBy,
message: `Pre-order now, ships ${formatDate(presaleInfo.estimatedShipBy)}`
};
}
}
2. Cart Separation
// Check before adding items to existing cart
async function addToCart(item: ICartUpdateItem, existingCartId?: string) {
if (existingCartId) {
// Check if existing cart has presale items
const existingCart = await liquidCommerce.cart.get(existingCartId);
if (existingCart.isPresaleLocked) {
// Create new cart for non-presale items
console.log('Creating new cart - existing cart contains presale items');
return await liquidCommerce.cart.update({
id: 'new',
items: [item],
loc: existingCart.loc
});
}
}
// Safe to use existing cart
return await liquidCommerce.cart.update({
id: existingCartId || 'new',
items: [item],
loc: { /* location */ }
});
}
3. Error Recovery
async function robustPresalePurchase(
partNumber: string,
quantity: number,
maxRetries: number = 3
) {
let attempts = 0;
while (attempts < maxRetries) {
try {
const cart = await liquidCommerce.cart.update({
id: 'new',
items: [{ partNumber, quantity, fulfillmentId: 'shipping_123' }],
loc: { /* location */ }
});
// Check for presale events
const hasPresaleError = cart.events.some(e =>
e.type === CART_EVENT_ENUM.PRESALE_LIMIT_EXCEEDED ||
e.type === CART_EVENT_ENUM.PRESALE_EXPIRED
);
if (hasPresaleError) {
throw new Error('Presale not available');
}
if (cart.isPresaleLocked) {
return { success: true, cart };
}
} catch (error) {
attempts++;
if (attempts >= maxRetries) {
return { success: false, error: error.message };
}
// Wait before retry
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
}
Common Integration Patterns
Pattern 1: Presale Product Page
class PresaleProductPage {
async displayProduct(productId: string) {
// Fetch product details
const availability = await liquidCommerce.catalog.availability({
ids: [productId],
loc: this.getUserLocation()
});
const product = availability.products[0];
const presaleSize = product.sizes.find(s =>
s.attributes?.presale?.isActive
);
if (presaleSize) {
this.showPresaleBadge();
this.showEstimatedShipping(presaleSize.attributes.presale.estimatedShipBy);
this.enablePresalePurchase(presaleSize);
}
}
private async enablePresalePurchase(size: IProductSize) {
// Set up purchase button
const purchaseButton = document.getElementById('purchase-btn');
purchaseButton.onclick = async () => {
const cart = await liquidCommerce.cart.update({
id: 'new',
items: [{
partNumber: size.variants[0].partNumber,
quantity: 1,
fulfillmentId: size.variants[0].fulfillments[0]
}],
loc: this.getUserLocation()
});
if (cart.isPresaleLocked) {
// Redirect to checkout
window.location.href = `/checkout?cartId=${cart.id}`;
}
};
}
}
Pattern 2: Presale Collection Page
async function createPresaleCollection() {
// Fetch all active presales
const presales = await liquidCommerce.catalog.search({
filters: [{
key: ENUM_FILTER_KEYS.PRESALE,
values: ENUM_BINARY_FILTER.YES
}],
orderBy: ENUM_ORDER_BY.PRICE,
orderDirection: ENUM_NAVIGATION_ORDER_DIRECTION_TYPE.ASC,
page: 1,
perPage: 20,
loc: { /* user location */ }
});
// Group by availability date
const groupedPresales = presales.products.reduce((acc, product) => {
product.sizes.forEach(size => {
const presale = size.attributes?.presale;
if (presale?.isActive && presale.canPurchaseOn) {
const date = new Date(presale.canPurchaseOn).toDateString();
if (!acc[date]) acc[date] = [];
acc[date].push({ product, size });
}
});
return acc;
}, {});
return groupedPresales;
}
Limitations and Considerations
System Limitations
Cart Exclusivity: Presale items require dedicated carts
Time Constraints: Reservations have expiration times
Quantity Restrictions: Limited inventory per customer
Geographic Availability: Some presales may be region-specific
User Experience Considerations
Transparency: Always show presale status and estimated shipping
Urgency: Display reservation timers when applicable
Clarity: Explain why presale items need separate orders
Feedback: Provide clear error messages for presale-specific issues
Summary
The presale feature in LiquidCommerce SDK enables:
Early access to limited products
Automated inventory management
Time-based reservations
Clear event-driven feedback
Successful presale integration requires careful attention to:
Event handling for various presale states
Time-sensitive checkout flows
Clear user communication
Proper cart segregation
By following this guide and the provided examples, you can create a smooth presale experience that maximizes conversion while maintaining inventory integrity.
Last updated