1import { getMenu, getConfig, formatPrice, getTagClass, getTagLabel } from './config.js'; 2import { getIngredients, getIngredientPrice } from './ingredients.js'; 3import { formatAllergenNumbersLabel, getItemAllergenNumbers } from './allergens.js'; 4import { addToCart } from './cart.js'; 5import { showToast } from './toast.js'; 6import { applyDishOfTheDayToBase, getDishOfTheDay, formatDishDiscountLabel } from './pizza-of-the-day.js'; 7import { 8 applyPercentOfferToPrice, 9 getBestPublicPercentOffer, 10 getDishOfTheDayDisplayPricing 11} from './promotions.js'; 12import { t } from './i18n.js'; 13import { resolvePublicPath, getLocale } from './locale.js'; 14import { trackAbEvent, trackAbImpression } from './ab-tests.js'; 15import { 16 applyFreeSaucesToExtras, 17 isSauceIngredientCategory, 18 resolveFreeSaucesPolicy 19} from './free-sauces.js'; 20 21const POPUP_TRIGGER_KEYS = ['before_order', 'after_add_to_cart', 'open_cart']; 22 23let modalInitialized = false; 24let pendingOrderSubmit = null; 25let currentModalSession = null; 26 27function isCartDrawerOpen() { 28 return document.getElementById('cart-drawer')?.classList.contains('open') ?? false; 29} 30 31function restoreBodyScroll() { 32 document.body.style.overflow = isCartDrawerOpen() ? 'hidden' : ''; 33} 34 35function clearModalSession() { 36 currentModalSession = null; 37} 38 39function closeModal() { 40 const { backdrop, modal, body, footer, title: titleEl } = getModalElements(); 41 if (backdrop) backdrop.hidden = true; 42 if (modal) modal.hidden = true; 43 if (body) body.innerHTML = ''; 44 if (footer) footer.innerHTML = ''; 45 if (titleEl) titleEl.textContent = ''; 46 restoreBodyScroll(); 47} 48 49function dismissModalSession(action = 'abort') { 50 const session = currentModalSession; 51 if (!session || session.completed) { 52 closeModal(); 53 clearModalSession(); 54 return; 55 } 56 57 if (session.type === 'customize' && session.parentSession) { 58 const parent = session.parentSession; 59 parent.completed = false; 60 currentModalSession = parent; 61 renderUpsellModal(parent); 62 if (action === 'abort') { 63 session.onClose?.(); 64 } 65 return; 66 } 67 68 session.completed = true; 69 currentModalSession = null; 70 closeModal(); 71 72 if (session.type === 'upsell') { 73 if (action === 'continue') { 74 session.onContinue?.(); 75 } 76 session.resolve?.(action === 'continue'); 77 return; 78 } 79 80 session.onClose?.(); 81} 82 83function restoreUpsellAfterCustomize(options = {}, event = 'cancelled') { 84 const parent = currentModalSession?.parentSession; 85 if (!parent || parent.type !== 'upsell') return false; 86 87 parent.completed = false; 88 currentModalSession = parent; 89 renderUpsellModal(parent); 90 91 if (event === 'added') { 92 options.onAdded?.(); 93 } else { 94 options.onCancelled?.(); 95 } 96 return true; 97} 98 99export function getPopupTriggers() { 100 return Object.fromEntries( 101 POPUP_TRIGGER_KEYS.map(key => [key, t(`modal.trigger.${key}`)]) 102 ); 103} 104 105function getAbMergedPopup(trigger) { 106 const config = getConfig(); 107 const ab = config?._abProductPopup; 108 if (!ab || ab.trigger !== trigger) return null; 109 const base = (Array.isArray(config.popups) ? config.popups : []).find((p) => p.enabled !== false); 110 if (!base) return null; 111 const english = getLocale() === 'en'; 112 return { 113 ...base, 114 trigger, 115 title: (english ? ab.titleEn : ab.titlePl) || base.title, 116 subtitle: (english ? ab.subtitleEn : ab.subtitlePl) || base.subtitle 117 }; 118} 119 120export function getPopupsForTrigger(trigger) { 121 const abPopup = getAbMergedPopup(trigger); 122 if (abPopup) return [abPopup]; 123 const popups = getConfig()?.popups; 124 if (!Array.isArray(popups)) return []; 125 return popups.filter(popup => popup.enabled !== false && popup.trigger === trigger); 126} 127 128export function findMenuItemById(itemId) { 129 const menu = getMenu(); 130 for (const category of menu.categories || []) { 131 const found = (category.items || []).find(item => item.id === itemId); 132 if (found) return { ...found, categoryId: category.id, categoryName: category.name }; 133 } 134 return null; 135} 136 137export function getAllMenuItemsFlat() { 138 return (getMenu().categories || []) 139 .filter(cat => cat.id !== 'bestsellery') 140 .flatMap(cat => (cat.items || []).map(item => ({ 141 ...item, 142 categoryId: cat.id, 143 categoryName: cat.name 144 }
144))); 145} 146 147function escapeHtml(value) { 148 return String(value) 149 .replaceAll('&', '&') 150 .replaceAll('<', '<') 151 .replaceAll('>', '>') 152 .replaceAll('"', '"'); 153} 154 155function getModalElements() { 156 return { 157 backdrop: document.getElementById('product-modal-backdrop'), 158 modal: document.getElementById('product-modal'), 159 title: document.getElementById('product-modal-title'), 160 body: document.getElementById('product-modal-body'), 161 footer: document.getElementById('product-modal-footer'), 162 closeBtn: document.getElementById('product-modal-close') 163 }; 164} 165 166function openModal(title, bodyHtml, footerHtml) { 167 const { backdrop, modal, title: titleEl, body, footer } = getModalElements(); 168 if (!backdrop || !modal || !body || !footer) return; 169 if (titleEl) titleEl.textContent = title; 170 body.innerHTML = bodyHtml; 171 footer.innerHTML = footerHtml; 172 backdrop.hidden = false; 173 modal.hidden = false; 174 document.body.style.overflow = 'hidden'; 175} 176 177function bindUpsellActions(session) { 178 const { popup, items } = session; 179 180 document.getElementById('product-modal-skip')?.addEventListener('click', (e) => { 181 e.preventDefault(); 182 dismissModalSession('continue'); 183 }, { once: true }); 184 185 document.getElementById('product-modal-continue')?.addEventListener('click', (e) => { 186 e.preventDefault(); 187 void trackAbEvent('product_popup', 'click'); 188 dismissModalSession('continue'); 189 }, { once: true }); 190 191 document.querySelectorAll('[data-upsell-add]').forEach(btn => { 192 btn.addEventListener('click', () => { 193 const item = findMenuItemById(btn.dataset.upsellAdd); 194 if (!item) return; 195 196 if (item.allowExtras || item.allowSaucesOnly || item.categoryId === 'pizza' || item.sizes?.length) { 197 openCustomizeModal(item); 198 return; 199 } 200 201 addToCart({ 202 id: item.id, 203 name: item.name, 204 size: null, 205 price: calculateItemCartPrice(item, null, []), 206 image: item.image, 207 extras: [] 208 }); 209 showToast(t('modal.addedShortToast', { name: item.name })); 210 renderUpsellModal(session); 211 }); 212 }); 213} 214 215function renderUpsellModal(session) { 216 const { popup, items } = session; 217 session.completed = false; 218 currentModalSession = session; 219 220 openModal( 221 popup.title || t('modal.upsellTitle'), 222 renderUpsellBody(popup, items), 223 ` 224 <button type="button" class="btn btn-secondary" id="product-modal-skip">${escapeHtml(t('modal.skip'))}</button> 225 <button type="button" class="btn btn-primary" id="product-modal-continue">${escapeHtml(t('modal.continue'))}</button> 226 ` 227 ); 228 229 trackAbImpression('product_popup'); 230 bindUpsellActions(session); 231} 232 233export function initProductModal() { 234 if (modalInitialized) return; 235 modalInitialized = true; 236 237 const { backdrop, closeBtn } = getModalElements(); 238 if (closeBtn) closeBtn.setAttribute('aria-label', t('modal.close')); 239 240 backdrop?.addEventListener('click', () => { 241 if (currentModalSession?.type === 'customize' && currentModalSession.parentSession) { 242 dismissModalSession('abort'); 243 return; 244 } 245 if (currentModalSession?.type === 'upsell') { 246 dismissModalSession(currentModalSession.trigger === 'before_order' ? 'continue' : 'abort'); 247 return; 248 } 249 dismissModalSession('abort'); 250 }); 251 252 closeBtn?.addEventListener('click', (e) => { 253 e.preventDefault(); 254 e.stopPropagation(); 255 if (currentModalSession?.type === 'customize' && currentModalSession.parentSession) { 256 dismissModalSession('abort'); 257 return; 258 } 259 if (currentModalSession?.type === 'upsell') { 260 dismissModalSession(currentModalSession.trigger === 'before_order' ? 'continue' : 'abort'); 261 return; 262 } 263 dismissModalSession('abort'); 264 }); 265 266 document.addEventListener('keydown', (e) => { 267 const { modal } = getModalElements(); 268 if (e.key !== 'Escape' || !modal || modal.hidden) return; 269 if (currentModalSession?.type === 'customize' && currentModalSession.parentSession) { 270 dismissModalSession('abort'); 271 return; 272 } 273 if (currentModalSession?.type === 'upsell') {
274 dismissModalSession(currentModalSession.trigger === 'before_order' ? 'continue' : 'abort'); 275 return; 276 } 277 dismissModalSession('abort'); 278 }); 279 280 closeModal(); 281 clearModalSession(); 282} 283 284function getCatalogUnitPrice(menuItem, sizeName) { 285 let price = Number(menuItem.price || 0); 286 const size = (menuItem.sizes || []).find(s => s.name === sizeName); 287 if (size) price += Number(size.priceModifier || 0); 288 return Math.round(price * 100) / 100; 289} 290 291/** Cena zapisywana w koszyku: danie dnia na pozycji; happy hours nie sumuje siÄ z daniem dnia. */ 292function calculateItemCartPrice(menuItem, sizeName, selectedExtras) { 293 let price = getCatalogUnitPrice(menuItem, sizeName); 294 price = applyDishOfTheDayToBase( 295 price, 296 menuItem.id, 297 getMenu(), 298 getConfig()?.promotions || {} 299 ); 300 for (const extra of selectedExtras) { 301 price += Number(extra.price || 0); 302 } 303 return Math.round(price * 100) / 100; 304} 305 306/** Cena w modalu âÅÄ cznieâ: danie dnia zastÄpuje happy hours; inaczej publiczny %. */ 307function calculateItemDisplayPrice(menuItem, sizeName, selectedExtras) { 308 const catalog = getCatalogUnitPrice(menuItem, sizeName); 309 const promotions = getConfig()?.promotions || {}; 310 const menu = getMenu(); 311 const dishDisplay = getDishOfTheDayDisplayPricing(catalog, menuItem.id, menu, promotions); 312 let unit = catalog; 313 if (dishDisplay.dishApplied) { 314 unit = dishDisplay.salePrice; 315 } else { 316 const autoOffer = getBestPublicPercentOffer(promotions, { subtotal: catalog }); 317 unit = applyPercentOfferToPrice(catalog, autoOffer); 318 } 319 let price = unit; 320 for (const extra of selectedExtras) { 321 price += Number(extra.price || 0); 322 } 323 return Math.round(price * 100) / 100; 324} 325 326function resolveInitialSize(menuItem, initialSizeName) { 327 const sizes = menuItem.sizes || []; 328 if (!sizes.length) return null; 329 if (initialSizeName) { 330 const found = sizes.find(size => size.name === initialSizeName); 331 if (found) return found; 332 } 333 return sizes[1] || sizes[0]; 334} 335 336function renderCustomizeBody(menuItem, initialSizeName = null) { 337 const ingredients = getIngredients(); 338 const allowAllExtras = menuItem.allowExtras || menuItem.categoryId === 'pizza'; 339 const allowSaucesOnly = !allowAllExtras && menuItem.allowSaucesOnly === true; 340 const allowAnyExtras = allowAllExtras || allowSaucesOnly; 341 const hasSizes = Array.isArray(menuItem.sizes) && menuItem.sizes.length > 0; 342 const defaultSize = hasSizes ? resolveInitialSize(menuItem, initialSizeName) : null; 343 344 const sizesHtml = hasSizes ? ` 345 <div class="product-modal__section"> 346 <p class="product-modal__label">${escapeHtml(t('modal.size'))}</p> 347 <div class="product-modal__sizes"> 348 ${menuItem.sizes.map((size, i) => ` 349 <button type="button" class="product-modal__size-btn ${size.name === defaultSize?.name ? 'is-active' : ''}" 350 data-size="${escapeHtml(size.name)}" data-modifier="${size.priceModifier ?? 0}"> 351 ${escapeHtml(size.name)} 352 </button> 353 `).join('')} 354 </div> 355 </div> 356 ` : ''; 357 358 const extrasHtml = allowAnyExtras ? (ingredients.categories || []) 359 .filter(cat => allowAllExtras || isSauceIngredientCategory(cat.id, cat.name)) 360 .map(cat => ({ 361 ...cat, 362 items: (cat.items || []).filter(extra => extra.enabled !== false) 363 })) 364 .filter(cat => cat.items.length) 365 .map(cat => ` 366 <div class="product-modal__section"> 367 <p class="product-modal__label">${escapeHtml(cat.name)}</p> 368 <div class="product-modal__extras"> 369 ${cat.items.map(extra => { 370 const price = getIngredientPrice(extra, defaultSize?.name || null); 371 return ` 372 <label class="product-modal__extra"> 373 <input type="checkbox" name="extra" value="${escapeHtml(extra.id)}" 374 data-name="${escapeHtml(extra.name)}" 375 data-category-id="${escapeHtml(cat.id || '')}" 376 data-category-name="${escapeHtml(cat.name || '')}" 377 data-price="${price}" 378 data-price-30="${getIngredientPrice(extra, '30 cm')}" 379 data-price-40="${getIngredientPrice(extra, '40 cm')}" 380 data-price-50="${getIngredientPrice(extra, '50 cm')}">
381 <span>${escapeHtml(extra.name)}</span> 382 <strong class="product-modal__extra-price">+${formatPrice(price)}</strong> 383 </label> 384 `; 385 }).join('')} 386 </div> 387 </div> 388 `).join('') : ''; 389 390 return ` 391 <div class="product-modal__product"> 392 ${menuItem.image ? `<img src="${resolvePublicPath(menuItem.image)}" alt="${escapeHtml(menuItem.name)}" class="product-modal__image">` : ''} 393 <div> 394 <h4>${escapeHtml(menuItem.name)}${(() => { 395 const dish = getDishOfTheDay(getMenu(), getConfig()?.promotions || {}); 396 if (dish?.item?.id !== menuItem.id) return ''; 397 const priced = getDishOfTheDayDisplayPricing( 398 getCatalogUnitPrice(menuItem, defaultSize?.name || null), 399 menuItem.id, 400 getMenu(), 401 getConfig()?.promotions || {} 402 ); 403 const label = formatDishDiscountLabel(dish, getLocale(), priced.autoOffer?.value || 0); 404 return ` <span class="badge ${getTagClass('danie-dnia')}">${escapeHtml(getTagLabel('danie-dnia'))} ${escapeHtml(label)}</span>`; 405 })()}</h4> 406 ${menuItem.description ? `<p>${escapeHtml(menuItem.description)}</p>` : ''} 407 ${(() => { 408 const nums = getItemAllergenNumbers(menuItem.id); 409 if (!nums.length) return ''; 410 return `<p class="product-modal__allergens">${escapeHtml(t('menu.allergens'))}: ${escapeHtml(formatAllergenNumbersLabel(nums))}</p>`; 411 })()} 412 </div> 413 </div> 414 ${sizesHtml} 415 ${extrasHtml} 416 <div class="product-modal__price-row"> 417 <span>${escapeHtml(t('modal.total'))}</span> 418 <strong id="product-modal-total">${formatPrice(calculateItemDisplayPrice(menuItem, defaultSize?.name || null, []))}</strong> 419 </div> 420 `; 421} 422 423function bindCustomizeModal(menuItem, options = {}) { 424 const modal = document.getElementById('product-modal'); 425 if (!modal) return; 426 427 const totalEl = modal.querySelector('#product-modal-total'); 428 const hasSizes = Array.isArray(menuItem.sizes) && menuItem.sizes.length > 0; 429 let selectedSize = hasSizes ? resolveInitialSize(menuItem, options.initialSize)?.name : null; 430 431 function applyFreeSaucesPricing(extras) { 432 const policy = resolveFreeSaucesPolicy(getMenu(), menuItem.id, selectedSize); 433 return applyFreeSaucesToExtras(extras, { 434 maxFreeSauces: policy.maxFreeSauces, 435 isSauceExtra: (extra) => isSauceIngredientCategory(extra.categoryId, extra.categoryName) 436 }); 437 } 438 439 function getSelectedExtras() { 440 const selected = Array.from(modal.querySelectorAll('input[name="extra"]:checked')).map(input => ({ 441 id: input.value, 442 name: input.dataset.name || '', 443 price: Number(input.dataset.price) || 0, 444 categoryId: input.dataset.categoryId || '', 445 categoryName: input.dataset.categoryName || '' 446 })); 447 return applyFreeSaucesPricing(selected).map((extra) => ({ 448 id: extra.id, 449 name: extra.name, 450 price: extra.price 451 })); 452 } 453 454 function isSauceInput(input) { 455 return isSauceIngredientCategory(input?.dataset?.categoryId || '', input?.dataset?.categoryName || ''); 456 } 457 458 function refreshExtraPriceLabels() { 459 const allInputs = Array.from(modal.querySelectorAll('input[name="extra"]')); 460 if (!allInputs.length) return; 461 462 const policy = resolveFreeSaucesPolicy(getMenu(), menuItem.id, selectedSize); 463 let freeSlotsLeft = Math.max(0, Number(policy.maxFreeSauces) || 0); 464 const freeCheckedInputs = new Set(); 465 466 const checkedSauceInputs = allInputs.filter((input) => input.checked && isSauceInput(input)); 467 checkedSauceInputs.forEach((input) => { 468 if (freeSlotsLeft <= 0) return; 469 freeCheckedInputs.add(input); 470 freeSlotsLeft -= 1; 471 }); 472 473 allInputs.forEach((input) => { 474 const priceEl = input.closest('.product-modal__extra')?.querySelector('.product-modal__extra-price'); 475 if (!priceEl) return; 476 477 const basePrice = Math.max(0, Number(input.dataset.price) || 0); 478 let showFree = false; 479 480 if (isSauceInput(input)) { 481 if (input.checked) { 482 showFree = freeCheckedInputs.has(input); 483 } else { 484 showFree = freeSlotsLeft > 0; 485 } 486 } 487 488 priceEl.textContent = showFree ? t('cart.free') : `+${formatPrice(basePrice)}`; 489 }); 490 } 491 492 function syncExtrasPricesForSize(sizeName) {
493 const key = String(sizeName || '').match(/(\d+)/)?.[1] || '40'; 494 modal.querySelectorAll('input[name="extra"]').forEach(input => { 495 // data-price-30 nie mapuje siÄ na dataset.price30 (cyfra po myÅlniku) â używaj getAttribute 496 const sizedAttr = input.getAttribute(`data-price-${key}`); 497 const nextPrice = Number(sizedAttr != null && sizedAttr !== '' ? sizedAttr : input.dataset.price) || 0; 498 input.dataset.price = String(nextPrice); 499 }); 500 refreshExtraPriceLabels(); 501 } 502 503 function updateTotal() { 504 const total = calculateItemDisplayPrice(menuItem, selectedSize, getSelectedExtras()); 505 if (totalEl) totalEl.textContent = formatPrice(total); 506 } 507 508 modal.querySelectorAll('.product-modal__size-btn').forEach(btn => { 509 btn.addEventListener('click', () => { 510 modal.querySelectorAll('.product-modal__size-btn').forEach(b => b.classList.remove('is-active')); 511 btn.classList.add('is-active'); 512 selectedSize = btn.dataset.size || null; 513 syncExtrasPricesForSize(selectedSize); 514 updateTotal(); 515 }); 516 }); 517 518 syncExtrasPricesForSize(selectedSize); 519 520 modal.querySelectorAll('input[name="extra"]').forEach(input => { 521 input.addEventListener('change', () => { 522 refreshExtraPriceLabels(); 523 updateTotal(); 524 }); 525 }); 526 527 modal.querySelector('#product-modal-confirm')?.addEventListener('click', () => { 528 const extras = getSelectedExtras(); 529 const price = calculateItemCartPrice(menuItem, selectedSize, extras); 530 531 addToCart({ 532 id: menuItem.id, 533 name: menuItem.name, 534 size: selectedSize, 535 price, 536 image: menuItem.image, 537 extras 538 }); 539 540 showToast(t('modal.addedToast', { name: menuItem.name })); 541 if (restoreUpsellAfterCustomize(options, 'added')) return; 542 dismissModalSession('abort'); 543 options.onAdded?.(); 544 maybeShowUpsell('after_add_to_cart'); 545 }, { once: true }); 546 547 modal.querySelector('#product-modal-cancel')?.addEventListener('click', (e) => { 548 e.preventDefault(); 549 if (restoreUpsellAfterCustomize(options, 'cancelled')) return; 550 dismissModalSession('abort'); 551 options.onCancelled?.(); 552 }, { once: true }); 553} 554 555export function openCustomizeModal(menuItem, options = {}) { 556 initProductModal(); 557 const allowAllExtras = menuItem.allowExtras || menuItem.categoryId === 'pizza'; 558 const allowAnyExtras = allowAllExtras || menuItem.allowSaucesOnly === true; 559 const needsModal = allowAnyExtras || (menuItem.sizes?.length > 0); 560 if (!needsModal) { 561 addToCart({ 562 id: menuItem.id, 563 name: menuItem.name, 564 size: null, 565 price: calculateItemCartPrice(menuItem, null, []), 566 image: menuItem.image, 567 extras: [] 568 }); 569 showToast(t('modal.addedToast', { name: menuItem.name })); 570 options.onAdded?.(); 571 maybeShowUpsell('after_add_to_cart'); 572 return; 573 } 574 575 const parentSession = currentModalSession?.type === 'upsell' ? currentModalSession : null; 576 577 currentModalSession = { 578 type: 'customize', 579 completed: false, 580 parentSession, 581 onClose: options.onCancelled 582 }; 583 584 openModal( 585 allowAnyExtras ? t('modal.chooseExtras') : t('modal.chooseOption'), 586 renderCustomizeBody(menuItem, options.initialSize), 587 ` 588 <button type="button" class="btn btn-secondary" id="product-modal-cancel">${escapeHtml(t('modal.cancel'))}</button> 589 <button type="button" class="btn btn-primary" id="product-modal-confirm">${escapeHtml(t('modal.addToCart'))}</button> 590 ` 591 ); 592 593 bindCustomizeModal(menuItem, options); 594} 595 596function renderUpsellBody(popup, items) { 597 return ` 598 <p class="product-modal__intro">${escapeHtml(popup.subtitle || t('modal.upsellSubtitle'))}</p> 599 <div class="product-modal__upsell-grid"> 600 ${items.map(item => ` 601 <article class="product-modal__upsell-card"> 602 ${item.image ? `<img src="${resolvePublicPath(item.image)}" alt="${escapeHtml(item.name)}">` : '<div class="product-modal__upsell-card-thumb" aria-hidden="true"></div>'} 603 <div class="product-modal__upsell-card-body"> 604 <strong class="product-modal__upsell-card-name">${escapeHtml(item.name)}</strong>
605 <span class="product-modal__upsell-card-price">${formatPrice(calculateItemDisplayPrice(item, null, []))}</span> 606 </div> 607 <button type="button" class="btn btn-primary btn-sm product-modal__upsell-add" data-upsell-add="${escapeHtml(item.id)}">${escapeHtml(t('modal.add'))}</button> 608 </article> 609 `).join('')} 610 </div> 611 `; 612} 613 614export function showUpsellPopup(trigger, onContinue) { 615 const popups = getPopupsForTrigger(trigger); 616 if (!popups.length) { 617 onContinue?.(); 618 return Promise.resolve(false); 619 } 620 621 const popup = popups[0]; 622 const items = (popup.itemIds || []) 623 .map(id => findMenuItemById(id)) 624 .filter(item => item && item.enabled !== false); 625 626 if (!items.length) { 627 onContinue?.(); 628 return Promise.resolve(false); 629 } 630 631 initProductModal(); 632 633 if (currentModalSession?.type === 'upsell' && currentModalSession.trigger === trigger) { 634 dismissModalSession('abort'); 635 } 636 637 return new Promise(resolve => { 638 const session = { 639 type: 'upsell', 640 trigger, 641 popup, 642 items, 643 onContinue, 644 resolve, 645 completed: false 646 }; 647 648 currentModalSession = session; 649 renderUpsellModal(session); 650 }); 651} 652 653export function maybeShowUpsell(trigger) { 654 const popups = getPopupsForTrigger(trigger); 655 if (!popups.length) return; 656 657 const popup = popups[0]; 658 const items = (popup.itemIds || []) 659 .map(id => findMenuItemById(id)) 660 .filter(item => item && item.enabled !== false); 661 if (!items.length) return; 662 663 showUpsellPopup(trigger); 664} 665 666export function interceptOrderSubmit(submitFn) { 667 pendingOrderSubmit = submitFn; 668 showUpsellPopup('before_order', () => { 669 pendingOrderSubmit?.(); 670 pendingOrderSubmit = null; 671 }); 672}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.