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