I’m having trouble with
Finding a solution or a way for my product to populate and carry over to the cart via velo code, its a dynamic page serving as a replacement product page, i cant get the product to the cart.
Working in
Wix Editor, Dev Mode, CMS, Velo, Dynamic Page
Heres my Backend Code so afar, some done my myself and AI mostly:
// /backend/store.jsw
import { getProductVariants } from 'wix-stores-backend';
import { cart } from 'wix-ecom-backend';
// ----------------------------------------------------------------------
// 1. VARIANT RETRIEVAL FUNCTION
// ----------------------------------------------------------------------
export async function getProductVariant(productId, options) {
try {
// ... (This function remains the same, as the error is now in cart function) ...
console.log("Backend: Calling API with options:", options);
const productOptions = { choices: options };
const variants = await getProductVariants(productId, productOptions);
console.log("Backend: RAW VARIANTS RESPONSE:", variants);
if (!variants || variants.length !== 1) {
console.error(`Backend: Failed to find unique variant. Found ${variants ? variants.length : 0} matching variants.`);
return null;
}
const foundVariant = variants[0];
const variantData = foundVariant['variant'] || foundVariant;
const finalVariant = {
variantId: foundVariant['_id'],
choices: options,
formattedPrice: variantData['formattedPrice'] || null,
sku: variantData['sku'] || null,
stock: {
inStock: variantData['inStock'] ?? true
}
};
console.log("Backend: Returning final variant object (PACKAGED):", finalVariant);
return finalVariant;
} catch (error) {
console.error("Backend Error in getProductVariant:", error);
// Throwing a descriptive error here is crucial for debugging
throw new Error("Failed to retrieve variant data from Wix Stores API. Check product ID and options structure.");
}
}
// ----------------------------------------------------------------------
// 2. ADD TO CART FUNCTION (Re-introducing 'appId: wix-stores')
// ----------------------------------------------------------------------
/**
* Adds a specific product variant to the cart using the reliable cart['addProducts'] method.
* Re-introduced the 'appId' as it is the standard, documented requirement for wix-ecom-backend.
*/
export function addVariantToCart(productId, variantId, quantity) {
// NEW LOGS: Confirm the data received from the frontend
console.log(`Backend CART: STARTING addVariantToCart.`);
console.log(`Backend CART: Received ProductId: ${productId}`);
console.log(`Backend CART: Received VariantId: ${variantId}`);
console.log(`Backend CART: Received Quantity: ${quantity}`);
// Construct the product item object
const productItem = {
catalogReference: {
// REVERTED FIX ATTEMPT: Re-added 'appId' as this is the standard, often mandatory structure.
appId: 'wix-stores',
catalogItemId: productId,
// Options structure required to specify the variant ID
options: {
variantId: variantId
}
},
quantity: quantity
};
// NEW LOG: Show the final object structure before calling the API
console.log("Backend CART: Final ProductItem object being sent to API:", JSON.stringify(productItem));
// Using the working bracket notation for 'addProducts'
return cart['addProducts']([productItem])
.then(result => {
console.log("Backend CART: SUCCESS! Cart update result:", result);
return result; // Pass success result back to frontend
})
.catch(error => {
// CRITICAL: Log the raw, specific API error
console.error("Backend CART: API Call FAILED! Raw Error:", error);
// Throw a specific error message back to the frontend
const errorMessage = (typeof error === 'object' && error !== null && error.message)
? error.message
: (typeof error === 'string' ? error : 'Unknown API Failure');
throw new Error(`Cart API Error: ${errorMessage}. Please check product variant stock and ID integrity.`);
});
}
Heres my frontend:
import wixData from 'wix-data';
import wixWindow from 'wix-window';
import wixStores from "wix-stores";
import { getProductVariant, addToCartProduct } from 'backend/store'; // <-- Restored backend import
// --- CONFIGURATION ---
const LIGHTBOX_PRODUCT_IDS = ['HPBEDCOVER', 'HPCAP', 'HPSINGLE', 'HPTWIN'];
const LIGHTBOX_NAME = 'ContactUsLightbox';
const STANDARD_COLOR_SIZE = '150ml'; // Used in filtering logic
// ---------------------
// Component IDs
const PRICE_TEXT_ID = "#priceText";
const SKU_TEXT_ID = "#text1";
const ADD_TO_CART_BUTTON_ID = "#addToCartButton";
const SIZE_DROP_ID = "#sizeDrop";
const COLOUR_DROP_ID = "#colourDrop";
const QUANTITY_INPUT_ID = "#quantityInput";
const BREADCRUMBS_ID = "#breadcrumbs1";
// Global variables
let productOptionsMap = {};
let currentSelectedVariantId = null;
let baseFormattedPrice = "";
let currentProductId = "";
let currentProductSku = "";
// Helper function to safely handle messages
function safelyHandleMessage(id, action, message = "") {
if ($w(id).type === 'Text') {
if (action === 'show') {
$w(id).text = message;
$w(id).show();
console.log(`UI: Showing message on ${id}: ${message}`);
} else {
$w(id).hide();
console.log(`UI: Hiding message on ${id}.`);
}
}
}
$w.onReady(function () {
console.log("--- Page Load: Starting $w.onReady ---");
// Initial UI setup (ensure dropdowns are hidden initially)
$w(SIZE_DROP_ID).hide();
$w(COLOUR_DROP_ID).hide();
safelyHandleMessage("#textSuccess", 'hide');
$w("#dynamicDataset").onReady(async () => {
const currentItem = $w("#dynamicDataset").getCurrentItem();
if (!currentItem) {
console.error("Initialization Error: No product data found.");
return;
}
currentProductId = currentItem._id;
currentProductSku = currentItem.sku;
// --- BREADCRUMBS FIX ---
updateBreadcrumbs(currentItem.name);
// --- 1. GET BASE PRICE & INITIAL SETUP ---
baseFormattedPrice = getBasePrice(currentItem);
$w(PRICE_TEXT_ID).text = baseFormattedPrice;
// --- 2. GET FULL PRODUCT DATA FOR VARIANTS & TABS ---
const product = await wixData.get("Stores/Products", currentProductId);
// Setup tabs with the FULL product data here
setupInfoTabs(product);
// --- SKU CHECK & BUTTON OVERRIDE ---
const isLightbox = isLightboxProduct(currentProductSku);
if (isLightbox) {
setupLightboxButton();
}
if (!isLightbox) {
// Variant logic only runs for non-lightbox products
if (product && product.productOptions) {
productOptionsMap = product.productOptions;
}
// Determine which options have more than one choice
const hasSize = productOptionsMap.Size && productOptionsMap.Size.choices.length > 1;
const hasColour = productOptionsMap.Colour && productOptionsMap.Colour.choices.length > 1;
if (hasSize || hasColour) {
console.log(`Product has variants. Has Size: ${hasSize}, Has Colour: ${hasColour}. Setting up dropdowns.`);
// Populate and show only the required dropdowns
populateDropdowns(productOptionsMap, hasSize, hasColour);
// Only attach onChange handlers if the dropdown is visible
if (hasSize) {
$w(SIZE_DROP_ID).show();
$w(SIZE_DROP_ID).onChange(() => filterDropdownOptions('Size'));
}
if (hasColour) {
$w(COLOUR_DROP_ID).show();
$w(COLOUR_DROP_ID).onChange(() => filterDropdownOptions('Colour'));
}
// If only one option type exists, or none need selection, run variant check immediately
if (!hasSize || !hasColour) {
checkForValidVariant();
}
} else {
// Set currentSelectedVariantId to currentProductId for single-variant products
console.log("Product is single variant (no meaningful options). Setting default variant ID.");
currentSelectedVariantId = currentProductId;
$w(ADD_TO_CART_BUTTON_ID).enable();
if ($w(SKU_TEXT_ID)) $w(SKU_TEXT_ID).text = `SKU: ${currentItem.sku || 'N/A'}`;
}
}
// Setup final handlers
$w(ADD_TO_CART_BUTTON_ID).onClick(handleAddToCartClick);
});
});
// ----------------------------------------------------------------------
// --- SKU CHECK AND BUTTON SETUP FUNCTIONS ---
// ----------------------------------------------------------------------
function isLightboxProduct(sku) {
if (!sku) return false;
return LIGHTBOX_PRODUCT_IDS.includes(sku.toUpperCase());
}
function setupLightboxButton() {
console.log("Lightbox Product detected. Overriding button for Contact Us.");
$w(SIZE_DROP_ID).hide();
$w(COLOUR_DROP_ID).hide();
$w(ADD_TO_CART_BUTTON_ID).label = "Contact Us For More";
$w(ADD_TO_CART_BUTTON_ID).enable();
}
// ----------------------------------------------------------------------
// --- BREADCRUMBS LOGIC ---
// ----------------------------------------------------------------------
function updateBreadcrumbs(productName) {
const breadcrumbs = $w(BREADCRUMBS_ID);
if (productName && breadcrumbs && breadcrumbs.items && breadcrumbs.items.length > 0) {
const items = breadcrumbs.items;
const updatedItems = items.map((item, index) => {
if (index === items.length - 1) {
return {
...item,
label: productName
};
}
return item;
});
breadcrumbs.items = updatedItems;
console.log(`Breadcrumbs: Updated last item label to "${productName}".`);
}
}
// ----------------------------------------------------------------------
// --- CORE LOGIC FUNCTIONS: PRICE & VARIANT SELECTION ---
// ----------------------------------------------------------------------
function getBasePrice(item) {
let price = "Base Price N/A";
// 1. Try formatted prices from the data source
if (item.formattedPrice) {
price = item.formattedPrice;
}
else if (item.price && item.price.formatted) {
price = item.price.formatted;
}
else if (item.pricing && item.pricing.formattedPrice) {
price = item.pricing.formattedPrice;
}
// 2. Fallback: Use raw price and format with GBP sign
else if (item.price) {
price = item.price.toFixed(2);
}
// Ensure the Pound sign is present at the start
if (price !== "Base Price N/A" && !price.startsWith('£') && !price.startsWith('GBP')) {
const cleanedPrice = price.toString().replace(/[^0-9.]/g, '');
return `£${cleanedPrice}`;
}
return price;
}
// Updated to only populate visible dropdowns
function populateDropdowns(optionsMap, hasSize, hasColour) {
if (hasSize) {
const sizeOptions = optionsMap.Size.choices.map(c => ({ label: c.description, value: c.description }));
$w(SIZE_DROP_ID).options = [{ label: "Select Size", value: "" }, ...sizeOptions];
$w(SIZE_DROP_ID).value = "";
}
if (hasColour) {
const colourOptions = optionsMap.Colour.choices.map(c => ({ label: c.description, value: c.description }));
$w(COLOUR_DROP_ID).options = [{ label: "Select Colour", value: "" }, ...colourOptions];
$w(COLOUR_DROP_ID).value = "";
}
}
function filterDropdownOptions(changedOption) {
const selectedSize = $w(SIZE_DROP_ID).value;
const selectedColour = $w(COLOUR_DROP_ID).value;
console.log(`Filter: Changed Option: ${changedOption}. Current Selections: Size=${selectedSize}, Colour=${selectedColour}`);
let dropdownToFilter, optionToFilter, currentSelection;
// Determine which dropdown to filter based on which one was changed,
// only if the dropdown being filtered is visible.
if (changedOption === 'Size' && $w(COLOUR_DROP_ID).isVisible) {
dropdownToFilter = $w(COLOUR_DROP_ID);
optionToFilter = 'Colour';
currentSelection = selectedSize;
} else if (changedOption === 'Colour' && $w(SIZE_DROP_ID).isVisible) {
dropdownToFilter = $w(SIZE_DROP_ID);
optionToFilter = 'Size';
currentSelection = selectedColour;
} else {
// Only one dropdown visible, or the other dropdown is already hidden.
checkForValidVariant();
return;
}
const allChoices = productOptionsMap[optionToFilter].choices.map(c => c.description);
let uniqueValidOptions = [];
// --- Custom Filtering Logic (preserved) ---
if (!currentSelection) {
uniqueValidOptions = allChoices;
} else if (optionToFilter === 'Colour') {
uniqueValidOptions = (currentSelection === STANDARD_COLOR_SIZE)
? allChoices
: allChoices.filter(color => color === 'White');
} else if (optionToFilter === 'Size') {
uniqueValidOptions = (currentSelection !== 'White')
? allChoices.filter(size => size === STANDARD_COLOR_SIZE)
: allChoices;
} else {
uniqueValidOptions = allChoices;
}
// ----------------------------------------
const newOptions = uniqueValidOptions.map(value => ({ label: value, value: value }));
const currentValue = dropdownToFilter.value;
dropdownToFilter.options = [{ label: `Select ${optionToFilter}`, value: "" }, ...newOptions];
// Restore selected value if valid, or select single option
if (currentValue && uniqueValidOptions.includes(currentValue)) {
dropdownToFilter.value = currentValue;
} else if (uniqueValidOptions.length === 1) {
dropdownToFilter.value = newOptions[0].value;
} else {
dropdownToFilter.value = "";
}
checkForValidVariant();
}
// NOTE: This function relies on the external backend function 'getProductVariant'
// to correctly return the 'variantId' which is CRITICAL for the price/stock logic.
async function checkForValidVariant() {
if (isLightboxProduct(currentProductSku)) return;
const options = {};
const requiredSelections = [];
// Safely build the options object based on visibility
if ($w(SIZE_DROP_ID).isVisible) {
const selectedSize = $w(SIZE_DROP_ID).value;
options["Size"] = selectedSize;
requiredSelections.push(selectedSize);
} else if (productOptionsMap.Size && productOptionsMap.Size.choices.length === 1) {
// If Size is not visible but has exactly one choice, use that as the option
options["Size"] = productOptionsMap.Size.choices[0].description;
}
if ($w(COLOUR_DROP_ID).isVisible) {
const selectedColour = $w(COLOUR_DROP_ID).value;
options["Colour"] = selectedColour;
requiredSelections.push(selectedColour);
} else if (productOptionsMap.Colour && productOptionsMap.Colour.choices.length === 1) {
// If Colour is not visible but has exactly one choice, use that as the option
options["Colour"] = productOptionsMap.Colour.choices[0].description;
}
// Check if all *visible* dropdowns have a value selected
if (requiredSelections.some(s => !s)) {
resetToBaseProduct("Select All Options");
// Ensure to clear the variant ID if selections are incomplete
currentSelectedVariantId = null;
return;
}
// If no variants exist, reset to base and rely on onReady
if (Object.keys(options).length === 0) {
if (currentSelectedVariantId === currentProductId) {
$w(ADD_TO_CART_BUTTON_ID).enable();
} else {
resetToBaseProduct("Ready to Add");
}
return;
}
$w(ADD_TO_CART_BUTTON_ID).disable();
try {
// Use the backend function to find the variant ID and check stock/price
const selectedVariant = await getProductVariant(currentProductId, options);
const variantExists = !!selectedVariant;
if (variantExists) {
const currentPrice = selectedVariant.formattedPrice || baseFormattedPrice;
$w(PRICE_TEXT_ID).text = currentPrice;
if ($w(SKU_TEXT_ID)) $w(SKU_TEXT_ID).text = `SKU: ${selectedVariant.sku || 'N/A'}`;
const isAvailable = selectedVariant.stock && selectedVariant.stock.inStock === true;
if (isAvailable) {
$w(ADD_TO_CART_BUTTON_ID).enable();
$w(ADD_TO_CART_BUTTON_ID).label = "Add to Cart";
// Store the unique Variant ID here for internal check, not for the API call anymore
currentSelectedVariantId = selectedVariant.variantId;
} else {
resetToBaseProduct("Out of Stock");
$w(PRICE_TEXT_ID).text = `${currentPrice} (Out of Stock)`;
currentSelectedVariantId = null; // Clear ID if out of stock
}
} else {
resetToBaseProduct("Invalid Combination");
console.log("Variant Check Failed: Invalid combination selected.");
currentSelectedVariantId = null;
}
} catch (error) {
console.error("Variant Check ERROR:", error);
resetToBaseProduct("Error checking");
console.log("Variant Check Failed: Error checking options. Try refreshing the page.");
currentSelectedVariantId = null;
}
}
// ----------------------------------------------------------------------
// --- 3. FINAL BUTTON HANDLER (Opens Lightbox OR Adds to Cart - WIX STORES API) ---
// ----------------------------------------------------------------------
export async function handleAddToCartClick(event) {
console.log("Button Clicked. Attempting stable Add to Cart via BACKEND Web Module.");
// --- Step 1: Handle Lightbox Products (Check SKU) ---
if (isLightboxProduct(currentProductSku)) {
console.log("Button Handler: Opening Lightbox for Contact Us.");
wixWindow.openLightbox(LIGHTBOX_NAME, {
productSku: currentProductSku,
productId: currentProductId
});
return;
}
// --- Step 2: Handle Add to Cart Products ---
const quantity = $w(QUANTITY_INPUT_ID).value || 1;
const numericQuantity = Number(quantity);
// Validate if a valid variant has been selected and set
if (!currentSelectedVariantId) {
console.log("Add to Cart Aborted: No valid variant ID selected/checked.");
checkForValidVariant();
return;
}
// --- Build the options payload for the wixStores API ---
const options = {};
if ($w(SIZE_DROP_ID).isVisible) {
options["Size"] = $w(SIZE_DROP_ID).value;
} else if (productOptionsMap.Size && productOptionsMap.Size.choices.length === 1) {
options["Size"] = productOptionsMap.Size.choices[0].description;
}
if ($w(COLOUR_DROP_ID).isVisible) {
options["Colour"] = $w(COLOUR_DROP_ID).value;
} else if (productOptionsMap.Colour && productOptionsMap.Colour.choices.length === 1) {
options["Colour"] = productOptionsMap.Colour.choices[0].description;
}
console.log("Cart Payload Strategy: Using backend addToCartProduct.");
console.log(`Payload: Product ID=${currentProductId}, Quantity=${numericQuantity}, Options=${JSON.stringify(options)}`);
// Execute the backend API Call
try {
$w(ADD_TO_CART_BUTTON_ID).disable();
safelyHandleMessage("#textSuccess", 'hide');
// 1. Call the new stable backend function
const responseData = await addToCartProduct(currentProductId, numericQuantity, options);
// --- 1.5. CRITICAL SYNCHRONIZATION PAUSE ---
// Wait a moment for the server to update the cart data before opening the UI component.
await new Promise(resolve => setTimeout(resolve, 100));
// 2. Open the side cart using bracket notation on the frontend API
wixStores['openCart']();
console.log("SUCCESS: Item added to cart (via backend) and side cart opened.", responseData);
// 3. Show local success message
safelyHandleMessage("#textSuccess", 'show', `Added to cart!`);
setTimeout(() => safelyHandleMessage("#textSuccess", 'hide'), 3000);
} catch (error) {
// Errors from the stable Backend API call are caught here
console.error("Add to Cart FAILED (Backend Catch):", error);
let errorMessage = "Failed to add item to cart. Please ensure selections are valid.";
if (error && error.message) {
errorMessage = `Failed to add item to cart. Error: ${error.message}`;
}
safelyHandleMessage("#textSuccess", 'show', errorMessage);
console.log(`Add to Cart UI Error (Visible): ${errorMessage}`);
} finally {
$w(ADD_TO_CART_BUTTON_ID).enable();
}
}
// ----------------------------------------------------------------------
// --- INFO TAB FUNCTIONS ---
// ----------------------------------------------------------------------
function setupInfoTabs(productItem) {
const infoSections = productItem?.additionalInfoSections;
if (!infoSections || infoSections.length < 3) {
console.warn("Info Tabs Setup: Not enough sections found in product data (requires at least 3).");
return;
}
console.log(`Info Tabs Setup: Found ${infoSections.length} sections. Populating tabs.`);
$w("#tabTitle1").text = infoSections[0].title;
$w("#tabDescription1").html = infoSections[0].description;
$w("#tabTitle2").text = infoSections[1].title;
$w("#tabDescription2").html = infoSections[1].description;
$w("#tabTitle3").text = infoSections[2].title;
$w("#tabDescription3").html = infoSections[2].description;
$w("#group45").show();
}
function resetToBaseProduct(label = "Select Options") {
currentSelectedVariantId = null;
$w(ADD_TO_CART_BUTTON_ID).disable();
$w(ADD_TO_CART_BUTTON_ID).label = label;
$w(PRICE_TEXT_ID).text = baseFormattedPrice;
if ($w(SKU_TEXT_ID)) $w(SKU_TEXT_ID).text = `SKU: N/A`;
}
// ----------------------------------------------------------------------
// --- TAB CLICK HANDLERS ---
// ----------------------------------------------------------------------
export function descTab_click(event) {
console.log("Tab Click: Description");
$w("#group45").show();
$w("#group44").hide();
$w("#group46").hide();
}
export function specsTab_click(event) {
console.log("Tab Click: Specifications");
$w("#group45").hide();
$w("#group44").show();
$w("#group46").hide();
}
export function instrTab_click(event) {
console.log("Tab Click: Instructions");
$w("#group45").hide();
$w("#group44").hide();
$w("#group46").show();
}
Much help appreciated.
