🎟️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

  1. Transparency: Always show presale status and estimated shipping

  2. Urgency: Display reservation timers when applicable

  3. Clarity: Explain why presale items need separate orders

  4. 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