From 42fa8304ddb811b0f725f245130f70c0f5e86a6c Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Tue, 04 Nov 2025 06:12:02 +0000
Subject: [PATCH] =Refactored LoginManager to be more extensible and configurable, as well as an AjaxRateLimiter

---
 assets/js/dash/SquareCheckout.js |  258 +++++++++++++++++++++++++++++++++++----------------
 1 files changed, 176 insertions(+), 82 deletions(-)

diff --git a/assets/js/dash/SquareCheckout.js b/assets/js/dash/SquareCheckout.js
index 3fc9a3c..72d033e 100644
--- a/assets/js/dash/SquareCheckout.js
+++ b/assets/js/dash/SquareCheckout.js
@@ -1,50 +1,50 @@
 class SquareCheckout {
 	constructor(config = {}) {
+		this.config = {
+			...squareConfig,
+			...config
+		};
 
-		this.checkout = document.querySelector('aside#cart');
-		if (!this.checkout) {
-			return;
-		}
+		this.payments 		= null;
+		this.card 			= null;
+		this.isInitialized 	= false;
+		this.cartItems 		= new Map();
+		this.checkout 		= document.querySelector('aside#cart');
 
-		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.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();
 
-		this.payments = null;
-		this.card = null;
-		this.isInitialized = false;
+			if (this.isLoggedIn) {
+				this.loadSavedCards();
+			}
+		}
 
+		this.stepMultiplier = 1;
 
-		this.clickHandler = this.handleClick.bind(this);
-		this.keyHandler = this.handleEscape.bind(this);
-		this.changeHandler = this.handleChange.bind(this);
-
-
-		this.initElements();
-		this.bindEvents();
-
+		//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),
 		});
-		this.init();
 
-		this.toggle.hidden = false;
+		console.log(this.popup);
+		// this.toggle.hidden = false;
 	}
 
 	async initCart() {
@@ -54,6 +54,7 @@
 			this.notifyRestoredCart();
 		}
 	}
+
 	handleClick(e) {
 		if (window.targetCheck(e, 'button') && window.targetCheck(e, 'div.quantity')) {
 			let quantity = window.targetCheck(e, 'div.quantity');
@@ -347,7 +348,7 @@
 	initElements() {
 		this.toggle = document.querySelector('.toggle-cart');
 
-		if (squareConfig.isOpen !== '1') {
+		if (!this.isOpen) {
 			this.toggle.disabled = true;
 			this.toggle.title = 'Currently closed for online ordering';
 		}
@@ -366,9 +367,12 @@
 		console.log('Initialized Checkout');
 	}
 
-	bindEvents() {
-		this.checkoutForm.addEventListener('submit', (e) => this.handleFormSubmit(e));
+	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);
 	}
@@ -382,12 +386,18 @@
 				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: {
@@ -401,7 +411,10 @@
 				borderRadius: '4px'
 			},
 			'.input-container.is-focus': {
-				borderColor: '#007cba'
+				borderColor: '#006AFF',
+				borderWidth: '2px',
+				outline: '2px solid #006AFF',
+				outlineOffset: '2px'
 			},
 			'.input-container.is-error': {
 				borderColor: '#d63638'
@@ -410,7 +423,7 @@
 	}
 
 	async handleFormSubmit(event) {
-		if (squareConfig.isOpen !== '1') {
+		if (!this.isOpen) {
 			return;
 		}
 		event.preventDefault();
@@ -435,23 +448,23 @@
 	}
 
 	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,
+			catalog_object_id: item.square_catalog_id,
+			quantity: String(item.quantity),
 			price: item.price,
-			name: item.name
+			note: item.note || ''
 		}));
 
 		const total = items.reduce((sum, item) =>
 			sum + (item.price * item.quantity), 0
 		);
 
+		// Pre-fill customer info if logged in
 		return {
-			total: total * 100, // Square expects amount in cents
+			total: Math.round(total * 100),
 			items: items,
 			customer: {
-				email: form.querySelector('[name="email"]')?.value || '',
+				email: this.isLoggedIn ? this.userEmail : (form.querySelector('[name="email"]')?.value || ''),
 				name: form.querySelector('[name="name"]')?.value || '',
 				phone: form.querySelector('[name="phone"]')?.value || ''
 			},
@@ -462,52 +475,101 @@
 
 	async processPayment(orderData) {
 		try {
-			const result = await this.card.tokenize();
+			let sourceToken = null;
 
-			if (result.status === 'OK') {
-				return await this.submitToServer(result.token, orderData);
+			// Check if using saved card or new card
+			if (this.selectedCardId) {
+				// Use saved card
+				sourceToken = this.selectedCardId;
 			} else {
-				throw new Error('Card tokenization failed: ' + (result.errors?.join(', ') || 'Unknown error'));
+				// 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(token, orderData) {
-		if (squareConfig.isOpen !== '1') {
-			return;
+	async submitToServer(sourceToken, orderData, isSavedCard = false) {
+		if (!this.isOpen) {
+			throw new Error('Store is currently closed');
 		}
 
-		// 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', {
+		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({
-				order_id: token.orderId,  // From Square SDK response
-				payment_id: token.paymentId,  // From Square SDK response
-				customer: orderData.customer,
+				source_id: sourceToken,
+				is_saved_card: isSavedCard,
+				cart_id: this.getCartId(),
+				amount: orderData.total,
 				items: orderData.items,
-				action: 'jvb_integration_action',
-				service: 'square',
-				integration_action: 'save_order'
+				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 || 'Failed to save order');
+			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();
@@ -545,43 +607,75 @@
 	/**************************************************************
 	 * 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 })
-		});
+	/**
+	 * 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 profile = await response.json();
+			const result = await response.json();
 
-		if (profile) {
-			this.displaySavedCards(profile.cards);
-			this.fillCustomerInfo(profile.customer);
+			if (result.success && result.cards) {
+				this.savedCards = result.cards;
+				this.renderSavedCards();
+			}
+		} catch (error) {
+			console.error('Failed to load saved cards:', error);
 		}
 	}
 
-	displaySavedCards(cards) {
+	/**
+	 * Render saved cards in the checkout form
+	 */
+	renderSavedCards() {
 		const container = document.getElementById('saved-cards');
-		if (!cards.length) return;
+		if (!container || this.savedCards.length === 0) {
+			return;
+		}
 
-		container.innerHTML = `
-            <h3>Saved Payment Methods</h3>
-            ${cards.map(card => `
-                <label>
-                    <input type="radio" name="payment_method" value="${card.id}">
-                    •••• ${card.last_4} (${card.card_brand})
+		const html = `
+            <div class="saved-cards-section">
+                <h4>Saved Payment Methods</h4>
+                ${this.savedCards.map(card => `
+                    <label class="saved-card">
+                        <input type="radio" name="payment-method" value="saved" data-card-id="${card.id}">
+                        <span class="card-info">
+                            <strong>${card.card_brand}</strong> ending in ${card.last_4}
+                            <small>Exp: ${card.exp_month}/${card.exp_year}</small>
+                        </span>
+                    </label>
+                `).join('')}
+                <label class="saved-card">
+                    <input type="radio" name="payment-method" value="new" checked>
+                    <span>Use a new card</span>
                 </label>
-            `).join('')}
-            <label>
-                <input type="radio" name="payment_method" value="new" checked>
-                Use new card
-            </label>
+            </div>
         `;
+
+		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', {

--
Gitblit v1.10.0