Bulk Order for Personalized Products (only for Campaign by Product Base)
Bulk Order for Personalized Products (Campaign by Product Base)
Selling personalized products often involves customers ordering multiple items with the same design but different variants (for example: different sizes, colors, or quantities).
The Bulk Order setup allows customers to customize a product once, then quickly add multiple variants of that same personalized design to the cart.
This approach works especially well for scenarios such as:
- Family shirts with the same design but different sizes
- Team merchandise with identical artwork
- Group gifts where each item shares the same personalization
This guide will show you how to implement a Bulk Order workflow using Teeinblue.
Important Notes
Before setting up Bulk Order, please keep the following limitations in mind:
This setup only works with Campaign by Product Base.- This ensures that the same personalized design can be reused when customers select multiple variants.
- These limitations are required because Teeinblue generates order designs based on the artworks applied to the campaign. If different artworks are linked to variants, the personalization form may change dynamically.
Overview of the Bulk Order Flow
With the Bulk Order setup:
- Customers personalize the product once.
- They can then select multiple variants and quantities in a bulk order form.
- All selected variants are added to the cart with the same personalized artwork.

Step 1: Create a product template
To keep the implementation consistent across products, you should create a dedicated product template.
How to create it
- Go to Shopify Admin → Online Store → Themes
- Create new Product template:
teeinblue-bulk-order
- After creating the template, assign it to the products that will use the Bulk Order layout.

