1/** 2 * Delivery Method & Postcode Validation 3 */ 4 5const DeliveryManager = { 6 state: { 7 selectedMethod: null, 8 postcodeData: null, 9 minOrder: 0, 10 deliveryFee: 0, 11 shopClosed: false, 12 userAddresses: [], 13 deliveryAvailable: true, 14 validating: false, 15 pendingAction: null, 16 originalCartAdd: null, 17 originalShowModal: null, 18 originalShowVariationsModal: null, 19 originalProductModalOpen: null, 20 originalProductModalQuickAdd: null 21 }, 22 23 // Storage key helpers for multi-store 24 getStorageKey(key) { 25 return window.STORE_SLUG ? `${key}_${window.STORE_SLUG}` : key; 26 }, 27 28 getFailedAddressIds() { 29 const stored = sessionStorage.getItem(this.getStorageKey('failed_address_ids')); 30 if (stored) { 31 try { return JSON.parse(stored); } 32 catch (e) { return []; } 33 } 34 return []; 35 }, 36 37 setFailedAddressIds(ids) { 38 sessionStorage.setItem(this.getStorageKey('failed_address_ids'), JSON.stringify(ids)); 39 }, 40 41 addFailedAddressId(id) { 42 const ids = this.getFailedAddressIds(); 43 if (!ids.includes(id)) { 44 ids.push(id); 45 this.setFailedAddressIds(ids); 46 } 47 }, 48 49 removeFailedAddressId(id) { 50 const ids = this.getFailedAddressIds().filter(i => i !== id); 51 this.setFailedAddressIds(ids); 52 }, 53 54 getPostcodeData() { 55 const stored = sessionStorage.getItem(this.getStorageKey('postcode_data')); 56 if (stored) { 57 try { return JSON.parse(stored); } 58 catch (e) { return null; } 59 } 60 return null; 61 }, 62 63 setPostcodeData(data) { 64 sessionStorage.setItem(this.getStorageKey('postcode_data'), JSON.stringify(data)); 65 }, 66 67 removePostcodeData() { 68 sessionStorage.removeItem(this.getStorageKey('postcode_data')); 69 }, 70 71 getSelectedMethod() { 72 return sessionStorage.getItem(this.getStorageKey('selected_method')); 73 }, 74 75 setSelectedMethod(method) { 76 sessionStorage.setItem(this.getStorageKey('selected_method'), method); 77 }, 78 79 removeSelectedMethod() { 80 sessionStorage.removeItem(this.getStorageKey('selected_method')); 81 }, 82 83 init() { 84 // Load user addresses from page 85 this.state.userAddresses = window.userAddresses || []; 86 this.state.deliveryAvailable = window.deliveryAvailable ?? true; 87 88 // Check if shop is closed 89 const deliverySection = document.querySelector('.delivery-methods-section'); 90 this.state.shopClosed = deliverySection && deliverySection.querySelector('.alert-danger') !== null; 91 92 // Load postcode from sessionStorage 93 this.state.postcodeData = this.getPostcodeData(); 94 95 // Store original Cart.add and override to check method selection 96 this.state.originalCartAdd = Cart.add.bind(Cart); 97 Cart.add = (...args) => { 98 if (!this.canAddToCart({type: 'cart_add', args: args})) { 99 return false; 100 } 101 return this.state.originalCartAdd(...args); 102 }; 103 104 // Store original ProductOptions methods and override to check method selection 105 if (typeof ProductOptions !== 'undefined') { 106 this.state.originalShowModal = ProductOptions.showModal.bind(ProductOptions); 107 ProductOptions.showModal = (button) => { 108 if (!this.canAddToCart({type: 'show_options', button: button})) { 109 return false; 110 } 111 return this.state.originalShowModal(button); 112 }; 113 114 this.state.originalShowVariationsModal = ProductOptions.showVariationsModal.bind(ProductOptions); 115 ProductOptions.showVariationsModal = (button) => { 116 if (!this.canAddToCart({type: 'show_variations', button: button})) { 117 return false; 118 } 119 return this.state.originalShowVariationsModal(button); 120 }; 121 } 122 123 // Store original ProductModal methods and override to check method selection 124 if (typeof ProductModal !== 'undefined') { 125 this.state.originalProductModalOpen = ProductModal.open.bind(ProductModal); 126 ProductModal.open = (card) => { 127 if (!this.canAddToCart({type: 'product_modal_open', card: card})) { 128 return false; 129 } 130 return this.state.originalProductModalOpen(card); 131 }; 132 133 this.state.originalProductModalQuickAdd = ProductModal.quickAdd.bind(ProductModal); 134 ProductModal.quickAdd = (card) => { 135 if (!this.canAddToCart({type: 'product_modal_quickadd', card: card})) { 136 return false; 137 } 138 return this.state.originalProductModalQuickAdd(card); 139 }; 140 } 141 142 // Listen for cart updates 143 const originalUpdateUI = Cart.updateUI; 144 Cart.updateUI = function() { 145 originalUpdateUI.call(this); 146 DeliveryManager.updateMinOrderWarning(); 147 DeliveryManager.updateCheckoutButton(); 148 }; 149 150 // Initialize on page load 151 this.initializeMethodSelection(); 152 153 this.updateCheckoutButton(); 154 }, 155 156 /** 157 * Initialize method selection on page load 158 * Priority: sessionStorage method â validate postcode â wait for user 159 */ 160 async initializeMethodSelection() { 161 // URL preselect (?method=delivery|collection) - takes priority over stored method
162 let urlMethod = (new URLSearchParams(window.location.search).get('method') || '').toLowerCase(); 163 if (urlMethod === 'pickup') urlMethod = 'collection'; 164 165 if (urlMethod === 'collection') { 166 const collectionBtn = document.querySelector('[data-method="collection"]'); 167 if (collectionBtn) { 168 this.selectMethod(collectionBtn, true); 169 return; 170 } 171 } 172 173 if (urlMethod === 'delivery' && this.state.deliveryAvailable) { 174 const deliveryBtn = document.querySelector('[data-method="delivery"]'); 175 if (deliveryBtn) { 176 this.selectMethod(deliveryBtn, true); 177 // Silent select - saved postcode/address validates in background; 178 // otherwise the postcode modal appears on first add-to-cart via canAddToCart() 179 await this.backgroundValidation(); 180 return; 181 } 182 } 183 184 const storedMethod = this.getSelectedMethod(); 185 186 // If collection is stored and available, select it 187 if (storedMethod === 'collection') { 188 const collectionBtn = document.querySelector('[data-method="collection"]'); 189 if (collectionBtn) { 190 this.selectMethod(collectionBtn, true); 191 return; 192 } 193 } 194 195 // For delivery (or no method stored), try to validate postcode in background 196 if (this.state.deliveryAvailable) { 197 await this.backgroundValidation(); 198 } else if (storedMethod === 'delivery') { 199 // Delivery was stored but no longer available - clear it 200 this.removeSelectedMethod(); 201 this.removePostcodeData(); 202 this.state.postcodeData = null; 203 } 204 }, 205 206 /** 207 * Background validation on page load 208 * Validates stored postcode or first saved address 209 */ 210 async backgroundValidation() { 211 let postcodeToValidate = null; 212 let addressId = null; 213 let address = null; 214 215 // Priority 1: sessionStorage postcode data 216 if (this.state.postcodeData?.postcode) { 217 postcodeToValidate = this.state.postcodeData.postcode; 218 addressId = this.state.postcodeData.addressId || null; 219 address = this.state.postcodeData.address || null; 220 } 221 // Priority 2: Logged in user's first address 222 else if (this.state.userAddresses.length > 0) { 223 postcodeToValidate = this.state.userAddresses[0].postcode; 224 addressId = this.state.userAddresses[0].address_id; 225 address = this.state.userAddresses[0].address_1; 226 } 227 228 // Nothing to validate 229 if (!postcodeToValidate) { 230 return; 231 } 232 233 this.state.validating = true; 234 this.setButtonsLoading(true); 235 try { 236 // If Uber Direct enabled, get Uber quote (by address_id or address+postcode) 237 if (window.UBER_DIRECT_ENABLED && typeof UberDelivery !== 'undefined' && (addressId || address)) { 238 const pd = this.state.postcodeData || {}; 239 const quoteParams = addressId 240 ? { address_id: addressId } 241 : { address: address, postcode: postcodeToValidate, 242 lat: pd.lat || null, lng: pd.lng || null, 243 town: pd.town || '', county: pd.county || '' }; 244 const quoteResult = await UberDelivery.getQuote(quoteParams); 245 246 if (quoteResult.success) { 247 // Also fetch delivery fee from our pricing 248 const feeResponse = await fetch(`${STORE_PATH}/checkout/validate-postcode`, { 249 method: 'POST', 250 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 251 body: `postcode=${encodeURIComponent(postcodeToValidate)}` 252 }); 253 const feeResult = await feeResponse.json(); 254 const deliveryFee = feeResult.success ? feeResult.data.deliveryFee : 0; 255 256 this.state.postcodeData = { 257 postcode: quoteResult.postcode || postcodeToValidate, 258 address: quoteResult.address || address, 259 addressId: addressId, 260 deliveryFee: deliveryFee, 261 validated: true, 262 timestamp: Date.now(), 263 uberQuoteId: quoteResult.quote_id, 264 uberExpires: quoteResult.expires, 265 lat: quoteResult.lat, 266 lng: quoteResult.lng, 267 town: pd.town || '', 268 county: pd.county || '', 269 geoPrecise: (quoteResult.lat && quoteResult.lng) ? 1 : 0 270 }; 271 this.setPostcodeData(this.state.postcodeData); 272 const deliveryBtn = document.querySelector('[data-method="delivery"]'); 273 if (deliveryBtn?.dataset.isActive === '1' && quoteResult.duration && quoteResult.pickup_duration) { 274 const prepMins = parseInt(deliveryBtn.dataset.readyMinutes) || 0; 275 const transit = quoteResult.duration - quoteResult.pickup_duration; 276 const displayMins = Math.max(prepMins, quoteResult.pickup_duration) + transit; 277 const small = deliveryBtn.querySelector('small'); 278 if (small) small.textContent = `${displayMins} mins`; 279 } 280 // Select delivery button 281 if (deliveryBtn) { 282 this.selectMethod(deliveryBtn, true); 283 this.updatePostcodeDisplay(); 284 } 285 286 this.processPendingAction(); 287 } else { 288 // Uber quote failed - clear stored data 289 this.removePostcodeData(); 290 this.removeSelectedMethod(); 291 this.state.postcodeData = null; 292 293 if (this.state.pendingAction) { 294 this.handleDeliverySelection(); 295 } 296 } 297 } else { 298 // Standard postcode validation (non-Uber or guest without address_id) 299 const response = await fetch(`${STORE_PATH}/checkout/validate-postcode`, { 300 method: 'POST', 301 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 302 body: `postcode=${encodeURIComponent(postcodeToValidate)}` 303 }); 304 305 const result = await response.json(); 306 307 if (result.success) { 308 this.state.postcodeData = { 309 postcode: postcodeToValidate, 310 address: address, 311 addressId: addressId, 312 distance: result.data.distance, 313 deliveryFee: result.data.deliveryFee, 314 validated: true, 315 timestamp: Date.now() 316 }; 317 this.setPostcodeData(this.state.postcodeData); 318 319 // Select delivery button 320 const deliveryBtn = document.querySelector('[data-method="delivery"]'); 321 if (deliveryBtn) { 322 this.selectMethod(deliveryBtn, true); 323 this.updatePostcodeDisplay(); 324 } 325 326 this.processPendingAction(); 327 } else { 328 // Invalid - clear stored data 329 this.removePostcodeData(); 330 this.removeSelectedMethod(); 331 this.state.postcodeData = null; 332 333 if (this.state.pendingAction) { 334 this.handleDeliverySelection(); 335 } 336 } 337 } 338 } catch (error) {
339 console.error('Background validation failed:', error); 340 } finally { 341 this.state.validating = false; 342 this.setButtonsLoading(false); 343 } 344 }, 345 346 canAddToCart(action = null) { 347 if (this.state.shopClosed) { 348 Swal.fire({ 349 icon: 'error', 350 title: 'Shop Closed', 351 text: 'Sorry, we are currently closed for orders.', 352 confirmButtonText: 'OK' 353 }); 354 return false; 355 } 356 357 const availableButtons = document.querySelectorAll('.delivery-method-btn'); 358 if (availableButtons.length === 0) { 359 Swal.fire({ 360 icon: 'error', 361 title: 'No Service Available', 362 text: 'Sorry, ordering is not available at this time.', 363 confirmButtonText: 'OK' 364 }); 365 return false; 366 } 367 368 // If background validation is in progress, store pending and wait 369 if (this.state.validating) { 370 if (action) { 371 this.state.pendingAction = action; 372 } 373 Cart.showNotification('Checking delivery availability...', 'info'); 374 return false; 375 } 376 377 // No method selected - need to prompt user 378 if (!this.state.selectedMethod) { 379 if (action) { 380 this.state.pendingAction = action; 381 } 382 383 // If postcode already validated, auto-select delivery 384 if (this.state.postcodeData) { 385 const deliveryBtn = document.querySelector('[data-method="delivery"]'); 386 if (deliveryBtn) { 387 this.selectMethod(deliveryBtn); 388 this.processPendingAction(); 389 return false; // We'll process via processPendingAction 390 } 391 } 392 393 // Show modal for user to choose 394 if (!this.state.deliveryAvailable) { 395 this.showPostcodeModal('unavailable'); 396 } else { 397 this.handleDeliverySelection(); 398 } 399 return false; 400 } 401 402 // Delivery selected but no validated postcode 403 if (this.state.selectedMethod === 'delivery' && !this.state.postcodeData) { 404 if (action) { 405 this.state.pendingAction = action; 406 } 407 this.showPostcodeModal(); 408 return false; 409 } 410 411 return true; 412 }, 413 414 processPendingAction() { 415 if (!this.state.pendingAction) return; 416 417 const action = this.state.pendingAction; 418 this.state.pendingAction = null; 419 420 if (action.type === 'cart_add' && this.state.originalCartAdd) { 421 this.state.originalCartAdd(...action.args); 422 } else if (action.type === 'show_options' && this.state.originalShowModal) { 423 this.state.originalShowModal(action.button); 424 } else if (action.type === 'show_variations' && this.state.originalShowVariationsModal) { 425 this.state.originalShowVariationsModal(action.button); 426 } else if (action.type === 'product_modal_open' && this.state.originalProductModalOpen) { 427 this.state.originalProductModalOpen(action.card); 428 } else if (action.type === 'product_modal_quickadd' && this.state.originalProductModalQuickAdd) { 429 this.state.originalProductModalQuickAdd(action.card); 430 } 431 }, 432 433 clearPendingAction() { 434 this.state.pendingAction = null; 435 }, 436 437 selectMethod(button, isRestoring = false) { 438 const method = button.dataset.method; 439 const minOrder = parseFloat(button.dataset.minOrder); 440 441 // Deselect all buttons 442 document.querySelectorAll('.delivery-method-btn').forEach(btn => { 443 btn.classList.remove('active'); 444 if (btn.dataset.method === 'delivery') { 445 btn.classList.remove('btn-primary'); 446 btn.classList.add('btn-outline-primary'); 447 } else { 448 btn.classList.remove('btn-warning'); 449 btn.classList.add('btn-outline-warning'); 450 } 451 }); 452 453 // Select this button 454 button.classList.add('active'); 455 if (method === 'delivery') { 456 button.classList.remove('btn-outline-primary'); 457 button.classList.add('btn-primary'); 458 } else { 459 button.classList.remove('btn-outline-warning'); 460 button.classList.add('btn-warning'); 461 } 462 463 this.state.selectedMethod = method; 464 this.state.minOrder = minOrder;
465 this.setSelectedMethod(method); 466 467 // Update postcode display 468 this.updatePostcodeDisplay(); 469 470 // If delivery and no postcode - prompt for it 471 if (method === 'delivery' && !this.state.postcodeData && !isRestoring) { 472 this.handleDeliverySelection(); 473 } else { 474 this.updateMinOrderWarning(); 475 this.updateCheckoutButton(); 476 } 477 }, 478 479 handleDeliverySelection() { 480 if (!this.state.deliveryAvailable) { 481 this.showPostcodeModal('unavailable'); 482 return; 483 } 484 485 const addresses = this.state.userAddresses; 486 487 if (addresses.length === 0) { 488 // No addresses - show postcode input 489 this.showPostcodeModal('input'); 490 } else if (addresses.length === 1) { 491 // Single address - validate it 492 this.validateAddress(addresses[0]); 493 } else { 494 // Multiple addresses - show selection 495 this.showPostcodeModal('select'); 496 } 497 }, 498 499 /** 500 * Validate an address (used for single address auto-validate and address selection) 501 */ 502 async validateAddress(address, showModal = true) { 503 const postcode = address.postcode; 504 505 if (showModal) { 506 Cart.showNotification('Checking delivery to ' + postcode + '...', 'info'); 507 } 508 509 try { 510 // Use Uber quote if enabled 511 if (window.UBER_DIRECT_ENABLED && typeof UberDelivery !== 'undefined') { 512 const quoteResult = await UberDelivery.getQuote({ 513 address_id: address.address_id 514 }); 515 516 if (quoteResult.success) { 517 // Also fetch delivery fee from our pricing 518 const feeResponse = await fetch(`${STORE_PATH}/checkout/validate-postcode`, { 519 method: 'POST', 520 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 521 body: `postcode=${encodeURIComponent(postcode)}` 522 }); 523 const feeResult = await feeResponse.json(); 524 const deliveryFee = feeResult.success ? feeResult.data.deliveryFee : 0; 525 526 this.state.postcodeData = { 527 postcode: quoteResult.postcode || postcode, 528 address: quoteResult.address, 529 addressId: address.address_id, 530 deliveryFee: deliveryFee, 531 validated: true, 532 timestamp: Date.now(), 533 // Uber specific 534 uberQuoteId: quoteResult.quote_id, 535 uberExpires: quoteResult.expires, 536 lat: quoteResult.lat, 537 lng: quoteResult.lng 538 }; 539 this.setPostcodeData(this.state.postcodeData); 540 this.removeFailedAddressId(address.address_id); 541 // Update delivery button time with Uber estimate 542 if (quoteResult.duration) { 543 const btn = document.querySelector('[data-method="delivery"]'); 544 if (btn) { 545 // Update delivery time with Uber estimate 546 if (btn.dataset.isActive === '1' && quoteResult.duration && quoteResult.pickup_duration) { 547 const prepMins = parseInt(btn.dataset.readyMinutes) || 0; 548 const transit = quoteResult.duration - quoteResult.pickup_duration; 549 const displayMins = Math.max(prepMins, quoteResult.pickup_duration) + transit; 550 const small = btn.querySelector('small'); 551 if (small) small.textContent = `${displayMins} mins`; 552 } 553 } 554 } 555 const modalEl = document.getElementById('postcodeModal'); 556 const modal = bootstrap.Modal.getInstance(modalEl); 557 if (modal) { 558 modal.hide(); 559 } 560 561 const deliveryBtn = document.querySelector('[data-method="delivery"]'); 562 if (deliveryBtn) { 563 this.selectMethod(deliveryBtn); 564 } 565 566 Cart.showNotification(`Delivery to ${postcode}`, 'success'); 567 this.processPendingAction(); 568 } else { 569 this.addFailedAddressId(address.address_id); 570 this.showPostcodeError(quoteResult.error || 'Delivery is not available to this address. Try another or choose collection.'); 571 this.populateAddressList(); 572 } 573 } else { 574 // Standard postcode validation 575 const response = await fetch(`${STORE_PATH}/checkout/validate-postcode`, { 576 method: 'POST', 577 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 578 body: `postcode=${encodeURIComponent(postcode)}` 579 }); 580 581 const result = await response.json(); 582 583 if (result.success) { 584 this.state.postcodeData = { 585 postcode: postcode, 586 addressId: address.address_id, 587 distance: result.data.distance, 588 deliveryFee: result.data.deliveryFee, 589 validated: true, 590 timestamp: Date.now() 591 }; 592 this.setPostcodeData(this.state.postcodeData); 593 this.removeFailedAddressId(address.address_id); 594 595 const modalEl = document.getElementById('postcodeModal'); 596 const modal = bootstrap.Modal.getInstance(modalEl); 597 if (modal) { 598 modal.hide(); 599 } 600 601 const deliveryBtn = document.querySelector('[data-method="delivery"]'); 602 if (deliveryBtn) { 603 this.selectMethod(deliveryBtn); 604 } 605 606 Cart.showNotification(`Delivery to ${postcode}`, 'success'); 607 this.processPendingAction(); 608 } else { 609 this.addFailedAddressId(address.address_id); 610 this.showPostcodeError(result.data.message + ' Try another or choose collection.'); 611 this.populateAddressList(); 612 } 613 } 614 } catch (error) { 615 this.showPostcodeError('Network error. Please try again.'); 616 } 617 }, 618 619 showPostcodeModal(mode = 'input') { 620 const modalEl = document.getElementById('postcodeModal'); 621 const modal = bootstrap.Modal.getOrCreateInstance(modalEl); 622 const errorDiv = document.getElementById('postcodeError'); 623 const unavailableSection = document.getElementById('deliveryUnavailableSection'); 624 const addressSection = document.getElementById('addressSelectSection'); 625 const inputSection = document.getElementById('postcodeInputSection');
626 const validateBtn = document.getElementById('validatePostcodeBtn'); 627 const title = document.getElementById('postcodeModalTitle'); 628 const postcodeInput = document.getElementById('postcodeInput'); 629 const houseInput = document.getElementById('houseInput'); 630 631 // Show/hide back link based on mode and saved addresses 632 const backLink = document.getElementById('backToAddressesLink'); 633 if (backLink) { 634 if (mode === 'input' && this.state.userAddresses.length > 0) { 635 backLink.classList.remove('d-none'); 636 } else { 637 backLink.classList.add('d-none'); 638 } 639 } 640 641 // Reset error state 642 errorDiv.classList.add('d-none'); 643 errorDiv.textContent = ''; 644 645 unavailableSection.classList.add('d-none'); 646 addressSection.classList.add('d-none'); 647 inputSection.classList.add('d-none'); 648 validateBtn.classList.add('d-none'); 649 650 if (mode === 'unavailable') { 651 unavailableSection.classList.remove('d-none'); 652 title.innerHTML = '<i class="bi bi-geo-alt"></i> Delivery Unavailable'; 653 } else if (mode === 'select') { 654 addressSection.classList.remove('d-none'); 655 title.innerHTML = '<i class="bi bi-geo-alt"></i> Choose Delivery Address'; 656 this.populateAddressList(); 657 } else { 658 inputSection.classList.remove('d-none'); 659 validateBtn.classList.remove('d-none'); 660 title.innerHTML = window.UBER_DIRECT_ENABLED 661 ? '<i class="bi bi-geo-alt"></i> Delivery Address' 662 : '<i class="bi bi-geo-alt"></i> Delivery Postcode'; 663 postcodeInput.value = ''; 664 if (houseInput) houseInput.value = ''; 665 this.clearAddressResolution(); 666 } 667 668 modal.show(); 669 }, 670 671 populateAddressList() { 672 const container = document.getElementById('addressList'); 673 container.innerHTML = ''; 674 675 const failedIds = this.getFailedAddressIds(); 676 677 this.state.userAddresses.forEach(addr => { 678 const isFailed = failedIds.includes(addr.address_id); 679 const item = document.createElement('button'); 680 item.type = 'button'; 681 item.className = 'list-group-item list-group-item-action' + (isFailed ? ' text-muted' : ''); 682 item.innerHTML = ` 683 <div class="d-flex justify-content-between align-items-center"> 684 <div> 685 <strong${isFailed ? ' class="text-muted"' : ''}>${addr.address_1}</strong> 686 ${addr.address_2 ? `<br><small class="text-muted">${addr.address_2}</small>` : ''} 687 <br><small class="text-muted">${addr.postcode}</small> 688 ${isFailed ? '<br><span class="badge bg-secondary mt-1">Not in delivery area</span>' : ''} 689 </div> 690 <i class="bi bi-chevron-right${isFailed ? ' text-muted' : ''}"></i> 691 </div> 692 `; 693 item.onclick = () => this.validateAddress(addr); 694 container.appendChild(item); 695 }); 696 }, 697 698 showPostcodeInput() { 699 // Switch from address list to postcode input 700 document.getElementById('addressSelectSection').classList.add('d-none'); 701 document.getElementById('postcodeInputSection').classList.remove('d-none');
702 document.getElementById('validatePostcodeBtn').classList.remove('d-none'); 703 document.getElementById('postcodeError').classList.add('d-none'); 704 document.getElementById('postcodeModalTitle').innerHTML = window.UBER_DIRECT_ENABLED 705 ? '<i class="bi bi-geo-alt"></i> Delivery Address' 706 : '<i class="bi bi-geo-alt"></i> Delivery Postcode'; 707 708 // Show back link if user has saved addresses 709 const backLink = document.getElementById('backToAddressesLink'); 710 if (backLink) { 711 if (this.state.userAddresses.length > 0) { 712 backLink.classList.remove('d-none'); 713 } else { 714 backLink.classList.add('d-none'); 715 } 716 } 717 this.clearAddressResolution(); 718 }, 719 720 showAddressSelect() { 721 // Switch from postcode input back to address list 722 document.getElementById('postcodeInputSection').classList.add('d-none'); 723 document.getElementById('addressSelectSection').classList.remove('d-none');
724 document.getElementById('validatePostcodeBtn').classList.add('d-none'); 725 document.getElementById('postcodeError').classList.add('d-none'); 726 document.getElementById('postcodeModalTitle').innerHTML = '<i class="bi bi-geo-alt"></i> Choose Delivery Address'; 727 this.populateAddressList(); 728 729 // Hide back link when showing address list 730 const backLink = document.getElementById('backToAddressesLink'); 731 if (backLink) backLink.classList.add('d-none'); 732 }, 733 734 // Menu delivery modal button: relabels Find -> Continue (Uber), or the 735 // standard postcode check (non-Uber). 736 async deliveryModalAction() { 737 if (!window.UBER_DIRECT_ENABLED) return this.validatePostcodeStandard(); 738 if (this._menuResolved) { this.proceedToDelivery(); } 739 else { await this.findAndCheck(); } 740 }, 741 742 // Non-Uber: validate postcode (radius + fee). No fee shown on this page. 743 async validatePostcodeStandard() { 744 const postcode = document.getElementById('postcodeInput').value.trim().toUpperCase(); 745 if (!postcode) { this.showPostcodeError('Please enter a postcode'); return; } 746
747 const btn = document.getElementById('validatePostcodeBtn'); 748 btn.disabled = true; 749 btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Checking...'; 750 try { 751 const response = await fetch(`${STORE_PATH}/checkout/validate-postcode`, { 752 method: 'POST', 753 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 754 body: `postcode=${encodeURIComponent(postcode)}&hp_field=${encodeURIComponent(document.getElementById('menuHpField')?.value || '')}` 755 }); 756 const result = await response.json(); 757 if (result.success) { 758 this.state.postcodeData = { 759 postcode: postcode, 760 distance: result.data.distance, 761 deliveryFee: result.data.deliveryFee, 762 validated: true, 763 timestamp: Date.now() 764 }; 765 this.setPostcodeData(this.state.postcodeData); 766 bootstrap.Modal.getInstance(document.getElementById('postcodeModal'))?.hide(); 767 const deliveryBtn = document.querySelector('[data-method="delivery"]'); 768 if (deliveryBtn) this.selectMethod(deliveryBtn); 769 Cart.showNotification(`Delivery to ${postcode}`, 'success'); 770 this.processPendingAction(); 771 } else { 772 this.showPostcodeError(result.data.message); 773 } 774 } catch (e) { 775 this.showPostcodeError('Network error. Please try again.'); 776 } finally { 777 btn.disabled = false; 778 btn.innerHTML = 'Check Delivery'; 779 } 780 }, 781
782 // Uber: one pass â findaddress lookup + deliverability + quote. 783 async findAndCheck() { 784 const house = document.getElementById('houseInput').value.trim(); 785 const postcode = document.getElementById('postcodeInput').value.trim().toUpperCase(); 786 const hp = document.getElementById('menuHpField')?.value || ''; 787 788 if (!house || !postcode) { 789 this.showPostcodeError('Please enter your house number/name and postcode.'); 790 return; 791 } 792 793 const key = house.toLowerCase() + '|' + postcode; 794 795 // Identical to the current resolved address and the quote is still fresh 796 // -> no-op: skip both the lookup and the quote, just re-affirm "Continue". 797 if (this._menuResolved 798 && this._lastLookupKey === key 799 && this.state.postcodeData?.uberExpires 800 && new Date(this.state.postcodeData.uberExpires).getTime() > Date.now() + 120000) { 801 this.setDeliveryButtonState('continue'); 802 return; 803 } 804 805 // Debounce: in-flight guard + ~1s min interval 806 const now = Date.now(); 807 if (this._lookupInFlight) return; 808 if (this._lastLookupAt && (now - this._lastLookupAt) < 1000) return; 809 810 this._lookupInFlight = true; 811 this._lastLookupAt = now; 812 this.setDeliveryButtonState('checking'); 813 document.getElementById('postcodeError').classList.add('d-none'); 814 815 try { 816 // Identical-input skip: reuse the last lookup result, no API call 817 let r; 818 if (this._lastLookupKey === key && this._lastLookupResult) { 819 r = this._lastLookupResult; 820 } else { 821 const res = await fetch(`${STORE_PATH}/checkout/lookup-address`, { 822 method: 'POST', 823 headers: { 'Content-Type': 'application/json' }, 824 body: JSON.stringify({ house, postcode, hp_field: hp }) 825 }); 826 r = await res.json(); 827 this._lastLookupKey = key; 828 this._lastLookupResult = r; 829 } 830 831 if (!r.precise) { 832 this.setDeliveryButtonState('find'); 833 this.showPostcodeError('We couldn\u2019t find that address \u2014 check the house number and postcode, then try again.'); 834 return; 835 } 836 837 document.getElementById('addrStreet').value = r.street; 838 document.getElementById('addrTown').value = r.town || ''; 839 document.getElementById('addrCounty').value = r.county || ''; 840 document.getElementById('addrLat').value = r.lat ?? ''; 841 document.getElementById('addrLng').value = r.lng ?? ''; 842 document.getElementById('addrGeoPrecise').value = '1'; 843 844 // Deliverability via Uber 845 const quoteResult = await UberDelivery.getQuote({ 846 address: r.street, postcode: postcode, 847 lat: r.lat, lng: r.lng, town: r.town, county: r.county 848 }); 849 if (!quoteResult.success) { 850 this.setDeliveryButtonState('find'); 851 this.showPostcodeError(quoteResult.error || 'Sorry, we can\u2019t deliver to this address.'); 852 return; 853 } 854 855 // Fee (stored for checkout, not shown here) 856 const feeResponse = await fetch(`${STORE_PATH}/checkout/validate-postcode`, { 857 method: 'POST', 858 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 859 body: `postcode=${encodeURIComponent(postcode)}&hp_field=${encodeURIComponent(hp)}` 860 }); 861 const feeResult = await feeResponse.json(); 862 const deliveryFee = feeResult.success ? feeResult.data.deliveryFee : 0; 863 864 this.state.postcodeData = { 865 postcode: postcode, address: r.street, town: r.town, county: r.county, geoPrecise: 1, 866 deliveryFee: deliveryFee, validated: true, timestamp: Date.now(), 867 uberQuoteId: quoteResult.quote_id, uberExpires: quoteResult.expires, 868 lat: quoteResult.lat, lng: quoteResult.lng 869 }; 870 this.setPostcodeData(this.state.postcodeData); 871 this._pendingQuote = quoteResult; 872 873 document.getElementById('resolvedAddressText').innerHTML = 874 this.escapeHtml(r.street) + 875 (r.town ? ', ' + this.escapeHtml(r.town) : '') + 876 (r.county ? ', ' + this.escapeHtml(r.county) : '') + 877 ' ' + this.escapeHtml(postcode); 878 document.getElementById('resolvedAddress').classList.remove('d-none'); 879 this._menuResolved = true; 880 this.setDeliveryButtonState('continue'); 881 } catch (e) { 882 this.setDeliveryButtonState('find'); 883 this.showPostcodeError('Couldn\u2019t check right now. Please try again.'); 884 } finally { 885 this._lookupInFlight = false; 886 } 887 }, 888 889 // Uber: user confirmed the resolved address â proceed to ordering. 890 proceedToDelivery() { 891 const deliveryBtn = document.querySelector('[data-method="delivery"]'); 892 const q = this._pendingQuote; 893 if (deliveryBtn && q && deliveryBtn.dataset.isActive === '1' && q.duration && q.pickup_duration) { 894 const prepMins = parseInt(deliveryBtn.dataset.readyMinutes) || 0; 895 const transit = q.duration - q.pickup_duration; 896 const displayMins = Math.max(prepMins, q.pickup_duration) + transit; 897 const small = deliveryBtn.querySelector('small'); 898 if (small) small.textContent = `${displayMins} mins`; 899 } 900 bootstrap.Modal.getInstance(document.getElementById('postcodeModal'))?.hide(); 901 if (deliveryBtn) this.selectMethod(deliveryBtn); 902 Cart.showNotification(`Delivery to ${this.state.postcodeData.postcode}`, 'success'); 903 this.processPendingAction(); 904 }, 905 906 // Editing house/postcode voids a prior resolution -> back to "Find". 907 onAddressInput() { 908 if (this._menuResolved) this.clearAddressResolution(); 909 }, 910 911 setDeliveryButtonState(mode) {
912 const btn = document.getElementById('validatePostcodeBtn'); 913 if (!btn) return; 914 btn.classList.remove('btn-primary', 'btn-outline-primary', 'btn-success'); 915 btn.disabled = false; 916 if (mode === 'continue') { 917 btn.classList.add('btn-success'); 918 btn.innerHTML = 'Continue <i class="bi bi-arrow-right ms-1"></i>'; 919 } else if (mode === 'checking') { 920 btn.classList.add('btn-outline-primary'); 921 btn.disabled = true; 922 btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Checking...'; 923 } else { // 'find' 924 btn.classList.add('btn-outline-primary'); 925 btn.innerHTML = 'Find'; 926 } 927 }, 928 929 clearAddressResolution() { 930 ['addrStreet','addrTown','addrCounty','addrLat','addrLng'].forEach(id => { 931 const el = document.getElementById(id); if (el) el.value = ''; 932 }); 933 const gp = document.getElementById('addrGeoPrecise'); if (gp) gp.value = '0'; 934 const rv = document.getElementById('resolvedAddress'); if (rv) rv.classList.add('d-none'); 935 this._menuResolved = false; 936 if (window.UBER_DIRECT_ENABLED) this.setDeliveryButtonState('find'); 937 }, 938 939 escapeHtml(str) { 940 const d = document.createElement('div'); 941 d.textContent = (str == null) ? '' : String(str); 942 return d.innerHTML; 943 }, 944 945 showPostcodeError(message) { 946 const errorDiv = document.getElementById('postcodeError'); 947 errorDiv.textContent = message; 948 errorDiv.classList.remove('d-none'); 949 }, 950 951 closePostcodeModal() { 952 const modalEl = document.getElementById('postcodeModal'); 953 const modal = bootstrap.Modal.getOrCreateInstance(modalEl); 954 modal.hide(); 955 956 // Select collection and process pending action 957 const collectionBtn = document.querySelector('[data-method="collection"]'); 958 if (collectionBtn) { 959 this.selectMethod(collectionBtn); 960 this.processPendingAction(); 961 } else { 962 this.clearPendingAction(); 963 } 964 }, 965 966 /** 967 * Update the postcode display UI 968 * Shows validated postcode with change option when delivery is selected 969 */ 970 updatePostcodeDisplay() { 971 let displayEl = document.getElementById('postcodeDisplay'); 972 973 // Create element if it doesn't exist 974 if (!displayEl) { 975 const methodsSection = document.querySelector('.delivery-methods-section .card-body'); 976 if (!methodsSection) return; 977 978 displayEl = document.createElement('div'); 979 displayEl.id = 'postcodeDisplay'; 980 displayEl.className = 'd-none mt-2 small'; 981 methodsSection.appendChild(displayEl); 982 } 983 984 // Show/hide based on state 985 if (this.state.selectedMethod === 'delivery' && this.state.postcodeData) { 986 const postcode = this.state.postcodeData.postcode; 987 const fee = this.state.postcodeData.deliveryFee; 988 989 displayEl.innerHTML = ` 990 <div class="d-flex justify-content-between align-items-center text-muted"> 991 <span> 992 <i class="bi bi-geo-alt-fill me-1"></i> 993 ${postcode} 994 </span> 995 <button type="button" class="btn btn-link btn-sm p-0" onclick="DeliveryManager.changePostcode()"> 996 Change 997 </button> 998 </div> 999 `; 1000 displayEl.classList.remove('d-none'); 1001 } else { 1002 displayEl.classList.add('d-none'); 1003 } 1004 }, 1005 1006 /** 1007 * Allow user to change their postcode 1008 */ 1009 changePostcode() { 1010 // Clear current postcode data 1011 this.state.postcodeData = null; 1012 this.removePostcodeData(); 1013 1014 // Clear Uber quote data if enabled 1015 if (window.UBER_DIRECT_ENABLED && typeof UberDelivery !== 'undefined') { 1016 UberDelivery.clearQuote(); 1017 } 1018 1019 // Update display 1020 this.updatePostcodeDisplay(); 1021 1022 // Show modal 1023 if (this.state.userAddresses.length > 1) { 1024 this.showPostcodeModal('select'); 1025 } else { 1026 this.showPostcodeModal('input'); 1027 } 1028 }, 1029 1030 updateMinOrderWarning() { 1031 // PromoManager handles this with post-discount total when available 1032 if (typeof PromoManager !== 'undefined') return; 1033 1034 if (!this.state.selectedMethod) return; 1035 1036 const cartTotal = Cart.getSubtotal(); 1037 const minOrder = this.state.minOrder; 1038 1039 const warningDiv = document.getElementById('minOrderWarning'); 1040 const warningText = document.getElementById('minOrderText'); 1041 if (warningDiv && warningText) { 1042 if (minOrder > 0 && cartTotal < minOrder) { 1043 const remaining = minOrder - cartTotal; 1044 warningText.textContent = `Min. order not met (add £${remaining.toFixed(2)})`; 1045 warningDiv.classList.remove('d-none'); 1046 } else { 1047 warningDiv.classList.add('d-none'); 1048 } 1049 } 1050 }, 1051 1052 updateCheckoutButton() { 1053 const checkoutBtn = document.getElementById('checkout-btn'); 1054 const mobileCheckoutBtn = document.getElementById('mobile-checkout-btn'); 1055 const cartCount = Cart.getCount(); 1056 const cartTotal = Cart.getSubtotal() - (typeof PromoManager !== 'undefined' ? PromoManager.getCurrentDiscount() : 0); 1057 const availableButtons = document.querySelectorAll('.delivery-method-btn'); 1058 const noMethodsAvailable = availableButtons.length === 0; 1059 const belowMinOrder = this.state.minOrder > 0 && cartTotal < this.state.minOrder; 1060 1061 const shouldDisable = 1062 cartCount === 0 || 1063 this.state.shopClosed || 1064 noMethodsAvailable || 1065 !this.state.selectedMethod || 1066 belowMinOrder; 1067 1068 // Determine button text based on state 1069 let desktopText = '<i class="bi bi-credit-card"></i> Checkout'; 1070 let mobileText = '<i class="bi bi-credit-card"></i> Checkout'; 1071 1072 if (this.state.shopClosed) { 1073 desktopText = '<i class="bi bi-x-circle"></i> Shop Closed'; 1074 mobileText = '<i class="bi bi-x-circle"></i> Closed'; 1075 } else if (noMethodsAvailable) { 1076 desktopText = '<i class="bi bi-clock"></i> Not Available'; 1077 mobileText = '<i class="bi bi-clock"></i> Not Available'; 1078 } else if (cartCount === 0) { 1079 desktopText = '<i class="bi bi-credit-card"></i> Checkout'; 1080 mobileText = '<i class="bi bi-credit-card"></i> Checkout'; 1081 } else if (!this.state.selectedMethod) { 1082 desktopText = 'Choose Delivery/Collection'; 1083 mobileText = 'Select Method'; 1084 } else if (belowMinOrder) { 1085 const remaining = (this.state.minOrder - cartTotal).toFixed(2); 1086 desktopText = `Spend £${remaining} more to checkout`; 1087 mobileText = `£${remaining} more required`; 1088 } 1089 1090 if (checkoutBtn) { 1091 checkoutBtn.disabled = shouldDisable; 1092 checkoutBtn.innerHTML = desktopText; 1093 } 1094 1095 if (mobileCheckoutBtn) { 1096 mobileCheckoutBtn.disabled = shouldDisable; 1097 mobileCheckoutBtn.innerHTML = mobileText; 1098 } 1099 }, 1100 1101 setButtonsLoading(loading) { 1102 const buttons = document.querySelectorAll('.delivery-method-btn'); 1103 buttons.forEach(btn => { 1104 if (loading) { 1105 btn.classList.add('disabled'); 1106 btn.style.pointerEvents = 'none'; 1107 if (btn.dataset.method === 'delivery') { 1108 const small = btn.querySelector('small'); 1109 if (small) { 1110 small.dataset.originalText = small.textContent; 1111 small.innerHTML = '<span class="spinner-border spinner-border-sm me-1" style="width: 0.75rem; height: 0.75rem;"></span>Checking...'; 1112 } 1113 } 1114 } else { 1115 btn.classList.remove('disabled'); 1116 btn.style.pointerEvents = ''; 1117 if (btn.dataset.method === 'delivery') { 1118 const small = btn.querySelector('small');
1119 if (small && small.textContent.includes('Checking') && small.dataset.originalText) { 1120 small.textContent = small.dataset.originalText; 1121 } 1122 } 1123 } 1124 }); 1125 } 1126}; 1127 1128document.addEventListener('DOMContentLoaded', () => { 1129 DeliveryManager.init(); 1130});
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.