class SquareCheckout { constructor(config = {}) { this.checkout = document.querySelector('aside#cart'); if (!this.checkout) { return; } this.config = Object.assign({ application_id: squareConfig.application_id, location_id: squareConfig.location_id, api_url: squareConfig.api_url, nonce: squareConfig.nonce, currency: squareConfig.currency || 'CAD' }, config); this.stepMultiplier = 1; this.cache = new window.jvbCache('cart', {TTL: 8.64e+7}); this.a11y = window.jvbA11y; this.initCart(); this.payments = null; this.card = null; this.isInitialized = false; this.clickHandler = this.handleClick.bind(this); this.keyHandler = this.handleEscape.bind(this); this.changeHandler = this.handleChange.bind(this); this.initElements(); this.bindEvents(); this.popup = new window.jvbPopup({ popup: this.checkout, toggle: this.toggle, name: 'Cart', onOpen: this.maybeAddEmptyState.bind(this), }); this.init(); this.toggle.hidden = false; } async initCart() { this.cartItems = await this.cache.get('cart') ?? new Map(); console.log('cart',this.cartItems); if (this.cartItems.size > 0) { this.notifyRestoredCart(); } } handleClick(e) { if (window.targetCheck(e, 'button') && window.targetCheck(e, 'div.quantity')) { let quantity = window.targetCheck(e, 'div.quantity'); this.handleNumberClick(e, quantity); }else if (window.targetCheck(e, '[data-add-to-cart]')) { let add = window.targetCheck(e, '[data-add-to-cart]'); this.handleAddToCart(add); } else if (window.targetCheck(e, '[data-remove-from-cart]')) { let remove = window.targetCheck(e, '[data-remove-from-cart]'); this.handleRemoveFromCart(remove); } else if (window.targetCheck(e, '[data-clear-cart]')) { this.clearCart(); } } handleChange(e, container) { console.log('Checkout change'); let input = window.targetCheck(e, '.quantity-input'); if (input) { let item = e.target.closest('.quantity'); let value = input.value; if (window.targetCheck(e, '.cart-items')) { let main = document.querySelector(`.menu-section [data-id="${item.dataset.id}"] input`); if (main) { main.value = input.value; } } if (value > 0) { this.handleAddToCart(item); } else { this.handleRemoveFromCart(item); } } } handleNumberClick(e, container) { console.log(container); e.preventDefault(); let change = 0; let action = ''; if (e.target.closest('.increase')) { change += 1; } else if (e.target.closest('.decrease')) { change -=1; } if (change !== 0) { let [ step, input ] = [ parseInt(container.dataset.step), container.querySelector('input'), ]; let value = (input.value === '') ? 0 : parseInt(input.value); input.value = (value + (step * change * this.stepMultiplier)); input.dispatchEvent(new Event('change', {bubbles: true})); this.handleNumberLimits(container); } } handleNumberLimits(container) { let [ min, max, input, increase, decrease ] = [ container.dataset.min, container.dataset.max, container.querySelector('input'), container.querySelector('.increase'), container.querySelector('.decrease') ]; let value = parseInt(input.value); if (value < min) { input.value = min; decrease.disabled = true; } else if (value > max) { input.value = max; increase.disabled = false; } else if (increase.disabled) { increase.disabled = false; } else if (decrease.disabled) { decrease.disabled = false; } } maybeAddEmptyState() { let empty = this.itemsList.querySelector('.empty'); if(empty) { empty.remove(); } if (this.cartItems.size === 0) { this.checkoutPanel.disabled = true; this.checkoutPanel.title = 'Add some things to your cart first!'; let empty = window.getTemplate('emptyCart'); this.itemsList.append(empty); this.table.closest('table').hidden = true; this.total.hidden = true; this.a11y.announce('Nothing in Cart'); } else { this.checkoutPanel.disabled = false; this.table.closest('table').hidden = false; this.total.hidden = false; this.checkoutPanel.title = 'Checkout'; } } handleEscape(e) { if (e.key === 'Escape') { this.stepMultiplier = 1; } else if (e.ctrlKey && e.shiftKey) { this.stepMultiplier = Math.max(parseInt(this.stepMultiplier) * 100, 1000); } else if (e.shiftKey) { this.stepMultiplier = Math.max(parseInt(this.stepMultiplier) * 10, 1000); } } handleAddToCart(item) { let id = item.dataset.id; this.createItemElement(item); let price = parseFloat(item.dataset.price); let quantity = parseInt(item.querySelector('.quantity-input')?.value)??1; let total = parseFloat(price * quantity); this.cartItems.set(id, { post_id: id, name: item.dataset.name, price: price, quantity: quantity, total: total, square_catalog_id: item.dataset.squareCatalogId }); this.saveCart(); } notifyRestoredCart() { let restored = window.getTemplate('restoredCart'); this.checkout.querySelector('.tab-content[data-tab=cartItems]').insertBefore(restored, this.itemsList); this.cartItems.forEach(item => { console.log(item); let element = window.getTemplate('cartItem'); let field = element.querySelector('.quantity'); let price = item.price; let quantity = item.quantity; [ field.dataset.id, element.querySelector('label').textContent, element.querySelector('.price').textContent, field.dataset.price, field.dataset.squareCatalogId, element.querySelector('[name="quantity"]').value, element.querySelector('.total').textContent ] = [ item['post_id'], item.name, window.formatPrice(price), price, item['square_catalog_id'], quantity, window.formatPrice(quantity * price) ]; this.table.append(element); }); this.updateTotal(); } handleRemoveFromCart(item) { if (confirm('This will remove this item from the cart. Continue?')) { if (!item.querySelector('[data-id]')) { //it's a remove button item = item.closest('.item')?.querySelector('.quantity.field'); } let id = item.dataset.id; this.cartItems.delete(id); this.table.querySelector(`[data-id="${id}"]`)?.closest('tr').remove(); //Reset the field value to 0 too let input = document.querySelector(`[data-id="${id}"] input`); if (input){ input.value = 0; } this.maybeAddEmptyState(); this.saveCart(); } } clearCart() { this.cartItems.clear(); window.removeChildren(this.table); this.saveCart(); } saveCart() { this.updateTotal(); this.cache.set('cart', this.cartItems); } updateTotal() { let total = 0; this.cartItems.forEach(item => { console.log(item); total += item.total; }); let tax = total * .05; total = window.formatPrice(total + tax); tax = window.formatPrice(tax); window.eraseText(this.totalTax); window.eraseText(this.grandTotal); window.typeText(this.totalTax, tax); window.typeText(this.grandTotal, total); this.totalTax.classList.remove('typeText'); } createItemElement(item) { let element = this.itemsList.querySelector(`[data-id="${item.dataset.id}"]`); let add = false; let price = item.dataset.price; let quantity = item.querySelector('[name="quantity"]')?.value??1; if (!element) { add = true; element = window.getTemplate('cartItem'); let field = element.querySelector('.quantity'); [ field.dataset.id, element.querySelector('label').textContent, element.querySelector('.price').textContent, field.dataset.price, field.dataset.squareCatalogId ] = [ item.dataset.id, item.dataset.name, window.formatPrice(price), price, item.dataset.squareCatalogId ]; }else { element = element.closest('tr'); } [ element.querySelector('[name="quantity"]').value, element.querySelector('.total').textContent ] = [ quantity, window.formatPrice(quantity * price) ]; if (add) { element.classList.add('adding'); this.table.append(element); setTimeout(()=> { element.classList.remove('adding'); }, 500); } } async init() { if (!window.Square) { console.error('Square Web Payments SDK not loaded'); return; } try { this.payments = window.Square.payments( this.config.application_id, this.config.location_id ); await this.initializePaymentMethods(); this.isInitialized = true; // Trigger ready event document.dispatchEvent(new CustomEvent('squareCheckoutReady', { detail: { checkout: this } })); } catch (error) { console.error('Failed to initialize Square payments:', error); this.handleError(error); } } initElements() { this.toggle = document.querySelector('.toggle-cart'); if (squareConfig.isOpen !== '1') { this.toggle.disabled = true; this.toggle.title = 'Currently closed for online ordering'; } this.checkoutPanel = this.checkout.querySelector('button[data-tab="checkout"]'); this.itemsList = this.checkout.querySelector('.cart-items'); this.table = this.checkout.querySelector('.cart-items tbody'); this.total = this.checkout.querySelector('.cart-total'); this.totalTax = this.total.querySelector('.tax span'); this.grandTotal = this.total.querySelector('.total span'); this.checkoutForm = this.checkout.querySelector('form'); this.tabs = new window.jvbTabs(this.checkoutForm, { updateURL: false }); console.log('Initialized Checkout'); } bindEvents() { this.checkoutForm.addEventListener('submit', (e) => this.handleFormSubmit(e)); document.addEventListener('click', this.clickHandler); document.addEventListener('change', this.changeHandler); } async initializePaymentMethods() { const cardContainer = document.getElementById('square-card-container'); if (!cardContainer) return; try { this.card = await this.payments.card({ style: this.getCardStyle() }); await this.card.attach('#square-card-container'); } catch (error) { console.error('Failed to initialize card:', error); throw error; } } getCardStyle() { return { input: { fontSize: '16px', fontFamily: 'inherit', color: '#333', backgroundColor: '#fff' }, '.input-container': { borderColor: '#ccc', borderRadius: '4px' }, '.input-container.is-focus': { borderColor: '#007cba' }, '.input-container.is-error': { borderColor: '#d63638' } }; } async handleFormSubmit(event) { if (squareConfig.isOpen !== '1') { return; } event.preventDefault(); if (!this.isInitialized) { this.handleError('Checkout not initialized'); return; } const form = event.target; const orderData = this.extractOrderData(form); try { window.jvbLoading.showLoading('Processing payment...'); const result = await this.processPayment(orderData); this.handleSuccess(result, form); } catch (error) { this.handleError(error); } finally { window.jvbLoading.hideLoading(); } } extractOrderData(form) { // Convert cart items Map to array with proper structure const items = Array.from(this.cartItems.values()).map(item => ({ post_id: item.post_id, quantity: item.quantity, price: item.price, name: item.name })); const total = items.reduce((sum, item) => sum + (item.price * item.quantity), 0 ); return { total: total * 100, // Square expects amount in cents items: items, customer: { email: form.querySelector('[name="email"]')?.value || '', name: form.querySelector('[name="name"]')?.value || '', phone: form.querySelector('[name="phone"]')?.value || '' }, note: form.querySelector('[name="special_instructions"]')?.value || '', pickup_time: form.querySelector('[name="pickup_time"]')?.value || '' }; } async processPayment(orderData) { try { const result = await this.card.tokenize(); if (result.status === 'OK') { return await this.submitToServer(result.token, orderData); } else { throw new Error('Card tokenization failed: ' + (result.errors?.join(', ') || 'Unknown error')); } } catch (error) { console.error('Payment processing failed:', error); throw error; } } async submitToServer(token, orderData) { if (squareConfig.isOpen !== '1') { return; } // Square Web Payments SDK handles EVERYTHING // We just need to track the order for status updates const response = await fetch(this.config.api_url + 'save-order', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': this.config.nonce }, body: JSON.stringify({ order_id: token.orderId, // From Square SDK response payment_id: token.paymentId, // From Square SDK response customer: orderData.customer, items: orderData.items, action: 'jvb_integration_action', service: 'square', integration_action: 'save_order' }) }); const result = await response.json(); if (!response.ok) { throw new Error(result.message || 'Failed to save order'); } return result; } trackOrder(orderNum) { this.orderId = orderNum; this.scheduleOrderCheck(); this.checkout.querySelector('button[data-tab=order]').hidden = false; } scheduleOrderCheck(){ window.debouncer.schedule( 'order', () => { this.checkOrderStatus()}, 30000 //30seconds ) } async checkOrderStatus() { const response = await fetch(`/wp-json/jvb/v1/square/order-status/${this.orderId}`); const data = await response.json(); if (data.status !== 'ready') { this.scheduleOrderCheck(); } this.updateOrderStatus(data); } updateOrderStatus(data) { // Update status timeline this.checkout.querySelectorAll('.status-item').forEach(item => { if(item.dataset.status === data.status) { item.classList.add('active'); } }); // Update ETA this.checkout.querySelector('#eta').textContent = data.eta || 'In progress'; } /************************************************************** * Customer Data **************************************************************/ async loadCustomerProfile(email) { const response = await fetch('/wp-json/jvb/v1/square/customer', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': this.config.nonce }, body: JSON.stringify({ email }) }); const profile = await response.json(); if (profile) { this.displaySavedCards(profile.cards); this.fillCustomerInfo(profile.customer); } } displaySavedCards(cards) { const container = document.getElementById('saved-cards'); if (!cards.length) return; container.innerHTML = `