Step 2: Prepare Your Campaign in Teeinblue
Create a campaign normally in Teeinblue, but ensure it follows these rules.
Campaign requirements
- Campaign type: Campaign by Product Base
- Only one artwork used across the campaign
- Artwork must apply to all variants
This ensures the personalization data remains consistent when customers order multiple variants.
Step 3: Add Bulk Order Layout to the Product Page
Next, we need to insert the Bulk Order interface into the product template.
- Create this file:
snippets/teeinblue-bundle.liquid
{{ 'teeinblue-bundle.css' | asset_url | stylesheet_tag }}
<script>
(() => {
/* Delay `autofixer` to avoid 429 and ATC too many times. */
window.teeinblue = {
...(window.teeinblue || {}),
autofixerDelay: 10000
}
/** Constants and shared state - START */
/** Namespace prefix for bundle-specific classes and data attributes. */
const TEE_BUNDLE_PREFIX = 'tee_bundle';
/** Event emitted by Teeinblue after the campaign UI is injected. */
const TEE_BUNDLE_INJECT_EVENT = 'teeinblue-event-component-injected';
/** Event to dispatch for ajax cart updating */
const TEE_EVENTS_AFTER_CART_ADDED = [
'cart:refresh', // for Kalles theme
{% comment %} 'teeinblue-event-after-cart-added' {% endcomment %}
]
/** Timeout for a single native add-to-cart cycle. */
const TEE_BUNDLE_ADD_TIMEOUT = 18000;
/** Timeout while waiting for the live artwork form to be available. */
const TEE_BUNDLE_FORM_TIMEOUT = 8000;
/** Timeout while waiting for the native add-to-cart button to become ready. */
const TEE_BUNDLE_BUTTON_TIMEOUT = 10000;
/** Timeout while waiting for Teeinblue lifecycle hooks. */
const TEE_BUNDLE_HOOK_TIMEOUT = 10000;
/** Polling interval while waiting for the cart to update. */
const TEE_BUNDLE_CART_POLL_INTERVAL = 500;
/** Hard limit for dynamic controller reconciliation loops. */
const TEE_BUNDLE_VARIANT_APPLY_MAX_STEPS = 24;
/** Console prefix for bundle logs. */
const TEE_BUNDLE_LOG_PREFIX = '[tee_bundle]';
/** Bridge state used to capture the next Teeinblue lifecycle hook payload. */
const tee_bundle_hookBridge = {
activeCapture: null,
};
/** Constants and shared state - END */
/** Debug logging - START */
/** Write an informational debug log. */
function tee_bundle_log(...args) {
console.log(TEE_BUNDLE_LOG_PREFIX, ...args);
}
/** Write a warning debug log. */
function tee_bundle_warn(...args) {
console.warn(TEE_BUNDLE_LOG_PREFIX, ...args);
}
/** Write an error debug log. */
function tee_bundle_error(...args) {
console.error(TEE_BUNDLE_LOG_PREFIX, ...args);
}
/** Debug logging - END */
/** Timing helpers - START */
/** Sleep for a fixed amount of time. */
function tee_bundle_wait(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/** Timing helpers - END */
/** Text, formatting và money helpers - START */
/** Escape HTML before rendering text inside injected markup. */
function tee_bundle_escapeHtml(value) {
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
};
return String(value == null ? '' : value).replace(/[&<>"']/g, (char) => map[char]);
}
/** Escape CSS selectors that may contain special characters. */
function tee_bundle_cssEscape(value) {
if (window.CSS && typeof window.CSS.escape === 'function') {
return window.CSS.escape(value);
}
return String(value).replace(/([ !"#$%&'()*+,./:;<=>?@[\]^`{|}~])/g, '\$1');
}
/** Normalize text for stable matching across different Teeinblue controls. */
function tee_bundle_normalizeText(value) {
return String(value == null ? '' : value)
.replace(/\s+/g, ' ')
.replace(/[“”]/g, '"')
.replace(/[‘’]/g, "'")
.trim()
.toLowerCase();
}
/** Compare two values after normalization. */
function tee_bundle_isSameText(a, b) {
return tee_bundle_normalizeText(a) === tee_bundle_normalizeText(b);
}
/** Deduplicate a list of display values while preserving order. */
function tee_bundle_uniqueTexts(values) {
const output = [];
const seen = new Set();
values.forEach((value) => {
const normalized = tee_bundle_normalizeText(value);
if (!normalized || seen.has(normalized)) return;
seen.add(normalized);
output.push(String(value).trim());
});
return output;
}
/** Resolve the active storefront currency. */
function tee_bundle_getCurrencyCode() {
return (
window.teeinblueShop?.shopCurrency ||
window.Shopify?.currency?.active ||
window.Shopify?.currency?.default ||
window.teeinblueCampaign?.currency ||
'USD'
);
}
/** Format a price amount expressed in the store's minor unit. */
function tee_bundle_formatMoney(amount) {
const safeAmount = Number(amount || 0) / 100;
const currency = tee_bundle_getCurrencyCode();
try {
return new Intl.NumberFormat(undefined, {
style: 'currency',
currency,
}).format(safeAmount);
} catch (error) {
return `${currency} ${safeAmount.toFixed(2)}`;
}
}
/** Create a safe deep clone for logging or payload validation. */
function tee_bundle_deepClone(value) {
if (typeof structuredClone === 'function') {
try {
return structuredClone(value);
} catch (error) {}
}
try {
return JSON.parse(JSON.stringify(value));
} catch (error) {
return value;
}
}
/** Text, formatting và money helpers - END */
/** DOM helpers - START */
/** Dispatch synthetic input and change events for a form control. */
function tee_bundle_dispatchChange(el) {
if (!el) return;
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
}
/** Emit a generic cart refresh event for themes that listen to it. */
function tee_bundle_emitCartRefresh() {
TEE_EVENTS_AFTER_CART_ADDED.forEach(eventName => document.dispatchEvent(new CustomEvent(eventName)));
}
/**
* Auto-scroll blocking is intentionally disabled.
* The bundle flow must never prevent user-driven scrolling.
*/
function tee_bundle_holdScrollPosition() {
const startX = window.scrollX || window.pageXOffset || 0;
const startY = window.scrollY || window.pageYOffset || 0;
const previousScrollRestoration = 'scrollRestoration' in history ? history.scrollRestoration : null;
let active = true;
let rafId = null;
if (previousScrollRestoration !== null) {
try {
history.scrollRestoration = 'manual';
} catch (error) {}
}
const restore = () => {
window.scrollTo(startX, startY);
};
const tick = () => {
if (!active) return;
const currentX = window.scrollX || window.pageXOffset || 0;
const currentY = window.scrollY || window.pageYOffset || 0;
if (Math.abs(currentX - startX) > 1 || Math.abs(currentY - startY) > 1) {
restore();
}
rafId = window.requestAnimationFrame(tick);
};
restore();
rafId = window.requestAnimationFrame(tick);
return () => {
active = false;
if (rafId) {
window.cancelAnimationFrame(rafId);
}
restore();
if (previousScrollRestoration !== null) {
try {
history.scrollRestoration = previousScrollRestoration;
} catch (error) {}
}
};
}
/** Check whether an element is visible in the current document flow. */
function tee_bundle_isVisible(el) {
if (!el || !el.isConnected) return false;
return !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length);
}
/** DOM helpers - END */
/** Campaign data - START */
/** Return the current Teeinblue campaign object when available. */
function tee_bundle_getCampaign() {
const campaign = window.teeinblueCampaign;
if (!campaign || typeof campaign !== 'object') return null;
return campaign;
}
/** Check whether the current page is a Teeinblue product page. */
function tee_bundle_isSupportedCampaign() {
const campaign = tee_bundle_getCampaign();
return !!(campaign && campaign.isTeeInBlueProduct === true);
}
/** Read all bundle-capable campaign variants from window.teeinblueCampaign.variantsById. */
function tee_bundle_getCampaignVariants() {
const campaign = tee_bundle_getCampaign();
const variantsById = campaign?.variantsById;
if (!variantsById || typeof variantsById !== 'object') {
return [];
}
return Object.values(variantsById)
.map((variant) => {
const options = Array.isArray(variant.options)
? variant.options.map((item) => String(item || '').trim()).filter(Boolean)
: [variant.option1, variant.option2, variant.option3]
.map((item) => String(item || '').trim())
.filter(Boolean);
return {
id: Number(variant.id),
title: String(variant.public_title || variant.title || '').trim(),
options,
available: variant.available !== false,
price: Number(variant.price || 0),
raw: variant,
};
})
.filter((variant) => Number.isFinite(variant.id) && variant.id > 0);
}
/** Resolve a campaign variant by id. */
function tee_bundle_findCampaignVariantById(variantId) {
return tee_bundle_getCampaignVariants().find((item) => String(item.id) === String(variantId)) || null;
}
/** Campaign data - END */
/** Artwork root resolution - START */
/** Resolve the Teeinblue artwork root. */
function tee_bundle_getArtworkRoot(root = document) {
if (root instanceof HTMLElement && root.id === 'tee-artwork-form') {
return root;
}
return root.querySelector?.('#tee-artwork-form') || null;
}
/** Resolve the parent form associated with the Teeinblue artwork root. */
function tee_bundle_getArtworkForm(artworkRoot) {
return artworkRoot?.closest('form') || null;
}
/** Resolve the native Teeinblue add-to-cart button. */
function tee_bundle_getTeeAtcButton(artworkRoot) {
return tee_bundle_getArtworkForm(artworkRoot)?.querySelector('#teeAtcButton') || null;
}
/** Resolve all quantity inputs associated with the native Teeinblue form. */
function tee_bundle_getQuantityInputs(artworkRoot) {
const form = tee_bundle_getArtworkForm(artworkRoot);
if (!form) return [];
const nodes = Array.from(
form.querySelectorAll('.tee-form-actions .tee-quantity-input, .tee-form-actions input[name="quantity"]')
);
return Array.from(new Set(nodes));
}
/** Build a stable identity for the current Teeinblue form instance. */
function tee_bundle_getArtworkIdentity(artworkRoot) {
const form = tee_bundle_getArtworkForm(artworkRoot);
return {
formId: form?.id || '',
productId: form?.getAttribute('data-productid') || '',
sectionId:
form?.querySelector('input[name="section-id"]')?.value ||
form?.querySelector('input[name="section_id"]')?.value ||
'',
};
}
/** Score a candidate artwork root during live re-resolution. */
function tee_bundle_scoreArtworkRoot(artworkRoot, identity) {
let score = 0;
if (!(artworkRoot instanceof HTMLElement)) return score;
if (artworkRoot.id === 'tee-artwork-form') score += 4;
if (tee_bundle_isVisible(artworkRoot)) score += 4;
const form = tee_bundle_getArtworkForm(artworkRoot);
if (form) score += 2;
if (identity.formId && form?.id === identity.formId) score += 6;
if (identity.productId && (form?.getAttribute('data-productid') || '') === identity.productId) score += 4;
const sectionId =
form?.querySelector('input[name="section-id"]')?.value ||
form?.querySelector('input[name="section_id"]')?.value ||
'';
if (identity.sectionId && sectionId === identity.sectionId) score += 4;
if (tee_bundle_getTeeAtcButton(artworkRoot)) score += 2;
return score;
}
/** Resolve the current live artwork root after any Teeinblue re-render. */
function tee_bundle_resolveLiveArtworkRoot(artworkRootOrIdentity) {
if (!artworkRootOrIdentity) return null;
if (
artworkRootOrIdentity instanceof HTMLElement &&
artworkRootOrIdentity.isConnected &&
artworkRootOrIdentity.id === 'tee-artwork-form'
) {
return artworkRootOrIdentity;
}
const identity =
artworkRootOrIdentity instanceof HTMLElement
? tee_bundle_getArtworkIdentity(artworkRootOrIdentity)
: artworkRootOrIdentity;
const candidates = Array.from(document.querySelectorAll('#tee-artwork-form'));
candidates.sort((a, b) => tee_bundle_scoreArtworkRoot(b, identity) - tee_bundle_scoreArtworkRoot(a, identity));
return candidates[0] || null;
}
/** Wait for the live artwork root to exist again after a re-render cycle. */
async function tee_bundle_waitForLiveArtworkRoot(artworkRootOrIdentity, timeout = TEE_BUNDLE_FORM_TIMEOUT) {
const startedAt = Date.now();
while (Date.now() - startedAt < timeout) {
const liveArtworkRoot = tee_bundle_resolveLiveArtworkRoot(artworkRootOrIdentity);
if (liveArtworkRoot) {
return liveArtworkRoot;
}
await tee_bundle_wait(120);
}
throw new Error('The live Teeinblue artwork form could not be found.');
}
/** Wait for the native add-to-cart button to become ready again. */
async function tee_bundle_waitForButtonReady(artworkRootOrIdentity, timeout = TEE_BUNDLE_BUTTON_TIMEOUT) {
const startedAt = Date.now();
while (Date.now() - startedAt < timeout) {
const liveArtworkRoot = await tee_bundle_waitForLiveArtworkRoot(artworkRootOrIdentity);
const button = tee_bundle_getTeeAtcButton(liveArtworkRoot);
if (
button &&
button.isConnected &&
!button.disabled &&
button.getAttribute('disabled') == null &&
button.getAttribute('aria-disabled') !== 'true' &&
!button.classList.contains('loading') &&
!button.classList.contains('is-loading')
) {
return { liveArtworkRoot, button };
}
await tee_bundle_wait(150);
}
throw new Error('The add-to-cart button did not become ready in time.');
}
/** Artwork root resolution - END */
/** Teeinblue controller inspection - START */
/** Build normalized option universes for each option index from campaign variants. */
function tee_bundle_getCampaignOptionUniverses() {
const variants = tee_bundle_getCampaignVariants();
const universes = [];
variants.forEach((variant) => {
(variant.options || []).forEach((optionValue, optionIndex) => {
if (!universes[optionIndex]) {
universes[optionIndex] = new Set();
}
universes[optionIndex].add(tee_bundle_normalizeText(optionValue));
});
});
return universes;
}
/** Resolve the visible Teeinblue option controllers currently rendered inside #tee-artwork-form. */
function tee_bundle_getArtworkVariantControllers(artworkRoot) {
if (!artworkRoot) return [];
const controllers = [];
const availableProductController =
artworkRoot.querySelector('.tee-available-products .tee-field__input') ||
artworkRoot.querySelector('.tee-available-products');
if (availableProductController && tee_bundle_isVisible(availableProductController)) {
controllers.push(availableProductController);
}
const variantControllers = Array.from(artworkRoot.querySelectorAll('.tee-block.tee-variants .tee-option')).filter(
tee_bundle_isVisible
);
controllers.push(...variantControllers);
return controllers;
}
/** Resolve the display text for a specific Teeinblue input. */
function tee_bundle_getControllerInputDisplayText(artworkRoot, input) {
if (!artworkRoot || !input) return '';
const label = input.id ? artworkRoot.querySelector(`label[for="${tee_bundle_cssEscape(input.id)}"]`) : null;
const labelText =
label?.querySelector('span:not(.sr-only)')?.textContent ||
label?.querySelector('.tee-radio-label__text')?.textContent ||
label?.textContent ||
'';
const candidates = [
label?.getAttribute('data-title'),
label?.getAttribute('title'),
input.getAttribute?.('data-title'),
input.getAttribute?.('title'),
labelText,
input.value,
];
return String(candidates.find((item) => tee_bundle_normalizeText(item)) || '').trim();
}
/** Resolve all selectable display values for a Teeinblue controller. */
function tee_bundle_getControllerChoiceValues(artworkRoot, controller) {
if (!artworkRoot || !controller) return [];
const radioInputs = Array.from(controller.querySelectorAll('input[type="radio"]'));
if (radioInputs.length) {
return tee_bundle_uniqueTexts(
radioInputs.map((input) => tee_bundle_getControllerInputDisplayText(artworkRoot, input))
);
}
const select = controller.querySelector('select');
if (select) {
return tee_bundle_uniqueTexts(
Array.from(select.options)
.map((option) => String(option.textContent || option.value || '').trim())
.filter(Boolean)
);
}
return [];
}
/** Resolve the current selected value for a visible Teeinblue controller. */
function tee_bundle_getControllerCurrentValue(artworkRoot, controller) {
if (!artworkRoot || !controller) return '';
const checkedInput = controller.querySelector('input[type="radio"]:checked');
if (checkedInput) {
return tee_bundle_getControllerInputDisplayText(artworkRoot, checkedInput);
}
const activeInput = controller.querySelector('.active input[type="radio"]');
if (activeInput) {
return tee_bundle_getControllerInputDisplayText(artworkRoot, activeInput);
}
const select = controller.querySelector('select');
if (select) {
const selectedOption = select.selectedOptions?.[0] || select.options?.[select.selectedIndex] || null;
if (selectedOption) {
return String(selectedOption.textContent || selectedOption.value || '').trim();
}
}
return '';
}
/** Build descriptors for all currently visible Teeinblue controllers. */
function tee_bundle_getArtworkControllerDescriptors(artworkRoot) {
return tee_bundle_getArtworkVariantControllers(artworkRoot)
.map((controller, index) => {
const choiceValues = tee_bundle_getControllerChoiceValues(artworkRoot, controller);
return {
key: `controller-${index}`,
index,
controller,
currentValue: tee_bundle_getControllerCurrentValue(artworkRoot, controller),
choiceValues,
};
})
.filter((descriptor) => descriptor.choiceValues.length > 0);
}
/**
* Score how likely a controller belongs to a specific option index,
* using campaign-wide option universes instead of UI labels.
*/
function tee_bundle_scoreDescriptorForOptionIndex(descriptor, optionIndex, targetVariant = null) {
const universes = tee_bundle_getCampaignOptionUniverses();
const optionUniverse = universes[optionIndex] || new Set();
let score = 0;
descriptor.choiceValues.forEach((choiceValue) => {
if (optionUniverse.has(tee_bundle_normalizeText(choiceValue))) {
score += 2;
}
});
if (descriptor.currentValue && optionUniverse.has(tee_bundle_normalizeText(descriptor.currentValue))) {
score += 1;
}
if (targetVariant && targetVariant.options?.[optionIndex]) {
const targetValue = targetVariant.options[optionIndex];
if (descriptor.choiceValues.some((choiceValue) => tee_bundle_isSameText(choiceValue, targetValue))) {
score += 6;
}
if (tee_bundle_isSameText(descriptor.currentValue, targetValue)) {
score += 3;
}
}
return score;
}
/**
* Resolve candidate option indexes for a descriptor.
* When targetVariant is provided, only indexes that can actually host the target value are returned.
*/
function tee_bundle_getDescriptorCandidateIndexes(descriptor, targetVariant = null, mode = 'target') {
const maxOptionCount = Math.max(
...tee_bundle_getCampaignVariants().map((variant) => variant.options?.length || 0),
targetVariant?.options?.length || 0,
0
);
const candidates = [];
for (let optionIndex = 0; optionIndex < maxOptionCount; optionIndex += 1) {
const score = tee_bundle_scoreDescriptorForOptionIndex(descriptor, optionIndex, targetVariant);
if (score <= 0) continue;
if (mode === 'target' && targetVariant?.options?.[optionIndex]) {
const targetValue = targetVariant.options[optionIndex];
const supportsTarget = descriptor.choiceValues.some((choiceValue) =>
tee_bundle_isSameText(choiceValue, targetValue)
);
if (!supportsTarget) {
continue;
}
}
if (mode === 'current') {
const universes = tee_bundle_getCampaignOptionUniverses();
const optionUniverse = universes[optionIndex] || new Set();
if (!descriptor.currentValue || !optionUniverse.has(tee_bundle_normalizeText(descriptor.currentValue))) {
continue;
}
}
candidates.push({
optionIndex,
score,
});
}
candidates.sort((a, b) => b.score - a.score);
return candidates;
}
/**
* Assign visible controllers to unique option indexes.
* This is label-independent and relies on campaign data plus the currently visible DOM values.
*/
function tee_bundle_assignControllersToOptionIndexes(descriptors, targetVariant = null, mode = 'target') {
if (!Array.isArray(descriptors) || !descriptors.length) {
return null;
}
const items = descriptors.map((descriptor) => {
return {
descriptor,
candidates: tee_bundle_getDescriptorCandidateIndexes(descriptor, targetVariant, mode),
};
});
if (items.some((item) => !item.candidates.length)) {
return null;
}
items.sort((a, b) => a.candidates.length - b.candidates.length);
const assigned = new Map();
const usedIndexes = new Set();
function backtrack(pointer) {
if (pointer >= items.length) {
return true;
}
const item = items[pointer];
for (const candidate of item.candidates) {
if (usedIndexes.has(candidate.optionIndex)) continue;
usedIndexes.add(candidate.optionIndex);
assigned.set(item.descriptor.key, candidate.optionIndex);
if (backtrack(pointer + 1)) {
return true;
}
assigned.delete(item.descriptor.key);
usedIndexes.delete(candidate.optionIndex);
}
return false;
}
return backtrack(0) ? assigned : null;
}
/** Infer the current campaign variant from the currently visible DOM state. */
function tee_bundle_getCurrentArtworkVariant(artworkRoot) {
const descriptors = tee_bundle_getArtworkControllerDescriptors(artworkRoot);
if (!descriptors.length) return null;
const variants = tee_bundle_getCampaignVariants();
const matches = variants.filter((variant) => {
const assignment = tee_bundle_assignControllersToOptionIndexes(descriptors, variant, 'target');
if (!assignment) return false;
return descriptors.every((descriptor) => {
const optionIndex = assignment.get(descriptor.key);
if (typeof optionIndex !== 'number') return false;
const expectedValue = variant.options?.[optionIndex];
if (!expectedValue) return false;
return tee_bundle_isSameText(descriptor.currentValue, expectedValue);
});
});
return matches[0] || null;
}
/** Build visible controller plans for a target variant. */
function tee_bundle_getTargetControllerPlans(artworkRoot, targetVariant) {
const descriptors = tee_bundle_getArtworkControllerDescriptors(artworkRoot);
if (!descriptors.length) return [];
const assignment = tee_bundle_assignControllersToOptionIndexes(descriptors, targetVariant, 'target');
if (!assignment) return [];
return descriptors
.map((descriptor) => {
const optionIndex = assignment.get(descriptor.key);
if (typeof optionIndex !== 'number') return null;
return {
key: descriptor.key,
controller: descriptor.controller,
currentValue: descriptor.currentValue,
choiceValues: descriptor.choiceValues,
optionIndex,
targetValue: targetVariant.options?.[optionIndex] || '',
};
})
.filter(Boolean);
}
/**
* Locate the best visible controller for a target option index without relying on labels.
* This is especially important for Available Product because merchants can rename labels freely.
*/
function tee_bundle_findBestControllerForOptionIndex(artworkRoot, targetVariant, optionIndex) {
const descriptors = tee_bundle_getArtworkControllerDescriptors(artworkRoot);
const targetValue = targetVariant?.options?.[optionIndex];
if (!targetValue) return null;
const candidates = descriptors
.map((descriptor) => {
const supportsTarget = descriptor.choiceValues.some((choiceValue) =>
tee_bundle_isSameText(choiceValue, targetValue)
);
if (!supportsTarget) return null;
return {
descriptor,
score: tee_bundle_scoreDescriptorForOptionIndex(descriptor, optionIndex, targetVariant),
};
})
.filter(Boolean)
.sort((a, b) => b.score - a.score);
return candidates[0]?.descriptor || null;
}
/**
* Wait until the DOM becomes compatible with the target product context after Available Product changes.
* During Teeinblue re-render, old controllers may remain briefly; that state must not be treated as a hard failure.
*/
async function tee_bundle_waitForTargetProductContext(artworkRootOrIdentity, targetVariant, timeout = 8000) {
const startedAt = Date.now();
while (Date.now() - startedAt < timeout) {
const liveArtworkRoot = await tee_bundle_waitForLiveArtworkRoot(artworkRootOrIdentity);
const currentVariant = tee_bundle_getCurrentArtworkVariant(liveArtworkRoot);
if (currentVariant && String(currentVariant.id) === String(targetVariant.id)) {
return liveArtworkRoot;
}
const availableProductController = tee_bundle_findBestControllerForOptionIndex(
liveArtworkRoot,
targetVariant,
0
);
const targetPlans = tee_bundle_getTargetControllerPlans(liveArtworkRoot, targetVariant);
if (availableProductController || targetPlans.length) {
return liveArtworkRoot;
}
await tee_bundle_wait(120);
}
throw new Error('The selected item is not available right now.');
}
/** Click a target value inside a visible controller. */
async function tee_bundle_clickControllerValue(artworkRootOrIdentity, controller, targetValue) {
const liveArtworkRoot = await tee_bundle_waitForLiveArtworkRoot(artworkRootOrIdentity);
if (!controller || !controller.isConnected) return false;
const radioInputs = Array.from(controller.querySelectorAll('input[type="radio"]'));
const matchingRadio = radioInputs.find((input) => {
return tee_bundle_isSameText(tee_bundle_getControllerInputDisplayText(liveArtworkRoot, input), targetValue);
});
if (matchingRadio) {
const label = matchingRadio.id
? liveArtworkRoot.querySelector(`label[for="${tee_bundle_cssEscape(matchingRadio.id)}"]`)
: null;
if (label) {
label.click();
} else {
matchingRadio.checked = true;
tee_bundle_dispatchChange(matchingRadio);
}
return true;
}
const select = controller.querySelector('select');
if (select) {
const option = Array.from(select.options).find((opt) => {
return tee_bundle_isSameText(opt.textContent || opt.value || '', targetValue);
});
if (option) {
select.value = option.value;
tee_bundle_dispatchChange(select);
return true;
}
}
return false;
}
/**
* Wait until a specific target value is reflected on the live DOM,
* or until Teeinblue finishes a re-render and the DOM becomes compatible with the target variant.
*/
async function tee_bundle_waitForControllerTransition(
artworkRootOrIdentity,
targetVariant,
targetValue,
timeout = 7000
) {
const startedAt = Date.now();
while (Date.now() - startedAt < timeout) {
const liveArtworkRoot = await tee_bundle_waitForLiveArtworkRoot(artworkRootOrIdentity);
const currentVariant = tee_bundle_getCurrentArtworkVariant(liveArtworkRoot);
if (currentVariant && String(currentVariant.id) === String(targetVariant.id)) {
return liveArtworkRoot;
}
const plans = tee_bundle_getTargetControllerPlans(liveArtworkRoot, targetVariant);
if (!plans.length) {
await tee_bundle_wait(120);
continue;
}
const explicitMatch = plans.find((plan) => tee_bundle_isSameText(plan.targetValue, targetValue));
if (explicitMatch && tee_bundle_isSameText(explicitMatch.currentValue, explicitMatch.targetValue)) {
return liveArtworkRoot;
}
const visiblePlansAligned = plans.every((plan) => tee_bundle_isSameText(plan.currentValue, plan.targetValue));
if (visiblePlansAligned) {
return liveArtworkRoot;
}
await tee_bundle_wait(120);
}
throw new Error(`The option "${targetValue}" could not be applied.`);
}
/**
* Apply a target variant using only #tee-artwork-form and campaign data.
* This implementation never relies on option labels and does not fail during transient DOM states.
*/
async function tee_bundle_setArtworkVariant(artworkRootOrIdentity, targetVariant) {
const startedAt = Date.now();
let step = 0;
tee_bundle_log('variant apply start', {
variantId: targetVariant.id,
title: targetVariant.title,
options: targetVariant.options,
});
while (Date.now() - startedAt < 15000 && step < TEE_BUNDLE_VARIANT_APPLY_MAX_STEPS) {
step += 1;
const liveArtworkRoot = await tee_bundle_waitForLiveArtworkRoot(artworkRootOrIdentity);
const currentVariant = tee_bundle_getCurrentArtworkVariant(liveArtworkRoot);
if (currentVariant && String(currentVariant.id) === String(targetVariant.id)) {
tee_bundle_log('variant apply complete by exact match', {
variantId: targetVariant.id,
step,
});
return liveArtworkRoot;
}
/**
* Step 1: force Available Product first when needed.
* This is handled independently because changing it can invalidate or replace all remaining controllers.
*/
const productController = tee_bundle_findBestControllerForOptionIndex(liveArtworkRoot, targetVariant, 0);
if (productController && !tee_bundle_isSameText(productController.currentValue, targetVariant.options[0])) {
tee_bundle_log('applying available product', {
step,
currentValue: productController.currentValue,
targetValue: targetVariant.options[0],
});
const clicked = await tee_bundle_clickControllerValue(
artworkRootOrIdentity,
productController.controller,
targetVariant.options[0]
).catch((error) => {
tee_bundle_error('available product click failed', error);
return false;
});
if (!clicked) {
throw new Error(`The option "${targetVariant.options[0]}" is not available right now.`);
}
await tee_bundle_waitForTargetProductContext(artworkRootOrIdentity, targetVariant, 8000);
await tee_bundle_waitForButtonReady(artworkRootOrIdentity, 8000);
await tee_bundle_wait(250);
continue;
}
/**
* Step 2: after product context is correct, reconcile the remaining visible controllers.
* If the DOM is still in transition and cannot be mapped yet, keep waiting instead of failing.
*/
const plans = tee_bundle_getTargetControllerPlans(liveArtworkRoot, targetVariant);
tee_bundle_log('variant reconciliation', {
step,
plans: plans.map((plan) => ({
optionIndex: plan.optionIndex,
currentValue: plan.currentValue,
targetValue: plan.targetValue,
choiceValues: plan.choiceValues,
})),
});
if (!plans.length) {
await tee_bundle_wait(120);
continue;
}
const mismatch = plans.find((plan) => !tee_bundle_isSameText(plan.currentValue, plan.targetValue));
if (!mismatch) {
await tee_bundle_waitForButtonReady(artworkRootOrIdentity, 8000);
await tee_bundle_wait(350);
const reResolvedRoot = await tee_bundle_waitForLiveArtworkRoot(artworkRootOrIdentity);
const maybeVariant = tee_bundle_getCurrentArtworkVariant(reResolvedRoot);
if (maybeVariant && String(maybeVariant.id) === String(targetVariant.id)) {
tee_bundle_log('variant apply complete after stabilization', {
variantId: targetVariant.id,
step,
});
return reResolvedRoot;
}
tee_bundle_warn('visible controllers are aligned; exact variant could not be confirmed from DOM only', {
targetVariantId: targetVariant.id,
targetTitle: targetVariant.title,
});
return reResolvedRoot;
}
tee_bundle_log('applying controller mismatch', {
step,
optionIndex: mismatch.optionIndex,
currentValue: mismatch.currentValue,
targetValue: mismatch.targetValue,
});
const clicked = await tee_bundle_clickControllerValue(
artworkRootOrIdentity,
mismatch.controller,
mismatch.targetValue
).catch((error) => {
tee_bundle_error('controller click failed', {
step,
optionIndex: mismatch.optionIndex,
targetValue: mismatch.targetValue,
error,
});
return false;
});
if (!clicked) {
throw new Error(`The option "${mismatch.targetValue}" is not available right now.`);
}
await tee_bundle_waitForControllerTransition(artworkRootOrIdentity, targetVariant, mismatch.targetValue, 7000);
await tee_bundle_waitForLiveArtworkRoot(artworkRootOrIdentity, 7000);
await tee_bundle_waitForButtonReady(artworkRootOrIdentity, 8000);
await tee_bundle_wait(250);
}
throw new Error(`The variant "${targetVariant.title}" could not be applied.`);
}
/** Teeinblue controller inspection - END */
/** Quantity và snapshot helpers - START */
/** Update all native Teeinblue quantity inputs for the current form. */
function tee_bundle_setArtworkQuantity(artworkRoot, quantity) {
const safeQty = Math.max(1, Number(quantity) || 1);
tee_bundle_getQuantityInputs(artworkRoot).forEach((input) => {
input.value = safeQty;
tee_bundle_dispatchChange(input);
});
}
/** Snapshot the current artwork state before the bundle flow mutates it. */
function tee_bundle_snapshotArtworkState(artworkRoot) {
const currentVariant = tee_bundle_getCurrentArtworkVariant(artworkRoot);
return {
artworkIdentity: tee_bundle_getArtworkIdentity(artworkRoot),
variantId: currentVariant?.id || null,
quantityValues: tee_bundle_getQuantityInputs(artworkRoot).map((input) => String(input.value || '1')),
};
}
/** Restore all quantity inputs to their original values. */
function tee_bundle_restoreArtworkQuantityState(artworkRoot, quantityValues) {
const inputs = tee_bundle_getQuantityInputs(artworkRoot);
if (!Array.isArray(quantityValues) || !inputs.length) return;
inputs.forEach((input, index) => {
input.value = quantityValues[index] || quantityValues[0] || '1';
tee_bundle_dispatchChange(input);
});
}
/** Restore the original artwork state after the bundle flow completes. */
async function tee_bundle_restoreArtworkState(snapshot) {
if (!snapshot) return;
tee_bundle_log('restoring artwork state', snapshot);
const liveArtworkRoot = await tee_bundle_waitForLiveArtworkRoot(snapshot.artworkIdentity);
if (snapshot.variantId) {
const targetVariant = tee_bundle_findCampaignVariantById(snapshot.variantId);
if (targetVariant) {
await tee_bundle_setArtworkVariant(snapshot.artworkIdentity, targetVariant);
}
}
const restoredArtworkRoot = await tee_bundle_waitForLiveArtworkRoot(snapshot.artworkIdentity);
tee_bundle_restoreArtworkQuantityState(restoredArtworkRoot, snapshot.quantityValues);
await tee_bundle_wait(220);
}
/** Quantity và snapshot helpers - END */
/** Cart API và rollback - START */
/** Read the current cart state from /cart.js. */
async function tee_bundle_fetchCart() {
const response = await fetch('/cart.js', {
headers: {
Accept: 'application/json',
},
});
if (!response.ok) {
throw new Error('Unable to read the current cart.');
}
return response.json();
}
/** Resolve the total cart item count from a cart payload. */
function tee_bundle_getCartItemCount(cart) {
if (typeof cart?.item_count === 'number') {
return cart.item_count;
}
return (cart?.items || []).reduce((sum, item) => {
return sum + (Number(item.quantity) || 0);
}, 0);
}
/** Wait until the cart item count increases after a native Teeinblue add-to-cart action. */
function tee_bundle_waitForCartIncrease(beforeCount, expectedIncrease) {
return (async () => {
const startedAt = Date.now();
while (Date.now() - startedAt < TEE_BUNDLE_ADD_TIMEOUT) {
const afterCart = await tee_bundle_fetchCart();
const afterCount = tee_bundle_getCartItemCount(afterCart);
if (afterCount >= beforeCount + expectedIncrease) {
return afterCart;
}
await tee_bundle_wait(TEE_BUNDLE_CART_POLL_INTERVAL);
}
throw new Error('The cart did not update after adding the selected item.');
})();
}
/** Update cart line quantities using cart/update.js and line keys. */
async function tee_bundle_updateCartByKeyQuantities(updates) {
const params = new URLSearchParams();
updates.forEach((quantity, key) => {
params.append(`updates[${key}]`, String(quantity));
});
const response = await fetch('/cart/update.js', {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
},
body: params.toString(),
});
if (!response.ok) {
throw new Error('Unable to restore the previous cart state.');
}
return response.json();
}
/** Restore the cart back to its original snapshot if the bundle flow fails. */
async function tee_bundle_rollbackCartToSnapshot(originalCart) {
const currentCart = await tee_bundle_fetchCart();
const originalQtyByKey = new Map(
(originalCart?.items || []).map((item) => [item.key, Number(item.quantity) || 0])
);
const updates = new Map();
for (const item of currentCart?.items || []) {
const originalQty = originalQtyByKey.get(item.key) || 0;
const currentQty = Number(item.quantity) || 0;
if (currentQty !== originalQty) {
updates.set(item.key, originalQty);
}
}
if (!updates.size) {
return currentCart;
}
tee_bundle_log('rolling back cart', {
updates: Array.from(updates.entries()),
});
const restoredCart = await tee_bundle_updateCartByKeyQuantities(updates);
await tee_bundle_wait(250);
tee_bundle_emitCartRefresh();
return restoredCart;
}
/** Cart API và rollback - END */
/** Teeinblue hooks bridge - START */
/** Resolve the Teeinblue API object on window. */
function tee_bundle_getTeeinblueApi() {
if (!window.teeinblue || typeof window.teeinblue !== 'object') {
return null;
}
return window.teeinblue;
}
/** Clear the active hook capture session. */
function tee_bundle_clearHookCapture() {
const capture = tee_bundle_hookBridge.activeCapture;
if (capture?.timer) {
clearTimeout(capture.timer);
}
tee_bundle_hookBridge.activeCapture = null;
}
/** Resolve the active hook capture promise. */
function tee_bundle_resolveHookCapture(payload) {
const capture = tee_bundle_hookBridge.activeCapture;
if (!capture || capture.done) return;
capture.done = true;
const resolve = capture.resolve;
tee_bundle_clearHookCapture();
resolve(payload);
}
/** Reject the active hook capture promise. */
function tee_bundle_rejectHookCapture(error) {
const capture = tee_bundle_hookBridge.activeCapture;
if (!capture || capture.done) return;
capture.done = true;
const reject = capture.reject;
tee_bundle_clearHookCapture();
reject(error);
}
/**
* Install wrappers around the Teeinblue lifecycle hooks while preserving any existing implementation.
* The captured payload is used to validate the exact variant and quantity before the cart update is accepted.
*/
function tee_bundle_installTeeinblueHooks() {
const api = tee_bundle_getTeeinblueApi();
if (!api) return false;
if (!Object.prototype.hasOwnProperty.call(api, '__tee_bundle_originalTransformBeforeAddToCart')) {
api.__tee_bundle_originalTransformBeforeAddToCart = api.transformBeforeAddToCart;
}
if (!Object.prototype.hasOwnProperty.call(api, '__tee_bundle_originalBeforeAddToCart')) {
api.__tee_bundle_originalBeforeAddToCart = api.beforeAddToCart;
}
if (!api.__tee_bundle_transformWrapped) {
api.transformBeforeAddToCart = async function (input) {
tee_bundle_log('transformBeforeAddToCart', tee_bundle_deepClone(input));
tee_bundle_resolveHookCapture({
hook: 'transformBeforeAddToCart',
input: tee_bundle_deepClone(input),
});
const original = api.__tee_bundle_originalTransformBeforeAddToCart;
if (typeof original === 'function') {
return await original(input);
}
return input;
};
api.__tee_bundle_transformWrapped = true;
}
if (!api.__tee_bundle_beforeWrapped) {
api.beforeAddToCart = async function (customizationId, customizationData, personalizedData) {
tee_bundle_log('beforeAddToCart', {
customizationId,
customizationData: tee_bundle_deepClone(customizationData),
personalizedData: tee_bundle_deepClone(personalizedData),
});
tee_bundle_resolveHookCapture({
hook: 'beforeAddToCart',
customizationId,
customizationData: tee_bundle_deepClone(customizationData),
personalizedData: tee_bundle_deepClone(personalizedData),
});
const original = api.__tee_bundle_originalBeforeAddToCart;
if (typeof original === 'function') {
return await original(customizationId, customizationData, personalizedData);
}
};
api.__tee_bundle_beforeWrapped = true;
}
return true;
}
/** Open a single-use capture session for the next native Teeinblue add-to-cart call. */
function tee_bundle_openHookCapture() {
if (!tee_bundle_installTeeinblueHooks()) {
throw new Error('Teeinblue hooks are not available on this page.');
}
tee_bundle_clearHookCapture();
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
tee_bundle_rejectHookCapture(new Error('Teeinblue did not complete the add-to-cart lifecycle in time.'));
}, TEE_BUNDLE_HOOK_TIMEOUT);
tee_bundle_hookBridge.activeCapture = {
resolve,
reject,
timer,
done: false,
};
});
}
/** Validate the captured Teeinblue hook payload against the expected selection. */
function tee_bundle_validateHookPayload(captured, selected) {
if (!captured) return;
if (captured.hook === 'transformBeforeAddToCart' && captured.input) {
const inputId = Number(captured.input.id);
const inputQuantity = Number(captured.input.quantity || 1);
if (Number.isFinite(inputId) && inputId !== Number(selected.variant.id)) {
throw new Error(`Teeinblue generated payload for variant ${inputId} instead of ${selected.variant.id}.`);
}
if (inputQuantity !== Number(selected.quantity)) {
throw new Error(
`Teeinblue generated quantity ${inputQuantity} instead of ${selected.quantity} for variant ${selected.variant.id}.`
);
}
}
}
/** Teeinblue hooks bridge - END */
/** Bundle UI rendering và state - START */
/** Render the bundle UI using campaign variant data. */
function tee_bundle_renderBundle(variants) {
const wrapper = document.createElement('section');
wrapper.className = `${TEE_BUNDLE_PREFIX}-bundle`;
wrapper.setAttribute('data-tee-bundle-bundle', 'true');
const rowsHtml = variants
.map((variant) => {
return `
<div class="${TEE_BUNDLE_PREFIX}-row" data-tee-bundle-variant-id="${variant.id}">
<label class="${TEE_BUNDLE_PREFIX}-variant">
<input
type="checkbox"
class="${TEE_BUNDLE_PREFIX}-checkbox"
data-tee-bundle-checkbox
value="${variant.id}"
>
<span class="${TEE_BUNDLE_PREFIX}-variant-meta">
<span class="${TEE_BUNDLE_PREFIX}-variant-title">${tee_bundle_escapeHtml(variant.title)}</span>
<span class="${TEE_BUNDLE_PREFIX}-variant-price">
${tee_bundle_escapeHtml(tee_bundle_formatMoney(variant.price))}
</span>
</span>
</label>
<div class="${TEE_BUNDLE_PREFIX}-qty">
<button type="button" class="${TEE_BUNDLE_PREFIX}-qty-btn" data-tee-bundle-qty="minus" disabled>-</button>
<input
type="number"
min="1"
step="1"
value="1"
class="${TEE_BUNDLE_PREFIX}-qty-input"
data-tee-bundle-qty-input
disabled
>
<button type="button" class="${TEE_BUNDLE_PREFIX}-qty-btn" data-tee-bundle-qty="plus" disabled>+</button>
</div>
</div>
`;
})
.join('');
wrapper.innerHTML = `
<div class="${TEE_BUNDLE_PREFIX}-header">
<div>
<h3 class="${TEE_BUNDLE_PREFIX}-title">Bundle Add to Cart</h3>
<p class="${TEE_BUNDLE_PREFIX}-desc">Choose multiple items and add them together.</p>
</div>
</div>
<div class="${TEE_BUNDLE_PREFIX}-list">
${rowsHtml}
</div>
<div class="${TEE_BUNDLE_PREFIX}-summary">
<span class="${TEE_BUNDLE_PREFIX}-summary-label">Bundle total</span>
<strong class="${TEE_BUNDLE_PREFIX}-summary-value" data-tee-bundle-total>${tee_bundle_formatMoney(0)}</strong>
</div>
<div class="${TEE_BUNDLE_PREFIX}-actions">
<button type="button" class="${TEE_BUNDLE_PREFIX}-atc" data-tee-bundle-bundle-atc>
<span class="${TEE_BUNDLE_PREFIX}-atc-label">Add Selected Items to Cart</span>
</button>
</div>
<div class="${TEE_BUNDLE_PREFIX}-status" data-tee-bundle-status aria-live="polite"></div>
`;
return wrapper;
}
/** Update the bundle status message using user-facing language. */
function tee_bundle_setStatus(root, message, type = 'info') {
const status = root.querySelector('[data-tee-bundle-status]');
if (!status) return;
status.className = `${TEE_BUNDLE_PREFIX}-status is-${type}`;
status.textContent = message || '';
}
/** Toggle the enabled state of an individual bundle row. */
function tee_bundle_toggleRow(row, checked) {
row.classList.toggle('is-selected', checked);
row.querySelectorAll('[data-tee-bundle-qty-input], [data-tee-bundle-qty]').forEach((el) => {
el.disabled = !checked;
});
const input = row.querySelector('[data-tee-bundle-qty-input]');
if (input && (!input.value || Number(input.value) < 1)) {
input.value = 1;
}
}
/** Read all selected bundle items from the bundle UI. */
function tee_bundle_getSelectedBundleItems(root, variants) {
return Array.from(root.querySelectorAll(`.${TEE_BUNDLE_PREFIX}-row`))
.map((row) => {
const checkbox = row.querySelector('[data-tee-bundle-checkbox]');
const qtyInput = row.querySelector('[data-tee-bundle-qty-input]');
const variantId = Number(row.getAttribute('data-tee-bundle-variant-id'));
const variant = variants.find((item) => item.id === variantId);
if (!checkbox || !qtyInput || !variant || !checkbox.checked) return null;
return {
variant,
quantity: Math.max(1, Number(qtyInput.value) || 1),
};
})
.filter(Boolean);
}
/** Recalculate and render the bundle total. */
function tee_bundle_updateTotal(root, variants) {
const totalNode = root.querySelector('[data-tee-bundle-total]');
if (!totalNode) return;
const selectedItems = tee_bundle_getSelectedBundleItems(root, variants);
const totalAmount = selectedItems.reduce((sum, item) => {
return sum + item.variant.price * item.quantity;
}, 0);
totalNode.textContent = tee_bundle_formatMoney(totalAmount);
}
/** Toggle the loading state of the bundle add-to-cart button. */
function tee_bundle_setButtonLoading(root, isLoading) {
const button = root.querySelector('[data-tee-bundle-bundle-atc]');
if (!button) return;
if (!button.dataset.defaultHtml) {
button.dataset.defaultHtml = button.innerHTML;
}
if (isLoading) {
button.classList.add('is-loading');
button.innerHTML = `
<span class="${TEE_BUNDLE_PREFIX}-spinner" aria-hidden="true"></span>
<span class="${TEE_BUNDLE_PREFIX}-atc-label">Adding...</span>
`;
} else {
button.classList.remove('is-loading');
button.innerHTML = button.dataset.defaultHtml;
}
}
/** Lock or unlock the bundle UI during processing. */
function tee_bundle_setLoading(root, isLoading) {
root.classList.toggle('is-loading', isLoading);
tee_bundle_setButtonLoading(root, isLoading);
root.querySelectorAll('button, input[type="checkbox"], input[type="number"]').forEach((el) => {
if (el.matches('[data-tee-bundle-checkbox]')) {
el.disabled = isLoading;
return;
}
if (el.matches('[data-tee-bundle-qty-input], [data-tee-bundle-qty]')) {
const row = el.closest(`.${TEE_BUNDLE_PREFIX}-row`);
const checkbox = row ? row.querySelector('[data-tee-bundle-checkbox]') : null;
el.disabled = isLoading || !(checkbox && checkbox.checked);
return;
}
el.disabled = isLoading;
});
}
/** Bundle UI rendering và state - END */
/** Native bundle flow - START */
/** Add one selected bundle item through the native Teeinblue add-to-cart flow. */
async function tee_bundle_nativeAddSelectedItem(artworkIdentity, selected, beforeCart) {
const beforeCount = tee_bundle_getCartItemCount(beforeCart);
tee_bundle_log('native add start', {
variantId: selected.variant.id,
title: selected.variant.title,
quantity: selected.quantity,
});
await tee_bundle_waitForButtonReady(artworkIdentity, 8000);
await tee_bundle_setArtworkVariant(artworkIdentity, selected.variant);
let readyState = await tee_bundle_waitForButtonReady(artworkIdentity, 8000);
let liveArtworkRoot = readyState.liveArtworkRoot;
let teeAtcButton = readyState.button;
tee_bundle_setArtworkQuantity(liveArtworkRoot, selected.quantity);
await tee_bundle_wait(220);
readyState = await tee_bundle_waitForButtonReady(artworkIdentity, 8000);
liveArtworkRoot = readyState.liveArtworkRoot;
teeAtcButton = readyState.button;
const hookPromise = tee_bundle_openHookCapture();
teeAtcButton.click();
const captured = await hookPromise;
tee_bundle_validateHookPayload(captured, selected);
const afterCart = await tee_bundle_waitForCartIncrease(beforeCount, selected.quantity);
await tee_bundle_waitForButtonReady(artworkIdentity, 10000);
await tee_bundle_waitForLiveArtworkRoot(artworkIdentity, 7000);
await tee_bundle_wait(350);
tee_bundle_log('native add complete', {
variantId: selected.variant.id,
quantity: selected.quantity,
});
return afterCart;
}
/** Execute the full bundle flow for all selected rows. */
async function tee_bundle_handleBundleAtc(artworkRoot, root, variants) {
const selectedItems = tee_bundle_getSelectedBundleItems(root, variants);
if (!selectedItems.length) {
tee_bundle_setStatus(root, 'Please choose at least one item.', 'error');
return;
}
const liveArtworkRoot = await tee_bundle_waitForLiveArtworkRoot(artworkRoot);
const snapshot = tee_bundle_snapshotArtworkState(liveArtworkRoot);
const artworkIdentity = snapshot.artworkIdentity;
const releaseScroll = tee_bundle_holdScrollPosition();
tee_bundle_log('bundle flow started', {
items: selectedItems.map((item) => ({
variantId: item.variant.id,
title: item.variant.title,
quantity: item.quantity,
})),
});
tee_bundle_setLoading(root, true);
tee_bundle_setStatus(root, 'Preparing your selected items...', 'info');
let originalCart = null;
let workingCart = null;
try {
originalCart = await tee_bundle_fetchCart();
workingCart = originalCart;
for (let index = 0; index < selectedItems.length; index += 1) {
const selected = selectedItems[index];
tee_bundle_setStatus(root, `Adding item ${index + 1} of ${selectedItems.length}...`, 'info');
workingCart = await tee_bundle_nativeAddSelectedItem(artworkIdentity, selected, workingCart);
await tee_bundle_wait(400);
}
await tee_bundle_restoreArtworkState(snapshot);
tee_bundle_emitCartRefresh();
tee_bundle_setStatus(root, 'Your selected items have been added to the cart.', 'success');
document.dispatchEvent(
new CustomEvent('tee_bundle:bundle:added', {
detail: {
items: selectedItems,
},
})
);
tee_bundle_log('bundle flow completed successfully');
} catch (error) {
tee_bundle_error('bundle flow failed', error);
try {
if (originalCart) {
tee_bundle_setStatus(root, 'Something went wrong. Restoring your cart...', 'info');
await tee_bundle_rollbackCartToSnapshot(originalCart);
}
} catch (rollbackError) {
tee_bundle_error('cart rollback failed', rollbackError);
tee_bundle_setStatus(
root,
'We could not finish adding your items, and part of the cart could not be restored automatically.',
'error'
);
try {
await tee_bundle_restoreArtworkState(snapshot);
} catch (restoreError) {
tee_bundle_error('artwork restore failed after rollback failure', restoreError);
}
tee_bundle_emitCartRefresh();
tee_bundle_setLoading(root, false);
releaseScroll();
return;
}
try {
await tee_bundle_restoreArtworkState(snapshot);
} catch (restoreError) {
tee_bundle_error('final artwork restore failed', restoreError);
}
tee_bundle_emitCartRefresh();
tee_bundle_setStatus(root, 'We could not add all selected items. Your cart has been restored.', 'error');
} finally {
tee_bundle_clearHookCapture();
try {
await tee_bundle_restoreArtworkState(snapshot);
} catch (restoreError) {
tee_bundle_error('final artwork restore failed', restoreError);
}
tee_bundle_setLoading(root, false);
tee_bundle_updateTotal(root, variants);
releaseScroll();
}
}
/** Native bundle flow - END */
/** Bundle event binding - START */
/** Bind bundle UI interactions for the current artwork instance. */
function tee_bundle_bindBundle(artworkRoot, root, variants) {
root.addEventListener('change', (event) => {
const target = event.target;
if (target.matches('[data-tee-bundle-checkbox]')) {
const row = target.closest(`.${TEE_BUNDLE_PREFIX}-row`);
if (row) {
tee_bundle_toggleRow(row, target.checked);
}
tee_bundle_updateTotal(root, variants);
}
if (target.matches('[data-tee-bundle-qty-input]')) {
const nextValue = Math.max(1, Number(target.value) || 1);
target.value = nextValue;
tee_bundle_updateTotal(root, variants);
}
});
root.addEventListener('click', (event) => {
const qtyButton = event.target.closest('[data-tee-bundle-qty]');
if (qtyButton) {
const row = qtyButton.closest(`.${TEE_BUNDLE_PREFIX}-row`);
const input = row ? row.querySelector('[data-tee-bundle-qty-input]') : null;
if (!input) return;
const currentValue = Math.max(1, Number(input.value) || 1);
const action = qtyButton.getAttribute('data-tee-bundle-qty');
input.value = action === 'minus' ? Math.max(1, currentValue - 1) : currentValue + 1;
tee_bundle_updateTotal(root, variants);
return;
}
const bundleButton = event.target.closest('[data-tee-bundle-bundle-atc]');
if (bundleButton) {
tee_bundle_handleBundleAtc(artworkRoot, root, variants);
}
});
}
/** Bundle event binding - END */
/** Mount logic - START */
/** Mount the bundle UI into #tee-artwork-form. */
function tee_bundle_mountArtwork(artworkRoot) {
if (!artworkRoot || artworkRoot.dataset.teeBundleMounted === 'true') return;
if (!tee_bundle_isSupportedCampaign()) return;
const variants = tee_bundle_getCampaignVariants();
const teeAtcButton = tee_bundle_getTeeAtcButton(artworkRoot);
if (!variants.length || !teeAtcButton) {
tee_bundle_warn('bundle mount skipped', {
hasVariants: !!variants.length,
hasButton: !!teeAtcButton,
});
return;
}
if (artworkRoot.querySelector(`.${TEE_BUNDLE_PREFIX}-bundle`)) {
artworkRoot.dataset.teeBundleMounted = 'true';
return;
}
tee_bundle_log('mounting bundle UI', {
variantCount: variants.length,
});
const bundle = tee_bundle_renderBundle(variants);
const anchor =
artworkRoot.querySelector('#teeFormActions') ||
artworkRoot.querySelector('.tee-customize-main-form') ||
artworkRoot.querySelector('.tee-customization-warning') ||
artworkRoot.firstElementChild ||
null;
if (anchor) {
anchor.insertAdjacentElement('beforebegin', bundle);
} else {
artworkRoot.appendChild(bundle);
}
tee_bundle_bindBundle(artworkRoot, bundle, variants);
tee_bundle_updateTotal(bundle, variants);
artworkRoot.dataset.teeBundleMounted = 'true';
}
/** Mount the bundle UI for all current #tee-artwork-form instances. */
function tee_bundle_mountAll() {
if (!tee_bundle_isSupportedCampaign()) {
tee_bundle_log('mount skipped: current page is not a Teeinblue product');
return;
}
document.querySelectorAll('#tee-artwork-form').forEach(tee_bundle_mountArtwork);
}
/** Mount logic - END */
/** Bootstrap - START */
/** Initialize the bundle runtime and remount whenever Teeinblue re-injects its UI. */
function tee_bundle_boot() {
tee_bundle_log('booting bundle system');
tee_bundle_installTeeinblueHooks();
tee_bundle_mountAll();
document.addEventListener('DOMContentLoaded', () => {
tee_bundle_log('DOMContentLoaded');
tee_bundle_installTeeinblueHooks();
tee_bundle_mountAll();
});
document.addEventListener(TEE_BUNDLE_INJECT_EVENT, () => {
tee_bundle_log('received event', TEE_BUNDLE_INJECT_EVENT, {
isTeeInBlueProduct: window.teeinblueCampaign?.isTeeInBlueProduct,
});
setTimeout(() => {
tee_bundle_installTeeinblueHooks();
tee_bundle_mountAll();
}, 80);
});
const observer = new MutationObserver((mutations) => {
let shouldMount = false;
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (!(node instanceof HTMLElement)) continue;
if (node.matches?.('#tee-artwork-form') || node.querySelector?.('#tee-artwork-form')) {
shouldMount = true;
break;
}
}
if (shouldMount) break;
}
if (shouldMount) {
tee_bundle_log('artwork mutation detected, remounting');
tee_bundle_installTeeinblueHooks();
tee_bundle_mountAll();
}
});
observer.observe(document.documentElement, {
childList: true,
subtree: true,
});
}
/** Bootstrap - END */
/** Run the bundle bootstrap routine. */
tee_bundle_boot();
})();
</script>
- Create this file:
assets/teeinblue-bundle.css
body.teeinblue-enabled:not(.teeinblue-platform-product-enabled) .tee-customization-wrapper~.tee_bundle-bundle {
display: block !important;
}
.tee_bundle-bundle {
margin: 18px 0 22px;
padding: 18px;
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 14px;
background: #fff;
}
.tee_bundle-header {
margin-bottom: 14px;
}
.tee_bundle-title {
margin: 0 0 6px;
font-size: 18px;
line-height: 1.3;
font-weight: 600;
color: #111;
}
.tee_bundle-desc {
margin: 0;
font-size: 13px;
line-height: 1.6;
color: #555;
}
.tee_bundle-list {
display: grid;
gap: 10px;
}
.tee_bundle-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 14px;
align-items: center;
padding: 12px 14px;
border: 1px solid rgba(0, 0, 0, 0.10);
border-radius: 12px;
background: #fafafa;
transition: border-color 0.2s ease, background 0.2s ease, opacity 0.2s ease;
}
.tee_bundle-row.is-selected {
border-color: #111;
background: #fff;
}
.tee_bundle-variant {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
margin: 0;
cursor: pointer;
}
.tee_bundle-checkbox {
flex: 0 0 auto;
margin: 0;
}
.tee_bundle-variant-title {
display: block;
min-width: 0;
font-size: 14px;
line-height: 1.5;
color: #111;
word-break: break-word;
}
.tee_bundle-qty {
display: inline-grid;
grid-template-columns: 34px 58px 34px;
gap: 6px;
align-items: center;
}
.tee_bundle-qty-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
border: 1px solid rgba(0, 0, 0, 0.16);
border-radius: 8px;
background: #fff;
color: #111;
font-size: 18px;
line-height: 1;
cursor: pointer;
}
.tee_bundle-qty-btn:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.tee_bundle-qty-input {
width: 58px;
height: 34px;
margin: 0;
padding: 0 8px;
border: 1px solid rgba(0, 0, 0, 0.16);
border-radius: 8px;
background: #fff;
text-align: center;
font-size: 14px;
}
.tee_bundle-actions {
margin-top: 16px;
}
.tee_bundle-atc {
width: 100%;
min-height: 46px;
padding: 12px 16px;
border: 0;
border-radius: 999px;
background: #111;
color: #fff;
font-size: 14px;
font-weight: 600;
line-height: 1.2;
cursor: pointer;
transition: opacity 0.2s ease, transform 0.2s ease;
}
.tee_bundle-atc:hover {
opacity: 0.92;
}
.tee_bundle-atc:disabled {
opacity: 0.55;
cursor: not-allowed;
}
.tee_bundle-status {
margin-top: 12px;
font-size: 13px;
line-height: 1.5;
min-height: 20px;
}
.tee_bundle-status.is-info {
color: #555;
}
.tee_bundle-status.is-success {
color: #0f7a32;
}
.tee_bundle-status.is-error {
color: #b42318;
}
.tee_bundle-bundle.is-loading {
opacity: 0.8;
}
@media (max-width: 767px) {
.tee_bundle-row {
grid-template-columns: 1fr;
}
.tee_bundle-qty {
justify-self: start;
}
}
.tee_bundle-atc {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.tee_bundle-atc.is-loading {
pointer-events: none;
}
.tee_bundle-spinner {
width: 16px;
height: 16px;
border: 2px solid currentColor;
border-right-color: transparent;
border-radius: 50%;
display: inline-block;
animation: tee_bundle-spin 0.7s linear infinite;
flex: 0 0 auto;
}
@keyframes tee_bundle-spin {
to {
transform: rotate(360deg);
}
}
#tee-artwork-form {
display: grid !important;
.tee-option {
order: 1000;
}
.tee-variants {
order: 1001;
}
.tee-form-actions {
order: 1002;
}
.tee-customization-warning {
order: 1003;
}
.tee-description {
order: 1004;
}
}
- Go to
layout/theme.liquidand add this script right above the close</head>tag:
{% if request.page_type == 'product' and product.template_suffix == 'teeinblue-bulk-order' %}
{% render 'teeinblue-bundle' %}
{% endif %}

Step 4: Test the Bulk Order Flow
Finally, test the entire process on your storefront.
Things to verify:
- Personalization form loads correctly
- Variant quantities can be selected
- Multiple variants are added to the cart
- All items contain the same personalized design
Updated on: 20/03/2026
Thank you!
