class SquareCheckout { constructor(config = {}) { this.config = { ...squareConfig, ...config }; this.payments = null; this.card = null; this.isInitialized = false; this.cartItems = new Map(); this.checkout = document.querySelector('aside#cart'); this.isOpen = this.config.isOpen !== '1' || false; //User Context this.isLoggedIn = this.config.is_logged_in|| false; this.userEmail = this.config.user_email || ''; this.savedCards = []; this.selectedCardId = null; this.cartId = null; this.cache = new window.jvbCache('cart', {TTL: 8.64e+7}); this.a11y = window.jvbA11y; this.initCart(); if (this.checkout) { this.initElements(); this.init(); this.initListeners(); if (this.isLoggedIn) { this.loadSavedCards(); } } this.stepMultiplier = 1; //Handle the opening and closing of the checkout window this.popup = new window.jvbPopup({ popup: this.checkout, toggle: this.toggle, name: 'Cart', onOpen: this.maybeAddEmptyState.bind(this), }); console.log(this.popup); // 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 (!this.isOpen) { 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'); } initListeners() { this.clickHandler = this.handleClick.bind(this); this.keyHandler = this.handleEscape.bind(this); this.changeHandler = this.handleChange.bind(this); 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'); this.card.addEventListener('cardBrandChanged', (event) => { console.log('Card brand:', event.detail.cardBrand); // You could show card brand icon here }); } 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: '#006AFF', borderWidth: '2px', outline: '2px solid #006AFF', outlineOffset: '2px' }, '.input-container.is-error': { borderColor: '#d63638' } }; } async handleFormSubmit(event) { if (!this.isOpen) { 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) { const items = Array.from(this.cartItems.values()).map(item => ({ catalog_object_id: item.square_catalog_id, quantity: String(item.quantity), price: item.price, note: item.note || '' })); const total = items.reduce((sum, item) => sum + (item.price * item.quantity), 0 ); // Pre-fill customer info if logged in return { total: Math.round(total * 100), items: items, customer: { email: this.isLoggedIn ? this.userEmail : (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 { let sourceToken = null; // Check if using saved card or new card if (this.selectedCardId) { // Use saved card sourceToken = this.selectedCardId; } else { // Tokenize new card const tokenResult = await this.card.tokenize({ verificationDetails: { amount: String(orderData.total), currencyCode: this.config.currency || 'CAD', intent: 'CHARGE', customerInitiated: true, billingContact: { givenName: orderData.customer.name.split(' ')[0], familyName: orderData.customer.name.split(' ').slice(1).join(' '), email: orderData.customer.email, phone: orderData.customer.phone, addressLines: [form.querySelector('[name="address"]')?.value || ''], city: form.querySelector('[name="city"]')?.value || '', state: form.querySelector('[name="state"]')?.value || '', postalCode: form.querySelector('[name="postal_code"]')?.value || '', countryCode: 'CA' // or 'US' } } }); if (tokenResult.status !== 'OK') { const errors = tokenResult.errors?.map(e => e.message).join(', ') || 'Unknown error'; throw new Error(`Card tokenization failed: ${errors}`); } sourceToken = tokenResult.token; if (tokenResult.details?.userChallenged) { console.log('3D Secure verification completed'); } } // Send to server return await this.submitToServer(sourceToken, orderData, !!this.selectedCardId); } catch (error) { console.error('Payment processing failed:', error); throw error; } } async submitToServer(sourceToken, orderData, isSavedCard = false) { if (!this.isOpen) { throw new Error('Store is currently closed'); } const response = await fetch(this.config.api_url + 'process-payment', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': this.config.nonce }, body: JSON.stringify({ source_id: sourceToken, is_saved_card: isSavedCard, cart_id: this.getCartId(), amount: orderData.total, items: orderData.items, customer: { email: this.isLoggedIn ? this.userEmail : orderData.customer.email, name: orderData.customer.name, phone: orderData.customer.phone }, note: orderData.note, pickup_time: orderData.pickup_time }) }); const result = await response.json(); if (!response.ok) { throw new Error(result.message || 'Payment processing failed'); } this.clearCart(); return result; } getCartId() { // Generate once per cart session if (!this.cartId) { this.cartId = crypto.randomUUID(); this.cache.set('cart_id', this.cartId); } return this.cartId; } 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 **************************************************************/ /** * Load saved cards for logged-in user */ async loadSavedCards() { try { const response = await fetch(this.config.api_url + 'saved-cards', { method: 'GET', headers: { 'X-WP-Nonce': this.config.nonce } }); const result = await response.json(); if (result.success && result.cards) { this.savedCards = result.cards; this.renderSavedCards(); } } catch (error) { console.error('Failed to load saved cards:', error); } } /** * Render saved cards in the checkout form */ renderSavedCards() { const container = document.getElementById('saved-cards'); if (!container || this.savedCards.length === 0) { return; } const html = `

Saved Payment Methods

${this.savedCards.map(card => ` `).join('')}
`; container.innerHTML = html; // Listen for payment method selection container.querySelectorAll('input[name="payment-method"]').forEach(radio => { radio.addEventListener('change', (e) => { const useNewCard = e.target.value === 'new'; const cardContainer = document.getElementById('square-card-container'); if (cardContainer) { cardContainer.style.display = useNewCard ? 'block' : 'none'; } this.selectedCardId = useNewCard ? null : e.target.dataset.cardId; }); }); } handleSuccess(result, form) { // Trigger success event document.dispatchEvent(new CustomEvent('squareCheckoutSuccess', { detail: { result, form } })); // Default redirect behavior const successUrl = form.dataset.successUrl || `/order-confirmation/?order=${result.wp_order_id}`; window.location.href = successUrl; } handleError(error) { console.error('Square checkout error:', error); // Trigger error event document.dispatchEvent(new CustomEvent('squareCheckoutError', { detail: { error } })); // Default error display window.jvbNotifications?.show?.(error.message || 'Payment failed', 'error'); } } document.addEventListener('DOMContentLoaded', () =>{ window.squareCheckout = new SquareCheckout(); }); // Usage example // document.addEventListener('DOMContentLoaded', async function() { // // Get config from WordPress // const configResponse = await fetch('/wp-json/jvb/v1/square/payment-form'); // const config = await configResponse.json(); // // const checkout = new SquareCheckout(config); // // // Handle form submission // document.getElementById('checkout-form').addEventListener('submit', async function(e) { // e.preventDefault(); // // const orderData = { // total: parseFloat(this.dataset.total), // items: JSON.parse(this.dataset.items), // customer: { // email: this.querySelector('[name="email"]').value, // name: this.querySelector('[name="name"]').value // } // }; // // try { // const result = await checkout.processPayment(orderData); // // Handle success - redirect or show confirmation // window.location.href = '/order-confirmation/?order=' + result.wp_order_id; // } catch (error) { // // Handle error // alert('Payment failed: ' + error.message); // } // }); // });