/** * CheckoutSquare — Square Web Payments SDK checkout * * Extends CartCheckout with: * - Square Web Payments SDK initialization * - Card tokenization (new card or saved card) * - Square-specific API submission * - Saved cards via Square Customers API * * All cart management, totals, UI, order tracking, etc. are * inherited from CartCheckout (cart-checkout.min.js). * * HTML structure: Checkout.php (JVBase\ui\Checkout) * - Mounts card form into #payment-container * - Uses data-catalog-id (mapped to catalog_object_id for Square API) */ class CheckoutSquare extends window.jvbCheckout { constructor(config = {}) { super({ ...window.squareConfig, ...config, }); // Square-specific this.payments = null; this.card = null; } /***************************************************************** * INIT — Square Web Payments SDK *****************************************************************/ 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; document.dispatchEvent(new CustomEvent('checkoutReady', { detail: { checkout: this, provider: 'square' } })); } catch (error) { console.error('Failed to initialize Square payments:', error); this.handleError(error); } } async initializePaymentMethods() { const cardContainer = document.getElementById('payment-container'); if (!cardContainer) return; try { this.card = await this.payments.card({ style: this.getCardStyle() }); await this.card.attach('#payment-container'); this.card.addEventListener('cardBrandChanged', (event) => { console.log('Card brand:', event.detail.cardBrand); }); } 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' } }; } /***************************************************************** * PAYMENT — Square tokenization *****************************************************************/ async processPayment(orderData) { try { let sourceToken = null; 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, } } }); 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'); } } return await this.submitToServer(sourceToken, orderData, !!this.selectedCardId); } catch (error) { console.error('Payment processing failed:', error); throw error; } } /***************************************************************** * SERVER — Square REST endpoint *****************************************************************/ 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; } /***************************************************************** * ORDER DATA — Square-specific field mapping * * Base class builds the generic structure. We override to map * catalog_id → catalog_object_id for the Square Orders API. *****************************************************************/ extractOrderData(form) { const base = super.extractOrderData(form); // Remap for Square's Orders API base.items = base.items.map(item => ({ catalog_object_id: item.catalog_id, quantity: item.quantity, price: item.price, note: item.note, })); return base; } /***************************************************************** * SAVED CARDS — Square Customers API *****************************************************************/ 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); } } } /***************************************************************** * BOOTSTRAP — only init if Square is the active provider *****************************************************************/ document.addEventListener('DOMContentLoaded', () => { const form = document.querySelector('#checkout[data-provider="square"]'); if (form) { window.squareCheckout = new CheckoutSquare(); } });