PageSourceSearch

https://cinnamon-brockley.co.uk/assets/js/delivery.js?v=1.2.4

js cinnamon-brockley.co.uk collected 2026-10-08 20:40:33 UTC 48,810 bytes, 1,130 lines download raw bytes

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.