From df6c00db050e188a6bd5707e72c4f1f331ced923 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sun, 08 Feb 2026 20:46:43 +0000
Subject: [PATCH] =Port over to jakevan 2

---
 assets/js/concise/CheckoutSquare.js                  |  244 ++
 inc/rest/routes/NotificationsRoutes.php              |   18 
 assets/js/concise/CheckoutHelcim.js                  |  274 ++
 assets/js/min/square.min.js                          |    2 
 inc/rest/routes/TermRoutes.php                       |    6 
 inc/meta/Form.php                                    |   64 
 assets/js/min/form.min.js                            |    2 
 inc/rest/routes/ContentRoutes.php                    |    3 
 inc/rest/routes/FeedRoutes.php                       |    6 
 inc/rest/routes/OptionsRoutes.php                    |    3 
 assets/js/concise/Checkout.js                        |  542 ++++
 inc/rest/routes/ApprovalRoutes.php                   |    3 
 inc/ui/Checkout.php                                  |  285 ++
 inc/managers/_setup.php                              |    2 
 inc/rest/routes/ImporterRoutes.php                   |    9 
 JVBase.php                                           |    8 
 inc/rest/routes/IntegrationsSquareRoutes.php         |   12 
 inc/integrations/Square.php                          |  469 +--
 inc/ui/CRUDSkeleton.php                              |    6 
 inc/integrations/Integrations.php                    |    2 
 inc/importers/JaneAppSalesImporter.php               |    1 
 inc/rest/routes/SEORoutes.php                        |    6 
 inc/rest/routes/NewsRoutes.php                       |    6 
 inc/managers/queue/executors/IntegrationExecutor.php |  287 ++
 assets/js/concise/FormController.js                  |  217 
 inc/rest/routes/IntegrationsHelcimRoutes.php         |  187 +
 inc/rest/routes/FormRoutes.php                       |    6 
 assets/js/concise/PopulateForm.js                    |   57 
 inc/ui/_setup.php                                    |    1 
 assets/js/min/helcim.min.js                          |    1 
 inc/registry/FieldRegistry.php                       |   15 
 inc/rest/Route.php                                   |   24 
 inc/rest/routes/ErrorRoutes.php                      |    3 
 inc/rest/routes/FavouritesRoutes.php                 |    9 
 inc/rest/routes/ReferralRoutes.php                   |   18 
 inc/rest/routes/ContentTermsRoutes.php               |   23 
 inc/managers/queue/Storage.php                       |   15 
 inc/rest/_setup.php                                  |    6 
 inc/rest/routes/UploadRoutes.php                     |    6 
 assets/js/min/populate.min.js                        |    2 
 assets/js/min/auth.min.js                            |    2 
 inc/rest/routes/QueueRoutes.php                      |   18 
 inc/rest/routes/IntegrationsRoutes.php               |    6 
 inc/managers/AdminPages.php                          |  457 ---
 assets/js/admin.js                                   |  394 ++
 inc/managers/ScriptLoader.php                        |    2 
 inc/rest/routes/Invitations.php                      |    3 
 inc/rest/routes/SettingsRoutes.php                   |    3 
 webpack.jvb.js                                       |    6 
 inc/admin/Integrations.php                           |    6 
 /dev/null                                            |  498 ----
 assets/js/min/checkout.min.js                        |    1 
 inc/rest/routes/LoginRoutes.php                      |   23 
 inc/integrations/Helcim.php                          | 2634 +++++++++------------
 inc/rest/routes/AdminRoutes.php                      |  225 +
 inc/managers/queue/_setup.php                        |    1 
 inc/rest/routes/VoteRoutes.php                       |    3 
 57 files changed, 4,192 insertions(+), 2,940 deletions(-)

diff --git a/JVBase.php b/JVBase.php
index 6eba3a6..3de959a 100644
--- a/JVBase.php
+++ b/JVBase.php
@@ -23,6 +23,7 @@
 use JVBase\rest\routes\FeedRoutes;
 use JVBase\rest\routes\FavouritesRoutes;
 use JVBase\rest\routes\IntegrationsSquareRoutes;
+use JVBase\rest\routes\IntegrationsHelcimRoutes;
 use JVBase\rest\routes\NotificationsRoutes;
 use JVBase\rest\routes\ContentRoutes;
 use JVBase\rest\routes\TermRoutes;
@@ -44,7 +45,7 @@
 use JVBase\rest\routes\VoteRoutes;
 use JVBase\rest\routes\Invitations;
 use JVBase\rest\routes\ApprovalRoutes;
-//use JVBase\rest\routes\AdminRoutes;
+use JVBase\rest\routes\AdminRoutes;
 use JVBase\rest\routes\IntegrationsRoutes;
 use JVBase\utility\Features;
 
@@ -128,6 +129,9 @@
 		if (Features::hasIntegration('square')) {
 			$this->routes['square'] = new IntegrationsSquareRoutes();
 		}
+		if (Features::hasIntegration('helcim')) {
+			$this->routes['helcim'] = new IntegrationsHelcimRoutes();
+		}
 
 		if (Features::forSite()->has('feed_block')) {
 			$this->routes['feed'] = new FeedRoutes();
@@ -146,7 +150,7 @@
 
 		if (jvbSiteHasDashboard()) {
 			$this->routes['error'] = new ErrorRoutes();
-//            $this->routes['admin']  = new AdminRoutes();
+            $this->routes['admin']  = new AdminRoutes();
 			$this->routes['content'] = new ContentRoutes();
 //            $this->routes['bio']    = new BioRoutes();
 //			$this->routes['shop'] = new ShopRoutes();
diff --git a/assets/js/admin.js b/assets/js/admin.js
index 75bbd7f..7230dc8 100644
--- a/assets/js/admin.js
+++ b/assets/js/admin.js
@@ -1,102 +1,320 @@
 document.addEventListener("DOMContentLoaded", function(e) {
-	console.log('working');
-    // Tabs functionality for settings pages
-    let tabs = document.querySelectorAll('.jvb-settings-tab');
+	// Tabs functionality for settings pages
+	let tabs = document.querySelectorAll('.jvb-settings-tab');
 
-    tabs.forEach(tab => {
-        tab.addEventListener('click', (e) => {
-            removeActiveTab(tabs);
-            tab.classList.add('active');
-            setActiveSection(tab);
-        });
-    });
+	tabs.forEach(tab => {
+		tab.addEventListener('click', (e) => {
+			removeActiveTab(tabs);
+			tab.classList.add('active');
+			setActiveSection(tab);
+		});
+	});
 
-    // Check for hash in URL and activate corresponding tab
-    if (window.location.hash) {
-        var hash = window.location.hash.substring(1);
-        document.querySelector('.jvb-settings-tab[data-tab="' + hash + '"]')?.click();
-    } else {
-        // Activate first tab by default
-        document.querySelector('.jvb-settings-tab')?.click();
-    }
+	// Check for hash in URL and activate corresponding tab
+	if (window.location.hash) {
+		var hash = window.location.hash.substring(1);
+		document.querySelector('.jvb-settings-tab[data-tab="' + hash + '"]')?.click();
+	} else {
+		// Activate first tab by default
+		document.querySelector('.jvb-settings-tab')?.click();
+	}
 
-    let confirm = document.querySelector('.jvb-confirm-action');
-    if (confirm) {
-        confirm.addEventListener('click', (e) => {
-            if (!window.confirm(confirm.dataset.confirm || 'Are you sure?')) {
-                e.preventDefault();
-                return false;
-            }
-        });
-    }
+	let confirm = document.querySelector('.jvb-confirm-action');
+	if (confirm) {
+		confirm.addEventListener('click', (e) => {
+			if (!window.confirm(confirm.dataset.confirm || 'Are you sure?')) {
+				e.preventDefault();
+				return false;
+			}
+		});
+	}
 
-    // Admin action buttons
-    document.querySelectorAll('a[data-action]').forEach(action => {
-        action.addEventListener('click', (e) => {
-            e.preventDefault();
-            let loader = action.querySelector('.loader');
-            loader.classList.add('loading');
-
-            let a = action.dataset.action;
-
-            fetch(jvbSettings.api, {
-                method: 'POST',
-                headers: {
-                    'Content-Type': 'application/json',
-                    'X-WP-Nonce': jvbSettings.nonce,
-                    'action_nonce': jvbSettings.action
-                },
-                body: JSON.stringify({
-                    action: a
-                })
-            })
-                .then(response => {
-                    if (!response.ok) {
-                        throw new Error('Network response was not ok');
-                    }
-                    return response.json();
-                })
-                .then(data => {
-                    if (data.success === true) {
-                        loader.classList.remove('loading');
-                        loader.classList.add('loaded');
-                        setTimeout(() => {
-                            loader.classList.remove('loaded');
-                        }, 3000);
-                    } else {
-                        throw new Error(data.message || 'Action failed');
-                    }
-                })
-                .catch(error => {
-                    console.error('Error:', error);
-                    loader.classList.remove('loading');
-                    // You might want to add an error state class here
-                });
-        });
-    });
+	// Initialize admin actions
+	initAdminActions();
+	initCacheActions();
+	initIconActions();
 });
 
 function removeActiveTab(tabs) {
-    let active = document.querySelectorAll('.active');
-    active.forEach(tab => {
-        tab.classList.remove('active');
-        if (tab.dataset.tab) {
-            setActiveSection(tab, false);
-            window.location.hash = tab.dataset.tab;
-        }
-    });
+	let active = document.querySelectorAll('.active');
+	active.forEach(tab => {
+		tab.classList.remove('active');
+		if (tab.dataset.tab) {
+			setActiveSection(tab, false);
+			window.location.hash = tab.dataset.tab;
+		}
+	});
 }
 
 function setActiveSection(tab, set = true) {
-    let id = tab.dataset.tab;
-    if (!id) return;
+	let id = tab.dataset.tab;
+	if (!id) return;
 
-    let section = document.querySelector('#' + id);
-    if (!section) return;
+	let section = document.querySelector('#' + id);
+	if (!section) return;
 
-    if (set) {
-        section.classList.add('active');
-    } else {
-        section.classList.remove('active');
-    }
+	if (set) {
+		section.classList.add('active');
+	} else {
+		section.classList.remove('active');
+	}
+}
+
+/**
+ * Initialize admin action buttons (dashboard quick actions)
+ */
+function initAdminActions() {
+	document.querySelectorAll('a[data-action]').forEach(action => {
+		action.addEventListener('click', (e) => {
+			e.preventDefault();
+			let loader = action.querySelector('.loader');
+			loader.classList.add('loading');
+
+			let a = action.dataset.action;
+
+			makeAdminRequest('admin-action', { action: a })
+				.then(data => {
+					if (data.success === true) {
+						loader.classList.remove('loading');
+						loader.classList.add('loaded');
+						setTimeout(() => {
+							loader.classList.remove('loaded');
+						}, 3000);
+					} else {
+						throw new Error(data.message || 'Action failed');
+					}
+				})
+				.catch(error => {
+					console.error('Error:', error);
+					loader.classList.remove('loading');
+					alert('Error: ' + error.message);
+				});
+		});
+	});
+}
+
+/**
+ * Initialize cache management actions
+ */
+function initCacheActions() {
+	const flushAllBtn = document.querySelector('[data-cache-action="flush-all"]');
+	if (flushAllBtn) {
+		flushAllBtn.addEventListener('click', function() {
+			const originalText = this.innerHTML;
+			this.disabled = true;
+			this.innerHTML = 'Flushing...';
+
+			makeAdminRequest('admin-cache', { action: 'flush-all' })
+				.then(data => handleActionResponse(data, this, originalText))
+				.catch(error => handleActionError(error, this, originalText));
+		});
+	}
+
+	document.querySelectorAll('[data-cache-action="flush-cache"]').forEach(btn => {
+		btn.addEventListener('click', function() {
+			const group = this.getAttribute('data-group');
+			const originalText = this.innerHTML;
+			this.disabled = true;
+			this.innerHTML = 'Flushing...';
+
+			makeAdminRequest('admin-cache', { action: 'flush-cache', group: group })
+				.then(data => handleActionResponse(data, this, originalText))
+				.catch(error => handleActionError(error, this, originalText));
+		});
+	});
+}
+
+/**
+ * Initialize icon management actions
+ */
+function initIconActions() {
+	const currentSource = document.getElementById('icon-source-select')?.value || 'icons';
+
+	// Select all checkbox
+	const selectAll = document.getElementById('select-all-versions');
+	if (selectAll) {
+		selectAll.addEventListener('change', function() {
+			document.querySelectorAll('.version-checkbox').forEach(checkbox => {
+				checkbox.checked = this.checked;
+				checkbox.dispatchEvent(new Event('change'));
+			});
+		});
+	}
+
+	// Enable/disable merge button based on selection
+	document.querySelectorAll('.version-checkbox').forEach(checkbox => {
+		checkbox.addEventListener('change', updateMergeButtonState);
+	});
+
+	// Toggle icon list view
+	document.querySelectorAll('.view-icon-list-btn').forEach(btn => {
+		btn.addEventListener('click', function() {
+			const timestamp = this.getAttribute('data-timestamp');
+			const row = document.getElementById('icon-list-' + timestamp);
+			if (row) {
+				row.style.display = row.style.display === 'none' ? '' : 'none';
+				this.textContent = row.style.display === 'none' ? '(view)' : '(hide)';
+			}
+		});
+	});
+
+	// Force refresh button
+	const refreshBtn = document.querySelector('[data-icon-action="refresh-icons"]');
+	if (refreshBtn) {
+		refreshBtn.addEventListener('click', function() {
+			const originalText = this.innerHTML;
+			this.disabled = true;
+			this.innerHTML = 'Regenerating...';
+
+			const source = this.getAttribute('data-source') || currentSource;
+			makeAdminRequest('admin-icons', { action: 'refresh-icons', source: source })
+				.then(data => {
+					handleActionResponse(data, this, originalText);
+				})
+				.catch(error => {
+					handleActionError(error, this, originalText);
+				});
+
+		});
+	}
+
+	// Merge versions button
+	const mergeBtn = document.getElementById('merge-versions-btn');
+	if (mergeBtn) {
+		mergeBtn.addEventListener('click', function() {
+			const checkboxes = document.querySelectorAll('.version-checkbox:checked');
+			const timestamps = Array.from(checkboxes).map(cb => parseInt(cb.value));
+
+			if (timestamps.length < 2) {
+				alert('Please select at least 2 versions to merge');
+				return;
+			}
+
+			if (confirm(`Merge ${timestamps.length} versions? This will create a new CSS file with all unique icons.`)) {
+				const originalText = this.innerHTML;
+				this.disabled = true;
+				this.innerHTML = 'Merging...';
+
+				const source = this.getAttribute('data-source') || currentSource;
+				makeAdminRequest('admin-icons', {
+					action: 'merge-icon-versions',
+					source: source,
+					timestamps: timestamps
+				})
+					.then(data => {
+						handleActionResponse(data, this, originalText);
+					})
+					.catch(error => {
+						handleActionError(error, this, originalText);
+					});
+			}
+		});
+	}
+
+	// Restore version buttons
+	document.querySelectorAll('[data-icon-action="restore-icon-version"]').forEach(btn => {
+		btn.addEventListener('click', function() {
+			const timestamp = parseInt(this.getAttribute('data-timestamp'));
+			const source = this.getAttribute('data-source') || currentSource;
+
+			if (confirm('Restore this icon version? This will reload the page.')) {
+				const originalText = this.innerHTML;
+				this.disabled = true;
+				this.innerHTML = 'Restoring...';
+
+				makeAdminRequest('admin-icons', {
+					action: 'restore-icon-version',
+					source: source,
+					timestamp: timestamp
+				})
+					.then(data => {
+						handleActionResponse(data, this, originalText);
+					})
+					.catch(error => {
+						handleActionError(error, this, originalText);
+					});
+			}
+		});
+	});
+}
+
+/**
+ * Update merge button state based on selected checkboxes
+ */
+function updateMergeButtonState() {
+	const checkedCount = document.querySelectorAll('.version-checkbox:checked').length;
+	const mergeBtn = document.getElementById('merge-versions-btn');
+	if (mergeBtn) {
+		mergeBtn.disabled = checkedCount < 2;
+	}
+}
+
+/**
+ * Make an admin API request
+ * @param {string} endpoint - The API endpoint (without 'jvb/v1/')
+ * @param {object} data - The data to send
+ * @returns {Promise}
+ */
+function makeAdminRequest(endpoint, data = {}) {
+	if (typeof jvbSettings === 'undefined') {
+		console.error('jvbSettings is not defined');
+		return Promise.reject(new Error('jvbSettings is not defined. Scripts may not be loaded correctly.'));
+	}
+
+	console.log('Making request to:', jvbSettings.api + endpoint, 'with data:', data);
+
+	return fetch(jvbSettings.api + endpoint, {
+		method: 'POST',
+		headers: {
+			'Content-Type': 'application/json',
+			'X-WP-Nonce': jvbSettings.nonce,
+			'X-Action-Nonce': jvbSettings.action
+		},
+		body: JSON.stringify(data)
+	})
+		.then(response => {
+			console.log('Response status:', response.status);
+			if (!response.ok) {
+				return response.json().then(err => {
+					throw new Error(err.message || 'Network response was not ok');
+				});
+			}
+			return response.json();
+		})
+		.then(data => {
+			console.log('Response data:', data);
+			return data;
+		});
+}
+
+/**
+ * Handle successful action response
+ */
+function handleActionResponse(data, button = null, originalText = null) {
+	if (!data.success) {
+		throw new Error(data.message || 'Unknown error');
+	}
+
+	if (button && originalText) {
+		button.innerHTML = '✓ Success!';
+		setTimeout(() => {
+			button.disabled = false;
+			button.innerHTML = originalText;
+		}, 1500);
+	}
+}
+
+/**
+ * Handle action error
+ */
+function handleActionError(error, button = null, originalText = null) {
+	console.error('Error:', error);
+
+	if (button && originalText) {
+		button.innerHTML = '✗ ' + (error.message || 'Error');
+		setTimeout(() => {
+			button.disabled = false;
+			button.innerHTML = originalText;
+		}, 2000);
+	}
 }
diff --git a/assets/js/concise/Checkout.js b/assets/js/concise/Checkout.js
new file mode 100644
index 0000000..ad0b11e
--- /dev/null
+++ b/assets/js/concise/Checkout.js
@@ -0,0 +1,542 @@
+/**
+ * CartCheckout — Provider-agnostic cart & checkout base class
+ *
+ * Handles: cart add/remove/clear, quantity management, totals,
+ * saved cards UI, order tracking, localStorage persistence.
+ *
+ * Subclasses must implement:
+ *   - init()              → initialize the payment provider SDK
+ *   - processPayment()    → handle payment with provider
+ *   - submitToServer()    → send payment data to WP REST endpoint
+ *   - loadSavedCards()    → fetch saved cards from provider
+ *
+ * HTML structure expected: see Checkout.php (JVBase\ui\Checkout)
+ */
+class Checkout {
+	constructor(config = {}) {
+		this.config = config;
+
+		this.isInitialized = false;
+		this.cartItems     = new Map();
+		this.checkout      = document.querySelector('aside#cart');
+		this.provider      = this.checkout?.querySelector('form')?.dataset.provider || '';
+
+		this.isOpen        = this.config.isOpen !== '1' || false;
+		this.isLoggedIn    = this.config.is_logged_in || false;
+		this.userEmail     = this.config.user_email || '';
+		this.savedCards    = [];
+		this.selectedCardId = null;
+		this.cartId        = null;
+		this.stepMultiplier = 1;
+
+		this.cache = new window.jvbCache('cart', { TTL: 8.64e+7 });
+		this.a11y  = window.jvbA11y;
+
+		this.initCart();
+
+		if (this.checkout) {
+			this.initElements();
+			this.init();        // provider-specific
+			this.initListeners();
+
+			if (this.isLoggedIn) {
+				this.loadSavedCards();
+			}
+		}
+
+		this.popup = new window.jvbPopup({
+			popup:  this.checkout,
+			toggle: this.toggle,
+			name:   'Cart',
+			onOpen: this.maybeAddEmptyState.bind(this),
+		});
+	}
+
+	/*****************************************************************
+	 * ABSTRACT — subclasses must implement
+	 *****************************************************************/
+
+	/** Initialize provider SDK (Square payments, Helcim iframe, etc.) */
+	async init() {
+		throw new Error('init() must be implemented by subclass');
+	}
+
+	/** Process payment through provider and return result */
+	async processPayment(orderData) {
+		throw new Error('processPayment() must be implemented by subclass');
+	}
+
+	/** Submit payment token/result to server */
+	async submitToServer(tokenOrData, orderData) {
+		throw new Error('submitToServer() must be implemented by subclass');
+	}
+
+	/** Load saved cards from provider */
+	async loadSavedCards() {
+		// Default no-op; override if provider supports saved cards
+	}
+
+	/*****************************************************************
+	 * CART PERSISTENCE
+	 *****************************************************************/
+
+	async initCart() {
+		this.cartItems = await this.cache.get('cart') ?? new Map();
+		if (this.cartItems.size > 0) {
+			this.notifyRestoredCart();
+		}
+	}
+
+	saveCart() {
+		this.updateTotal();
+		this.cache.set('cart', this.cartItems);
+	}
+
+	clearCart() {
+		this.cartItems.clear();
+		window.removeChildren(this.table);
+		this.saveCart();
+	}
+
+	getCartId() {
+		if (!this.cartId) {
+			this.cartId = crypto.randomUUID();
+			this.cache.set('cart_id', this.cartId);
+		}
+		return this.cartId;
+	}
+
+	/*****************************************************************
+	 * ELEMENT SETUP
+	 *****************************************************************/
+
+	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
+		});
+	}
+
+	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);
+	}
+
+	/*****************************************************************
+	 * EVENT HANDLERS
+	 *****************************************************************/
+
+	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();
+		} else if (window.targetCheck(e, '[data-dismiss]')) {
+			window.targetCheck(e, '[data-dismiss]').closest('.restored')?.remove();
+		}
+	}
+
+	handleChange(e) {
+		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);
+			}
+		}
+	}
+
+	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);
+		}
+	}
+
+	/*****************************************************************
+	 * CART ITEM MANAGEMENT
+	 *****************************************************************/
+
+	handleAddToCart(item) {
+		let id       = item.dataset.id;
+		let price    = parseFloat(item.dataset.price);
+		let quantity = parseInt(item.querySelector('.quantity-input')?.value) ?? 1;
+		let total    = parseFloat(price * quantity);
+
+		this.createItemElement(item);
+
+		this.cartItems.set(id, {
+			post_id:    id,
+			name:       item.dataset.name,
+			price:      price,
+			quantity:   quantity,
+			total:      total,
+			catalog_id: item.dataset.catalogId || '',
+		});
+		this.saveCart();
+	}
+
+	handleRemoveFromCart(item) {
+		if (confirm('This will remove this item from the cart. Continue?')) {
+			if (!item.querySelector('[data-id]')) {
+				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();
+
+			let input = document.querySelector(`[data-id="${id}"] input`);
+			if (input) input.value = 0;
+
+			this.maybeAddEmptyState();
+			this.saveCart();
+		}
+	}
+
+	handleNumberClick(e, container) {
+		e.preventDefault();
+		let change = 0;
+		if (e.target.closest('.increase'))      change += 1;
+		else if (e.target.closest('.decrease'))  change -= 1;
+
+		if (change !== 0) {
+			let step  = parseInt(container.dataset.step);
+			let input = 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      = container.dataset.min;
+		let max      = container.dataset.max;
+		let input    = container.querySelector('input');
+		let increase = container.querySelector('.increase');
+		let decrease = 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 = true;
+		} else {
+			increase.disabled = false;
+			decrease.disabled = false;
+		}
+	}
+
+	/*****************************************************************
+	 * ITEM ELEMENT CREATION
+	 *****************************************************************/
+
+	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.catalogId,
+			] = [
+				item.dataset.id,
+				item.dataset.name,
+				window.formatPrice(price),
+				price,
+				item.dataset.catalogId || '',
+			];
+		} 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);
+		}
+	}
+
+	/*****************************************************************
+	 * CART STATE
+	 *****************************************************************/
+
+	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 emptyEl = window.getTemplate('emptyCart');
+			this.itemsList.append(emptyEl);
+			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';
+		}
+	}
+
+	notifyRestoredCart() {
+		let restored = window.getTemplate('restoredCart');
+		this.checkout.querySelector('.tab-content[data-tab=cartItems]').insertBefore(restored, this.itemsList);
+
+		this.cartItems.forEach(item => {
+			let 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.catalogId,
+				element.querySelector('[name="quantity"]').value,
+				element.querySelector('.total').textContent,
+			] = [
+				item.post_id,
+				item.name,
+				window.formatPrice(item.price),
+				item.price,
+				item.catalog_id || '',
+				item.quantity,
+				window.formatPrice(item.quantity * item.price),
+			];
+			this.table.append(element);
+		});
+		this.updateTotal();
+	}
+
+	/*****************************************************************
+	 * TOTALS
+	 *****************************************************************/
+
+	updateTotal() {
+		let total = 0;
+		this.cartItems.forEach(item => total += item.total);
+		let tax = total * 0.05;
+		window.eraseText(this.totalTax);
+		window.eraseText(this.grandTotal);
+		window.typeText(this.totalTax, window.formatPrice(tax));
+		window.typeText(this.grandTotal, window.formatPrice(total + tax));
+		this.totalTax.classList.remove('typeText');
+	}
+
+	/*****************************************************************
+	 * FORM SUBMIT
+	 *****************************************************************/
+
+	extractOrderData(form) {
+		const items = Array.from(this.cartItems.values()).map(item => ({
+			catalog_id: item.catalog_id,
+			quantity:   String(item.quantity),
+			price:      item.price,
+			note:       item.note || '',
+		}));
+
+		const total = items.reduce((sum, item) =>
+			sum + (item.price * item.quantity), 0
+		);
+
+		return {
+			total:    Math.round(total * 100),
+			items:    items,
+			customer: {
+				email: this.isLoggedIn ? this.userEmail : (form.querySelector('[name="cart_email"]')?.value || ''),
+				name:  form.querySelector('[name="cart_name"]')?.value || '',
+				phone: form.querySelector('[name="cart_phone"]')?.value || '',
+			},
+			note:        form.querySelector('[name="special_instructions"]')?.value || '',
+			pickup_time: form.querySelector('[name="pickup_time"]')?.value || '',
+		};
+	}
+
+	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?.();
+		}
+	}
+
+	/*****************************************************************
+	 * ORDER TRACKING
+	 *****************************************************************/
+
+	trackOrder(orderNum) {
+		this.orderId = orderNum;
+		this.scheduleOrderCheck();
+		this.checkout.querySelector('button[data-tab=order]').hidden = false;
+	}
+
+	scheduleOrderCheck() {
+		window.debouncer.schedule(
+			'order',
+			() => this.checkOrderStatus(),
+			30000
+		);
+	}
+
+	async checkOrderStatus() {
+		const response = await fetch(`${this.config.api_url}order-status/${this.orderId}`, {
+			headers: { 'X-WP-Nonce': this.config.nonce }
+		});
+		const data = await response.json();
+		if (data.status !== 'ready') {
+			this.scheduleOrderCheck();
+		}
+		this.updateOrderStatus(data);
+	}
+
+	updateOrderStatus(data) {
+		this.checkout.querySelectorAll('.status-item').forEach(item => {
+			if (item.dataset.status === data.status) {
+				item.classList.add('active');
+			}
+		});
+		this.checkout.querySelector('#eta').textContent = data.eta || 'In progress';
+	}
+
+	/*****************************************************************
+	 * SAVED CARDS — shared rendering, provider fetches cards
+	 *****************************************************************/
+
+	renderSavedCards() {
+		const container = document.getElementById('saved-cards');
+		if (!container || this.savedCards.length === 0) return;
+
+		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>
+			</div>
+		`;
+
+		container.innerHTML = html;
+
+		container.querySelectorAll('input[name="payment-method"]').forEach(radio => {
+			radio.addEventListener('change', (e) => {
+				const useNewCard     = e.target.value === 'new';
+				const paymentContainer = document.getElementById('payment-container');
+				if (paymentContainer) {
+					paymentContainer.style.display = useNewCard ? 'block' : 'none';
+				}
+				this.selectedCardId = useNewCard ? null : e.target.dataset.cardId;
+			});
+		});
+	}
+
+	/*****************************************************************
+	 * RESULT HANDLERS
+	 *****************************************************************/
+
+	handleSuccess(result, form) {
+		document.dispatchEvent(new CustomEvent('checkoutSuccess', {
+			detail: { result, form, provider: this.provider }
+		}));
+
+		const successUrl = form.dataset.successUrl ||
+			`/order-confirmation/?order=${result.order_id || result.wp_order_id}`;
+		window.location.href = successUrl;
+	}
+
+	handleError(error) {
+		console.error(`${this.provider} checkout error:`, error);
+
+		document.dispatchEvent(new CustomEvent('checkoutError', {
+			detail: { error, provider: this.provider }
+		}));
+
+		window.jvbNotifications?.show?.(
+			error.message || error || 'Payment failed',
+			'error'
+		);
+	}
+}
+
+window.jvbCheckout = Checkout;
diff --git a/assets/js/concise/CheckoutHelcim.js b/assets/js/concise/CheckoutHelcim.js
new file mode 100644
index 0000000..1cfd2c3
--- /dev/null
+++ b/assets/js/concise/CheckoutHelcim.js
@@ -0,0 +1,274 @@
+/**
+ * HelcimCheckout — extends CartCheckout for HelcimPay.js payments
+ *
+ * Payment flow:
+ *   1. User clicks checkout → extractOrderData()
+ *   2. Server call to /helcim/initialize-checkout → returns checkoutToken
+ *   3. Call appendHelcimPayIframe(checkoutToken) → Helcim renders modal
+ *   4. Listen for window 'message' event → SUCCESS / CANCELLED / ERROR
+ *   5. On SUCCESS, validate transaction server-side
+ *
+ * @see https://devdocs.helcim.com/docs/helcim-pay-js
+ */
+class CheckoutHelcim extends window.jvbCheckout {
+	constructor(config = {}) {
+		super({
+			...window.helcimConfig,
+			...config,
+		});
+		this.pendingSecretToken = null;
+	}
+
+	/*****************************************************************
+	 * INIT — HelcimPay.js SDK (loaded externally)
+	 *****************************************************************/
+
+	async init() {
+		// HelcimPay.js is loaded via <script> tag, no SDK init needed.
+		// We just need the global appendHelcimPayIframe function.
+		if (typeof window.appendHelcimPayIframe !== 'function') {
+			console.warn('HelcimPay.js SDK not loaded — payment will initialize on first checkout');
+		}
+
+		this.isInitialized = true;
+
+		// Listen for HelcimPay.js message events
+		window.addEventListener('message', (e) => this.handleHelcimMessage(e));
+
+		document.dispatchEvent(new CustomEvent('checkoutReady', {
+			detail: { checkout: this, provider: 'helcim' }
+		}));
+	}
+
+	/*****************************************************************
+	 * PAYMENT FLOW
+	 *****************************************************************/
+
+	async processPayment(orderData) {
+		// If using a saved card, process server-side directly
+		if (this.selectedCardId) {
+			return this.submitToServer({
+				card_id:   this.selectedCardId,
+				is_saved:  true,
+			}, orderData);
+		}
+
+		// Otherwise, initialize HelcimPay.js checkout
+		const session = await this.initializeCheckoutSession(orderData);
+		if (!session.success) {
+			throw new Error(session.message || 'Failed to initialize checkout');
+		}
+
+		// Store secretToken for server-side validation after payment
+		this.pendingSecretToken = session.secretToken;
+		this.pendingOrderData   = orderData;
+
+		// Open HelcimPay.js iframe modal
+		window.appendHelcimPayIframe(session.checkoutToken, {
+			type: 'modal', // 'modal' or 'inline'
+		});
+
+		// The flow continues in handleHelcimMessage() when the iframe posts back
+		// Return a promise that resolves when payment completes
+		return new Promise((resolve, reject) => {
+			this._paymentResolve = resolve;
+			this._paymentReject  = reject;
+		});
+	}
+
+	/**
+	 * Server call: initialize a HelcimPay.js checkout session
+	 */
+	async initializeCheckoutSession(orderData) {
+		const response = await fetch(this.config.api_url + 'initialize-checkout', {
+			method:  'POST',
+			headers: {
+				'Content-Type': 'application/json',
+				'X-WP-Nonce':   this.config.nonce,
+			},
+			body: JSON.stringify({
+				amount:      orderData.total / 100, // Convert cents back to dollars
+				customer:    orderData.customer,
+				items:       orderData.items,
+				cart_id:     this.getCartId(),
+			}),
+		});
+
+		return response.json();
+	}
+
+	/**
+	 * Handle postMessage events from HelcimPay.js iframe
+	 */
+	handleHelcimMessage(event) {
+		const data = event.data;
+
+		// HelcimPay.js sends messages with specific event types
+		if (!data || typeof data !== 'object') return;
+
+		// Helcim sends eventStatus: 'ABORTED' | 'SUCCESS' | 'FAILED'
+		if (data.eventStatus === 'SUCCESS') {
+			this.handleHelcimSuccess(data);
+		} else if (data.eventStatus === 'ABORTED') {
+			this.handleHelcimCancelled();
+		} else if (data.eventStatus === 'FAILED') {
+			this.handleHelcimError(data);
+		}
+	}
+
+	async handleHelcimSuccess(data) {
+		try {
+			// Validate the transaction server-side using secretToken
+			const result = await this.submitToServer({
+				transaction_id: data.transactionId,
+				secret_token:   this.pendingSecretToken,
+				event_data:     data,
+			}, this.pendingOrderData);
+
+			this.clearPending();
+			this._paymentResolve?.(result);
+		} catch (error) {
+			this.clearPending();
+			this._paymentReject?.(error);
+		}
+	}
+
+	handleHelcimCancelled() {
+		this.clearPending();
+		window.jvbLoading?.hideLoading?.();
+		this.a11y.announce('Payment cancelled');
+		this._paymentReject?.(new Error('Payment cancelled by user'));
+	}
+
+	handleHelcimError(data) {
+		this.clearPending();
+		window.jvbLoading?.hideLoading?.();
+		const message = data.errorMessage || 'Payment failed';
+		this._paymentReject?.(new Error(message));
+	}
+
+	clearPending() {
+		this.pendingSecretToken = null;
+		this.pendingOrderData   = null;
+	}
+
+	/*****************************************************************
+	 * SERVER COMMUNICATION
+	 *****************************************************************/
+
+	async submitToServer(paymentData, orderData) {
+		if (!this.isOpen) {
+			throw new Error('Store is currently closed');
+		}
+
+		const endpoint = paymentData.is_saved
+			? 'process-saved-payment'
+			: 'validate-transaction';
+
+		const response = await fetch(this.config.api_url + endpoint, {
+			method:  'POST',
+			headers: {
+				'Content-Type': 'application/json',
+				'X-WP-Nonce':   this.config.nonce,
+			},
+			body: JSON.stringify({
+				...paymentData,
+				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;
+	}
+
+	/*****************************************************************
+	 * SAVED CARDS
+	 *****************************************************************/
+
+	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);
+		}
+	}
+
+	/*****************************************************************
+	 * INVOICES — Helcim-specific (source of truth is Helcim)
+	 *****************************************************************/
+
+	async loadInvoices() {
+		try {
+			const response = await fetch(this.config.api_url + 'invoices', {
+				headers: { 'X-WP-Nonce': this.config.nonce },
+			});
+			const result = await response.json();
+			if (result.success) {
+				return result.invoices || [];
+			}
+		} catch (error) {
+			console.error('Failed to load invoices:', error);
+		}
+		return [];
+	}
+
+	async payInvoice(invoiceId) {
+		const session = await fetch(this.config.api_url + 'initialize-checkout', {
+			method: 'POST',
+			headers: {
+				'Content-Type': 'application/json',
+				'X-WP-Nonce':   this.config.nonce,
+			},
+			body: JSON.stringify({
+				invoice_id: invoiceId,
+			}),
+		}).then(r => r.json());
+
+		if (!session.success) {
+			throw new Error(session.message || 'Failed to initialize invoice payment');
+		}
+
+		this.pendingSecretToken = session.secretToken;
+		this.pendingOrderData   = { total: 0, items: [], customer: {} };
+
+		window.appendHelcimPayIframe(session.checkoutToken, { type: 'modal' });
+
+		return new Promise((resolve, reject) => {
+			this._paymentResolve = resolve;
+			this._paymentReject  = reject;
+		});
+	}
+}
+
+document.addEventListener('DOMContentLoaded', () => {
+	// Only init if Helcim is the active provider
+	const form = document.querySelector('#checkout[data-provider="helcim"]');
+	if (form) {
+		window.jvbHelcim = new CheckoutHelcim();
+	}
+});
diff --git a/assets/js/concise/CheckoutSquare.js b/assets/js/concise/CheckoutSquare.js
new file mode 100644
index 0000000..f1fb448
--- /dev/null
+++ b/assets/js/concise/CheckoutSquare.js
@@ -0,0 +1,244 @@
+/**
+ * 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();
+	}
+});
diff --git a/assets/js/concise/FormController.js b/assets/js/concise/FormController.js
index 1286355..8996cbf 100644
--- a/assets/js/concise/FormController.js
+++ b/assets/js/concise/FormController.js
@@ -59,7 +59,7 @@
 				field: '.field',					//querySelectorAll
 				label: 'label',
 				success: '.success',
-				error: '.success',
+				error: '.error',
 				message: '.validation-message',
 			},
 			repeater: {
@@ -78,6 +78,7 @@
 				remove: '.remove-tag',
 				label: '.tag-label',
 				items: '.tag-items',
+				item: '.tag-item',
 				inputs: this.inputSelectors,				//querySelectorAll
 				value: 'input[type="hidden"]'		//querySelectorAll
 			},
@@ -334,7 +335,7 @@
 			});
 		}
 
-		if (Object.hasOwn(field.dataset, 'repeater-id') || Object.hasOwn(field.dataset,'tag-list-id')) {
+		if (field.dataset.fieldType === 'repeater' || field.dataset.fieldType === 'tag-list') {
 			this.updateCollectionField(field);
 			return;
 		}
@@ -763,14 +764,14 @@
 					let conf = this.quantityFields.get(e.target.closest('[data-num-id]')?.dataset.numId);
 					if(!conf) return;
 					let change = 0;
-					if (conf.increase.contains(e.target)) {
+					if (conf.ui.increase.contains(e.target)) {
 						change++;
-					} else if (conf.decrease.contains(e.target)) {
+					} else if (conf.ui.decrease.contains(e.target)) {
 						change--;
 					}
 					if (change === 0) return;
 					let field = this.getField(e.target);
-					let step = conf.input.step;
+					let step = conf.ui.input.step;
 					step = Math.max(step, 1);
 					if (e.ctrlKey && e.shiftKey) {
 						step = step * 50;
@@ -779,20 +780,20 @@
 					} else if (e.shiftKey) {
 						step = step * 10;
 					}
-					let value = (conf.input.value === '') ? 0 : parseFloat(conf.input.value);
-					conf.input.value = (value + (step * change));
+					let value = (conf.ui.input.value === '') ? 0 : parseFloat(conf.ui.input.value);
+					conf.ui.input.value = (value + (step * change));
 
-					value = parseFloat(conf.input.value);
+					value = parseFloat(conf.ui.input.value);
 
-					if (conf.input.min && value < conf.input.min) {
-						conf.input.value = conf.input.min;
-						conf.decrease.disabled = true;
-					} else if (conf.input.max && value > conf.input.max) {
-						conf.input.value = conf.input.max;
-						conf.increase.disabled = true;
+					if (conf.ui.input.min && value < conf.ui.input.min) {
+						conf.ui.input.value = conf.ui.input.min;
+						conf.ui.decrease.disabled = true;
+					} else if (conf.ui.input.max && value > conf.ui.input.max) {
+						conf.ui.input.value = conf.ui.input.max;
+						conf.ui.increase.disabled = true;
 					} else {
-						if (conf.decrease.disabled) conf.decrease.disabled = false;
-						if (conf.increase.disabled) conf.increase.disabled = false;
+						if (conf.ui.decrease.disabled) conf.ui.decrease.disabled = false;
+						if (conf.ui.increase.disabled) conf.ui.increase.disabled = false;
 					}
 				}
 			checkForRepeaters(form) {
@@ -824,7 +825,7 @@
 
 
 								manyRefs.inputs?.forEach(input => {
-									window.prefixInput(input, `${data.repeater.dataset.fieldName}:${index}:`, el);
+									window.prefixInput(input, `${data.repeater.dataset.field}:${index}:`, el);
 								});
 							}
 						},
@@ -853,10 +854,8 @@
 				}
 				handleRepeaterClick(e) {
 					if (e.target.matches(this.selectors.repeater.add)) {
-						console.log('Add Repeater Row');
 						this.addRepeaterRow(e.target.closest('[data-repeater-id]'));
 					} else if (e.target.matches(this.selectors.repeater.remove)) {
-						console.log('Remove Repeater Row');
 						this.removeRepeaterRow(e.target.closest('[data-index]'));
 					}
 				}
@@ -882,10 +881,10 @@
 						form: form.dataset.formId,
 						format: field.dataset.tagFormat??'first_field'
 					};
-					console.log('Registering Tag List with config', config);
 					if (!config.ui.input || !config.ui.add || !config.ui.items) return;
 
 					field.dataset.tagListId = config.id;
+					config.fieldName = field.dataset.field;
 
 					let template = field.querySelector('template');
 					this.templates.define(
@@ -902,7 +901,7 @@
 								el.dataset.index = index;
 								manyRefs.inputs?.forEach(input => {
 									let wrapper = input.closest('.tag-item');
-									window.prefixInput(input, `${el.dataset.fieldName}:${index}:`, wrapper)
+									window.prefixInput(input, `${data.fieldName}:${index}:`, wrapper)
 								});
 
 								if (refs.label) {
@@ -914,7 +913,6 @@
 					config.ui.inputs = Array.from(field.querySelectorAll(this.selectors.tagList.inputs));
 					config.ui.value = Array.from(field.querySelectorAll(this.selectors.tagList.value));
 					this.tagLists.set(config.id, config);
-					console.log('Adding tag list listeners to ', field);
 					this.addTagListListeners(field);
 				});
 
@@ -931,82 +929,114 @@
 				handleTagListClick(e) {
 					if (window.targetCheck(e,this.selectors.tagList.add)) {
 						this.addTagListItem(e.target.closest('[data-tag-list-id]'));
-					} else if (e.target.matches(this.selectors.tagList.remove)) {
-						this.removeTagListItem(e.target.closest(this.selectors.tagList.remove));
+					} else if (window.targetCheck(e, this.selectors.tagList.remove)) {
+						this.removeTagListItem(e.target.closest(this.selectors.tagList.item));
 					}
 				}
-					addTagListItem(tagList) {
-						let config = this.tagLists.get(tagList.dataset.tagListId);
-						if (!config) return;
+			addTagListItem(tagList) {
+				let config = this.tagLists.get(tagList.dataset.tagListId);
+				if (!config) return;
 
-						let data = {};
-						let hasValue = false;
-						for (let input of config.ui.inputs) {
-							this.validateField(input);
-							const fieldName = input.name.replace('new_','');
-							const value = this.getFieldValue(input);
-							if (value) hasValue = true;
-							data[fieldName] = value;
+				let data = {};
+				let hasValue = false;
+				let isValid = true;
 
-							//clear values and validation
-							if (['checkbox', 'radio'].includes(input.type)) {
-								input.checked = false;
-							} else {
-								input.value = '';
+				// First pass: validate all inputs
+				for (let input of config.ui.inputs) {
+					const isRequired = input.required || input.dataset.required === 'true';
+					const value = this.getFieldValue(input);
+
+					if (value) hasValue = true;
+
+					// Validate and check for errors
+					const valid = this.validateField(input);
+
+					if (isRequired && !value) {
+						this.showError(input, 'This field is required');
+						isValid = false;
+					} else if (!valid) {
+						isValid = false;
+					}
+
+					const fieldName = input.name.replace('new_','');
+					data[fieldName] = value;
+				}
+
+				// Stop if validation failed
+				if (!isValid) {
+					this.a11y.announce('Please correct the errors before adding');
+					const firstInvalid = config.ui.inputs.find(input => {
+						const isRequired = input.required || input.dataset.required === 'true';
+						return (isRequired && !this.getFieldValue(input));
+					});
+					if (firstInvalid) firstInvalid.focus();
+					return;
+				}
+
+				if (!hasValue) {
+					this.a11y.announce('Please fill in at least one field');
+					config.ui.inputs[0].focus();
+					return;
+				}
+
+				// Build label
+				let label;
+				switch (config.format) {
+					case 'first_field':
+						label = Object.values(data)[0];
+						break;
+					case 'all_fields':
+						label = Object.values(data).join(', ');
+						break;
+					default:
+						if (config.format.includes('{')) {
+							label = config.format;
+							for (const [key, value] of Object.entries(data)) {
+								label = label.replace(`{${key}}`, value);
 							}
-							this.clearValidation(input);
+						} else {
+							label = data[config.format]??Object.values(data)[0];
 						}
+						break;
+				}
 
-						if (!hasValue) {
-							this.a11y.announce('Please fill in at least one field');
-							config.ui.inputs[0].focus();
-							return;
-						}
+				let newItem = this.templates.create(tagList.dataset.tagListId, {
+					label: label,
+					fieldName: config.fieldName
+				});
 
-						let label;
-						switch (config.format) {
-							case 'first_field':
-								label = Object.values(data)[0];
-								break;
-							case 'all_fields':
-								label = Object.values(data).join(', ');
-								break;
-							default:
-								if (config.format.includes('{')) {
-									let label = config.format;
-									for (const [key, value] of Object.entries(data)) {
-										label = label.replace(`{${key}}`, value);
-									}
-								} else {
-									label = data[config.format]??Object.values(data)[0];
-								}
-								break;
-						}
+				const index = config.ui.items?.children?.length ?? 0;
+				newItem?.querySelectorAll('input[type=hidden]')?.forEach(input => {
+					const fieldKey = input.dataset.field;
+					input.name = `${config.fieldName}:${index}:${fieldKey}`;
+					input.id = `${config.fieldName}:${index}:${fieldKey}`;
+					input.value = data[fieldKey] || '';
+				});
 
-						let newItem = this.templates.create(tagList.dataset.tagListId, {
-							label: label
-						});
+				config.ui.items.append(newItem);
 
-						const index = config.ui.items?.children?.length ?? 0;
-						newItem?.querySelectorAll('input[type=hidden]')?.forEach(input => {
-							const fieldKey = input.dataset.field;
-							input.name = `${config.element.field}:${index}:${fieldKey}`;
-							input.value = data[fieldKey] || '';
-						});
-
-						config.ui.items.append(newItem);
-						config.ui.inputs[0]?.focus();
-
-						this.updateCollectionField(tagList);
-
-						this.a11y.announce('Item added');
+				// Clear inputs AFTER success
+				for (let input of config.ui.inputs) {
+					if (['checkbox', 'radio'].includes(input.type)) {
+						input.checked = false;
+					} else {
+						input.value = '';
 					}
-					removeTagListItem(tag) {
-						let tagList = tag.closest('[data-tag-list-id]');
-						tag.remove();
-						this.reindexList(tagList);
-						this.a11y.announce('Item removed');
-					}
+					this.clearValidation(input);
+				}
+
+				config.ui.inputs[0]?.focus();
+				this.updateCollectionField(tagList);
+				this.a11y.announce('Item added');
+			}
+				removeTagListItem(item) {
+					let tagList = item.closest('[data-tag-list-id]');
+					if (!tagList) return;
+					item.remove();
+					this.reindexList(tagList);
+					this.updateCollectionField(tagList);
+					this.a11y.announce('Item removed');
+				}
 				handleTagListInput(e) {
 					let target = e.target;
 					let field = target.closest('[data-tag-list-id]');
@@ -1397,28 +1427,20 @@
 		if (data.field) {
 			const fieldWrapper = form.querySelector(`[data-field="${data.field}"]`);
 			if (fieldWrapper) {
-				// Use existing showError method for consistency
 				this.showError(fieldWrapper, data.message);
 
-				// Mark as touched so validation persists
-				this.touchedFields.add(data.field);
-
-				// Scroll to error
 				fieldWrapper.scrollIntoView({ behavior: 'smooth', block: 'center' });
 
-				// Focus the input for better UX
 				const input = fieldWrapper.querySelector('input, textarea, select');
 				if (input) {
 					input.focus();
 				}
 			}
 		} else {
-			// General form error (not field-specific)
 			const error = document.createElement('div');
 			error.className = 'form-error error-message';
 			error.textContent = data.message;
 
-			// Add icon for consistency
 			const icon = window.getIcon?.('close-circle');
 			if (icon) {
 				icon.classList.add('error-icon');
@@ -1426,12 +1448,9 @@
 			}
 
 			form.insertBefore(error, form.firstChild);
-
-			// Scroll to top to show the error
 			form.scrollIntoView({ behavior: 'smooth', block: 'start' });
 		}
 
-		// Announce error for accessibility
 		if (window.jvbA11y) {
 			const announcement = data.field
 				? `Error in ${data.field}: ${data.message}`
@@ -1439,7 +1458,6 @@
 			window.jvbA11y.announce(announcement);
 		}
 
-		// Trigger custom event
 		form.dispatchEvent(new CustomEvent('jvb-form-error', {
 			detail: data
 		}));
@@ -1502,6 +1520,7 @@
 	**********************************************************************/
 	getForm(element) {
 		let form = element.closest('[data-form-id]');
+		if (!form) return false;
 		let id = form.dataset.formId;
 		if (!id) return false;
 		let config = this.forms.get(id);
diff --git a/assets/js/concise/PopulateForm.js b/assets/js/concise/PopulateForm.js
index 64b3f65..2bd02b8 100644
--- a/assets/js/concise/PopulateForm.js
+++ b/assets/js/concise/PopulateForm.js
@@ -91,7 +91,7 @@
 		if (!value || !Array.isArray(value)) return;
 
 		const container = field.querySelector('.repeater-items');
-		let template = field.querySelector('template')?.className??false;
+		let template = field.querySelector('template')?.className ?? false;
 		if (!container || !template) return;
 
 		window.removeChildren(container);
@@ -99,8 +99,16 @@
 		value.forEach((data, index) => {
 			data.index = index;
 			const row = this.templates.create(template, data);
-			let fields = row.querySelectorAll('.field');
-			this.populate(fields, data);
+			if (!row) return;
+
+			for (let [fieldName, fieldValue] of Object.entries(data)) {
+				if (fieldName === 'index') continue;
+				let subField = row.querySelector(`[data-field="${fieldName}"]`);
+				if (subField) {
+					this.populateField(subField, fieldName, fieldValue);
+				}
+			}
+
 			container.append(row);
 		});
 	}
@@ -108,19 +116,52 @@
 		if (!value || !Array.isArray(value)) return;
 
 		const container = field.querySelector('.tag-items');
-		let template = field.querySelector('template')?.className??false;
+		let template = field.querySelector('template')?.className ?? false;
 		if (!container || !template) return;
 
 		window.removeChildren(container);
 
 		value.forEach((data, index) => {
-			data.index = index;
-			const row = this.templates.create(template, data);
-			let fields = row.querySelectorAll('.field');
-			this.populate(fields, data);
+			const row = this.templates.create(template, {
+				label: this.getTagLabel(data, field.dataset.tagFormat ?? 'first_field'),
+				fieldName: name,
+				...data
+			});
+			if (!row) return;
+
+			// Set hidden input values directly
+			row.querySelectorAll('input[type="hidden"]').forEach(input => {
+				const key = input.dataset.field;
+				if (key && data[key] !== undefined) {
+					input.value = data[key];
+				}
+			});
+
 			container.append(row);
 		});
 	}
+	/**
+	 * Build tag label from data - mirrors addTagListItem logic
+	 */
+	getTagLabel(data, format) {
+		const values = Object.values(data).filter(v => !this.isEmptyValue(v));
+		switch (format) {
+			case 'first_field':
+				return values[0] ?? 'New Item';
+			case 'all_fields':
+				return values.join(', ') || 'New Item';
+			default:
+				if (format.includes('{')) {
+					let label = format;
+					for (const [key, value] of Object.entries(data)) {
+						label = label.replace(`{${key}}`, value);
+					}
+					return label;
+				}
+				return data[format] ?? values[0] ?? 'New Item';
+		}
+	}
+
 	populateLocation(field, name, value) {
 		const subFields = ['address', 'lat', 'lng', 'street', 'city', 'province', 'postal_code', 'country'];
 		subFields.forEach(subField => {
diff --git a/assets/js/concise/SquareCheckout.js b/assets/js/concise/SquareCheckout.js
deleted file mode 100644
index 72d033e..0000000
--- a/assets/js/concise/SquareCheckout.js
+++ /dev/null
@@ -1,739 +0,0 @@
-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 = `
-            <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>
-            </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', {
-			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);
-// 		}
-// 	});
-// });
diff --git a/assets/js/min/auth.min.js b/assets/js/min/auth.min.js
index 4c588df..9317888 100644
--- a/assets/js/min/auth.min.js
+++ b/assets/js/min/auth.min.js
@@ -1 +1 @@
-window.auth=new class{constructor(){this.initialized=!1,this.isAuthenticating=!1,this.authenticated=!1,this.user=!1,this.nonces={},this.subscribers=new Set,this.storageKey="jvb_auth_state",this.cacheMetaKey="jvb_auth_meta",this.cacheExpiry=3e5,this.init()}async init(){if(this.isAuthenticating)return new Promise((t=>{const e=setInterval((()=>{this.initialized&&(clearInterval(e),t())}),50)}));this.isAuthenticating=!0;try{const t=this.getCachedAuth();if(t)return this.setAuthData(t),this.initialized=!0,this.isAuthenticating=!1,void this.notify("auth-loaded",{fromCache:!0});await this.fetchAuth()}catch(t){console.error("Failed to initialize auth:",t),this.clearAuthData(),this.initialized=!0,this.isAuthenticating=!1,this.notify("auth-error",{error:t})}}async refreshNonce(t="wp_rest"){try{return await this.fetchAuth(),this.getNonce(t)}catch(t){return console.error("Failed to refresh nonce:",t),null}}async fetch(t,e={}){const i=async(s=0)=>{const a={"Content-Type":"application/json",...e.headers,"X-WP-Nonce":this.getNonce()},h=await fetch(t,{...e,credentials:"same-origin",headers:a});if((403===h.status||401===h.status)&&0===s){const t=await h.clone().json();if("rest_cookie_invalid_nonce"===t.code||t.message?.includes("Cookie check"))return console.log("Nonce invalid, refreshing auth..."),await this.refresh(),i(1)}return h};return i()}async fetchAuth(){const t=await fetch(`${jvbSettings.api}auth/status`,{method:"GET",credentials:"same-origin",headers:{"Content-Type":"application/json"}});console.log(t);const e=await t.json();if(console.log(e),!t.ok)throw new Error("Auth check failed");const i=sessionStorage.getItem(this.cacheMetaKey);if(i){const t=JSON.parse(i);t.session_id&&t.session_id!==e.session_id&&(this.clearCachedAuth(),this.notify("session-changed",{}))}this.cacheAuth(e),this.setAuthData(e),this.initialized=!0,this.isAuthenticating=!1,this.notify("auth-loaded",{fromCache:!1})}setAuthData(t){this.authenticated=t.authenticated||!1,this.user=t.user||!1,this.nonces=t.nonces||{}}clearAuthData(){this.authenticated=!1,this.user=null,this.nonces={},sessionStorage.removeItem(this.storageKey),sessionStorage.removeItem(this.cacheMetaKey)}getCachedAuth(){try{const t=sessionStorage.getItem(this.storageKey),e=sessionStorage.getItem(this.cacheMetaKey);if(!t||!e)return null;const i=JSON.parse(e),s=JSON.parse(t);return Date.now()-i.timestamp>this.cacheExpiry?(this.clearCachedAuth(),null):s}catch(t){return console.error("Error reading cached auth:",t),null}}cacheAuth(t){try{sessionStorage.setItem(this.storageKey,JSON.stringify(t)),sessionStorage.setItem(this.cacheMetaKey,JSON.stringify({session_id:t.session_id||null,timestamp:Date.now()}))}catch(t){console.error("Error caching auth:",t)}}clearCachedAuth(){sessionStorage.removeItem(this.storageKey),sessionStorage.removeItem(this.cacheMetaKey)}async refresh(){this.isAuthenticating=!0,this.initialized=!1;try{await this.fetchAuth(),this.notify("auth-refreshed",{})}catch(t){console.error("Failed to refresh auth:",t),this.clearAuthData(),this.initialized=!0,this.isAuthenticating=!1,this.notify("auth-error",{error:t})}}getNonce(t="wp_rest"){return this.nonces[t]||""}getUser(){return this.user}isAuthenticated(){return this.authenticated}async handleLogin(t=null){if(sessionStorage.removeItem(this.storageKey),sessionStorage.removeItem(this.cacheMetaKey),t)return this.cacheAuth(t),this.setAuthData(t),this.initialized=!0,this.isAuthenticating=!1,void this.notify("auth-loaded",{fromCache:!1,fromLogin:!0});await this.refresh()}handleLogout(){this.clearAuthData(),this.notify("logged-out",{})}subscribe(t){return this.subscribers.add(t),this.initialized&&t("auth-loaded",{fromCache:!1,immediate:!0}),()=>this.subscribers.delete(t)}notify(t,e){this.subscribers.forEach((i=>{try{i(t,e)}catch(t){console.error("Subscriber error:",t)}}))}ready(){return this.initialized?Promise.resolve():new Promise((t=>{const e=this.subscribe((i=>{"auth-loaded"!==i&&"auth-error"!==i||(e(),t())}))}))}};
\ No newline at end of file
+window.auth=new class{constructor(){this.initialized=!1,this.isAuthenticating=!1,this.authenticated=!1,this.user=!1,this.nonces={},this.subscribers=new Set,this.storageKey="jvb_auth_state",this.cacheMetaKey="jvb_auth_meta",this.cacheExpiry=3e5,this.init()}async init(){if(this.isAuthenticating)return new Promise((t=>{const e=setInterval((()=>{this.initialized&&(clearInterval(e),t())}),50)}));this.isAuthenticating=!0;try{const t=this.getCachedAuth();if(t)return this.setAuthData(t),this.initialized=!0,this.isAuthenticating=!1,void this.notify("auth-loaded",{fromCache:!0});await this.fetchAuth()}catch(t){console.error("Failed to initialize auth:",t),this.clearAuthData(),this.initialized=!0,this.isAuthenticating=!1,this.notify("auth-error",{error:t})}}async refreshNonce(t="wp_rest"){try{return await this.fetchAuth(),this.getNonce(t)}catch(t){return console.error("Failed to refresh nonce:",t),null}}async fetch(t,e={}){const i=async(s=0)=>{const a={"Content-Type":"application/json",...e.headers,"X-WP-Nonce":this.getNonce()},h=await fetch(t,{...e,credentials:"same-origin",headers:a});if((403===h.status||401===h.status)&&0===s){const t=await h.clone().json();if("rest_cookie_invalid_nonce"===t.code||t.message?.includes("Cookie check"))return console.log("Nonce invalid, refreshing auth..."),await this.refresh(),i(1)}return h};return i()}async fetchAuth(){const t=await fetch(`${jvbSettings.api}auth/status`,{method:"GET",credentials:"same-origin",headers:{"Content-Type":"application/json"}});if(!t.ok)throw new Error("Auth check failed");const e=await t.json(),i=sessionStorage.getItem(this.cacheMetaKey);if(i){const t=JSON.parse(i);t.session_id&&t.session_id!==e.session_id&&(this.clearCachedAuth(),this.notify("session-changed",{}))}this.cacheAuth(e),this.setAuthData(e),this.initialized=!0,this.isAuthenticating=!1,this.notify("auth-loaded",{fromCache:!1})}setAuthData(t){this.authenticated=t.authenticated||!1,this.user=t.user||!1,this.nonces=t.nonces||{}}clearAuthData(){this.authenticated=!1,this.user=null,this.nonces={},sessionStorage.removeItem(this.storageKey),sessionStorage.removeItem(this.cacheMetaKey)}getCachedAuth(){try{const t=sessionStorage.getItem(this.storageKey),e=sessionStorage.getItem(this.cacheMetaKey);if(!t||!e)return null;const i=JSON.parse(e),s=JSON.parse(t);return Date.now()-i.timestamp>this.cacheExpiry?(this.clearCachedAuth(),null):s}catch(t){return console.error("Error reading cached auth:",t),null}}cacheAuth(t){try{sessionStorage.setItem(this.storageKey,JSON.stringify(t)),sessionStorage.setItem(this.cacheMetaKey,JSON.stringify({session_id:t.session_id||null,timestamp:Date.now()}))}catch(t){console.error("Error caching auth:",t)}}clearCachedAuth(){sessionStorage.removeItem(this.storageKey),sessionStorage.removeItem(this.cacheMetaKey)}async refresh(){this.isAuthenticating=!0,this.initialized=!1;try{await this.fetchAuth(),this.notify("auth-refreshed",{})}catch(t){console.error("Failed to refresh auth:",t),this.clearAuthData(),this.initialized=!0,this.isAuthenticating=!1,this.notify("auth-error",{error:t})}}getNonce(t="wp_rest"){return this.nonces[t]||""}getUser(){return this.user}isAuthenticated(){return this.authenticated}async handleLogin(t=null){if(sessionStorage.removeItem(this.storageKey),sessionStorage.removeItem(this.cacheMetaKey),t)return this.cacheAuth(t),this.setAuthData(t),this.initialized=!0,this.isAuthenticating=!1,void this.notify("auth-loaded",{fromCache:!1,fromLogin:!0});await this.refresh()}handleLogout(){this.clearAuthData(),this.notify("logged-out",{})}subscribe(t){return this.subscribers.add(t),this.initialized&&t("auth-loaded",{fromCache:!1,immediate:!0}),()=>this.subscribers.delete(t)}notify(t,e){this.subscribers.forEach((i=>{try{i(t,e)}catch(t){console.error("Subscriber error:",t)}}))}ready(){return this.initialized?Promise.resolve():new Promise((t=>{const e=this.subscribe((i=>{"auth-loaded"!==i&&"auth-error"!==i||(e(),t())}))}))}};
\ No newline at end of file
diff --git a/assets/js/min/checkout.min.js b/assets/js/min/checkout.min.js
new file mode 100644
index 0000000..071bdc0
--- /dev/null
+++ b/assets/js/min/checkout.min.js
@@ -0,0 +1 @@
+window.jvbCheckout=class{constructor(t={}){this.config=t,this.isInitialized=!1,this.cartItems=new Map,this.checkout=document.querySelector("aside#cart"),this.provider=this.checkout?.querySelector("form")?.dataset.provider||"",this.isOpen="1"!==this.config.isOpen||!1,this.isLoggedIn=this.config.is_logged_in||!1,this.userEmail=this.config.user_email||"",this.savedCards=[],this.selectedCardId=null,this.cartId=null,this.stepMultiplier=1,this.cache=new window.jvbCache("cart",{TTL:864e5}),this.a11y=window.jvbA11y,this.initCart(),this.checkout&&(this.initElements(),this.init(),this.initListeners(),this.isLoggedIn&&this.loadSavedCards()),this.popup=new window.jvbPopup({popup:this.checkout,toggle:this.toggle,name:"Cart",onOpen:this.maybeAddEmptyState.bind(this)})}async init(){throw new Error("init() must be implemented by subclass")}async processPayment(t){throw new Error("processPayment() must be implemented by subclass")}async submitToServer(t,e){throw new Error("submitToServer() must be implemented by subclass")}async loadSavedCards(){}async initCart(){this.cartItems=await this.cache.get("cart")??new Map,this.cartItems.size>0&&this.notifyRestoredCart()}saveCart(){this.updateTotal(),this.cache.set("cart",this.cartItems)}clearCart(){this.cartItems.clear(),window.removeChildren(this.table),this.saveCart()}getCartId(){return this.cartId||(this.cartId=crypto.randomUUID(),this.cache.set("cart_id",this.cartId)),this.cartId}initElements(){this.toggle=document.querySelector(".toggle-cart"),this.isOpen||(this.toggle.disabled=!0,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:!1})}initListeners(){this.clickHandler=this.handleClick.bind(this),this.keyHandler=this.handleEscape.bind(this),this.changeHandler=this.handleChange.bind(this),this.checkoutForm.addEventListener("submit",(t=>this.handleFormSubmit(t))),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler)}handleClick(t){if(window.targetCheck(t,"button")&&window.targetCheck(t,"div.quantity")){let e=window.targetCheck(t,"div.quantity");this.handleNumberClick(t,e)}else if(window.targetCheck(t,"[data-add-to-cart]")){let e=window.targetCheck(t,"[data-add-to-cart]");this.handleAddToCart(e)}else if(window.targetCheck(t,"[data-remove-from-cart]")){let e=window.targetCheck(t,"[data-remove-from-cart]");this.handleRemoveFromCart(e)}else window.targetCheck(t,"[data-clear-cart]")?this.clearCart():window.targetCheck(t,"[data-dismiss]")&&window.targetCheck(t,"[data-dismiss]").closest(".restored")?.remove()}handleChange(t){let e=window.targetCheck(t,".quantity-input");if(e){let a=t.target.closest(".quantity"),i=e.value;if(window.targetCheck(t,".cart-items")){let t=document.querySelector(`.menu-section [data-id="${a.dataset.id}"] input`);t&&(t.value=e.value)}i>0?this.handleAddToCart(a):this.handleRemoveFromCart(a)}}handleEscape(t){"Escape"===t.key?this.stepMultiplier=1:t.ctrlKey&&t.shiftKey?this.stepMultiplier=Math.max(100*parseInt(this.stepMultiplier),1e3):t.shiftKey&&(this.stepMultiplier=Math.max(10*parseInt(this.stepMultiplier),1e3))}handleAddToCart(t){let e=t.dataset.id,a=parseFloat(t.dataset.price),i=parseInt(t.querySelector(".quantity-input")?.value)??1,s=parseFloat(a*i);this.createItemElement(t),this.cartItems.set(e,{post_id:e,name:t.dataset.name,price:a,quantity:i,total:s,catalog_id:t.dataset.catalogId||""}),this.saveCart()}handleRemoveFromCart(t){if(confirm("This will remove this item from the cart. Continue?")){t.querySelector("[data-id]")||(t=t.closest(".item")?.querySelector(".quantity.field"));let e=t.dataset.id;this.cartItems.delete(e),this.table.querySelector(`[data-id="${e}"]`)?.closest("tr").remove();let a=document.querySelector(`[data-id="${e}"] input`);a&&(a.value=0),this.maybeAddEmptyState(),this.saveCart()}}handleNumberClick(t,e){t.preventDefault();let a=0;if(t.target.closest(".increase")?a+=1:t.target.closest(".decrease")&&(a-=1),0!==a){let t=parseInt(e.dataset.step),i=e.querySelector("input"),s=""===i.value?0:parseInt(i.value);i.value=s+t*a*this.stepMultiplier,i.dispatchEvent(new Event("change",{bubbles:!0})),this.handleNumberLimits(e)}}handleNumberLimits(t){let e=t.dataset.min,a=t.dataset.max,i=t.querySelector("input"),s=t.querySelector(".increase"),r=t.querySelector(".decrease"),n=parseInt(i.value);n<=e?(i.value=e,r.disabled=!0):n>=a?(i.value=a,s.disabled=!0):(s.disabled=!1,r.disabled=!1)}createItemElement(t){let e=this.itemsList.querySelector(`[data-id="${t.dataset.id}"]`),a=!1,i=t.dataset.price,s=t.querySelector('[name="quantity"]')?.value??1;if(e)e=e.closest("tr");else{a=!0,e=window.getTemplate("cartItem");let s=e.querySelector(".quantity");[s.dataset.id,e.querySelector("label").textContent,e.querySelector(".price").textContent,s.dataset.price,s.dataset.catalogId]=[t.dataset.id,t.dataset.name,window.formatPrice(i),i,t.dataset.catalogId||""]}[e.querySelector('[name="quantity"]').value,e.querySelector(".total").textContent]=[s,window.formatPrice(s*i)],a&&(e.classList.add("adding"),this.table.append(e),setTimeout((()=>e.classList.remove("adding")),500))}maybeAddEmptyState(){let t=this.itemsList.querySelector(".empty");if(t&&t.remove(),0===this.cartItems.size){this.checkoutPanel.disabled=!0,this.checkoutPanel.title="Add some things to your cart first!";let t=window.getTemplate("emptyCart");this.itemsList.append(t),this.table.closest("table").hidden=!0,this.total.hidden=!0,this.a11y.announce("Nothing in Cart")}else this.checkoutPanel.disabled=!1,this.table.closest("table").hidden=!1,this.total.hidden=!1,this.checkoutPanel.title="Checkout"}notifyRestoredCart(){let t=window.getTemplate("restoredCart");this.checkout.querySelector(".tab-content[data-tab=cartItems]").insertBefore(t,this.itemsList),this.cartItems.forEach((t=>{let e=window.getTemplate("cartItem"),a=e.querySelector(".quantity");[a.dataset.id,e.querySelector("label").textContent,e.querySelector(".price").textContent,a.dataset.price,a.dataset.catalogId,e.querySelector('[name="quantity"]').value,e.querySelector(".total").textContent]=[t.post_id,t.name,window.formatPrice(t.price),t.price,t.catalog_id||"",t.quantity,window.formatPrice(t.quantity*t.price)],this.table.append(e)})),this.updateTotal()}updateTotal(){let t=0;this.cartItems.forEach((e=>t+=e.total));let e=.05*t;window.eraseText(this.totalTax),window.eraseText(this.grandTotal),window.typeText(this.totalTax,window.formatPrice(e)),window.typeText(this.grandTotal,window.formatPrice(t+e)),this.totalTax.classList.remove("typeText")}extractOrderData(t){const e=Array.from(this.cartItems.values()).map((t=>({catalog_id:t.catalog_id,quantity:String(t.quantity),price:t.price,note:t.note||""}))),a=e.reduce(((t,e)=>t+e.price*e.quantity),0);return{total:Math.round(100*a),items:e,customer:{email:this.isLoggedIn?this.userEmail:t.querySelector('[name="cart_email"]')?.value||"",name:t.querySelector('[name="cart_name"]')?.value||"",phone:t.querySelector('[name="cart_phone"]')?.value||""},note:t.querySelector('[name="special_instructions"]')?.value||"",pickup_time:t.querySelector('[name="pickup_time"]')?.value||""}}async handleFormSubmit(t){if(!this.isOpen)return;if(t.preventDefault(),!this.isInitialized)return void this.handleError("Checkout not initialized");const e=t.target,a=this.extractOrderData(e);try{window.jvbLoading?.showLoading?.("Processing payment...");const t=await this.processPayment(a);this.handleSuccess(t,e)}catch(t){this.handleError(t)}finally{window.jvbLoading?.hideLoading?.()}}trackOrder(t){this.orderId=t,this.scheduleOrderCheck(),this.checkout.querySelector("button[data-tab=order]").hidden=!1}scheduleOrderCheck(){window.debouncer.schedule("order",(()=>this.checkOrderStatus()),3e4)}async checkOrderStatus(){const t=await fetch(`${this.config.api_url}order-status/${this.orderId}`,{headers:{"X-WP-Nonce":this.config.nonce}}),e=await t.json();"ready"!==e.status&&this.scheduleOrderCheck(),this.updateOrderStatus(e)}updateOrderStatus(t){this.checkout.querySelectorAll(".status-item").forEach((e=>{e.dataset.status===t.status&&e.classList.add("active")})),this.checkout.querySelector("#eta").textContent=t.eta||"In progress"}renderSavedCards(){const t=document.getElementById("saved-cards");if(!t||0===this.savedCards.length)return;const e=`\n\t\t\t<div class="saved-cards-section">\n\t\t\t\t<h4>Saved Payment Methods</h4>\n\t\t\t\t${this.savedCards.map((t=>`\n\t\t\t\t\t<label class="saved-card">\n\t\t\t\t\t\t<input type="radio" name="payment-method" value="saved" data-card-id="${t.id}">\n\t\t\t\t\t\t<span class="card-info">\n\t\t\t\t\t\t\t<strong>${t.card_brand}</strong> ending in ${t.last_4}\n\t\t\t\t\t\t\t<small>Exp: ${t.exp_month}/${t.exp_year}</small>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</label>\n\t\t\t\t`)).join("")}\n\t\t\t\t<label class="saved-card">\n\t\t\t\t\t<input type="radio" name="payment-method" value="new" checked>\n\t\t\t\t\t<span>Use a new card</span>\n\t\t\t\t</label>\n\t\t\t</div>\n\t\t`;t.innerHTML=e,t.querySelectorAll('input[name="payment-method"]').forEach((t=>{t.addEventListener("change",(t=>{const e="new"===t.target.value,a=document.getElementById("payment-container");a&&(a.style.display=e?"block":"none"),this.selectedCardId=e?null:t.target.dataset.cardId}))}))}handleSuccess(t,e){document.dispatchEvent(new CustomEvent("checkoutSuccess",{detail:{result:t,form:e,provider:this.provider}}));const a=e.dataset.successUrl||`/order-confirmation/?order=${t.order_id||t.wp_order_id}`;window.location.href=a}handleError(t){console.error(`${this.provider} checkout error:`,t),document.dispatchEvent(new CustomEvent("checkoutError",{detail:{error:t,provider:this.provider}})),window.jvbNotifications?.show?.(t.message||t||"Payment failed","error")}};
\ No newline at end of file
diff --git a/assets/js/min/form.min.js b/assets/js/min/form.min.js
index 4c61125..e36ea87 100644
--- a/assets/js/min/form.min.js
+++ b/assets/js/min/form.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(){this.a11y=window.jvbA11y,this.error=window.jvbError,this.queue=window.jvbQueue,this.populate=window.jvbPopulate,this.changes=new Map,this.forms=new Map,this.inputs=new Map,this.repeaters=new Map,this.tagLists=new Map,this.charLimits=new Map,this.quantityFields=new Map,this.quillInstances=new Map,this.dependencies=new Map,this.subscribers=new Set,this.isRestoring=!1,this.hasListeners=!1,this.summaryTemplate=!1,this.init()}init(){this.templates=window.jvbTemplates,this.defineSummaryTemplate(),this.initElements(),this.initListeners(),this.initStore(),this.initValidators()}initElements(){this.inputSelectors="input, textarea, select",this.selectors={tabs:{nav:"nav.tabs",sections:".tab.content",progress:{progress:".progress",fill:".progress .fill",details:".progress .details",icon:".progress .icon"},buttons:"nav.tabs button"},dependsOn:"[data-depends-on]",forms:{status:{status:".fstatus",message:".fstatus .message",icon:".fstatus .icon",actions:".fstatus .actions"}},inputs:this.inputSelectors,fields:{field:".field",label:"label",success:".success",error:".success",message:".validation-message"},repeater:{repeater:".repeater",header:".repeater-row-header",remove:".remove-row",add:".add-repeater-row",template:"template",items:".repeater-items",inputs:this.inputSelectors},tagList:{tagList:".field.tag-list",input:".row",add:".add-tag",remove:".remove-tag",label:".tag-label",items:".tag-items",inputs:this.inputSelectors,value:'input[type="hidden"]'},tag:{label:".tag-label"},number:{number:".field div.quantity",increase:"button.increase",decrease:"button.decrease",input:'input[type="number"]'},limits:{hasLimit:"[data-limit]",limit:".limit",current:".current"}}}initListeners(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.blurHandler=this.handleBlur.bind(this),this.inputHandler=this.handleInput.bind(this),this.submitHandler=this.handleSubmit.bind(this),this.quantityClick=this.handleQuantityClick.bind(this),this.repeaterClick=this.handleRepeaterClick.bind(this),this.tagListClick=this.handleTagListClick.bind(this),this.tagListInput=this.handleTagListInput.bind(this)}addFormListeners(e){e.addEventListener("click",this.clickHandler),e.addEventListener("change",this.changeHandler),e.addEventListener("input",this.inputHandler),e.addEventListener("blur",this.blurHandler),e.addEventListener("submit",this.submitHandler)}removeFormListeners(e){e.removeEventListener("click",this.clickHandler),e.removeEventListener("change",this.changeHandler),e.removeEventListener("input",this.inputHandler),e.removeEventListener("blur",this.blurHandler),e.removeEventListener("submit",this.submitHandler)}initStore(){const e=window.jvbStore.register("forms",{storeName:"forms",keyPath:"id",indexes:[{name:"src",keyPath:"src"},{name:"timestamp",keyPath:"timestamp"},{name:"formType",keyPath:"type"}],TTL:1008e4});this.store=e.forms,this.store.subscribe(((e,t)=>{if("data-ready"===e){let e=this.store.getFiltered().filter((e=>e.src===window.location.pathname));for(let t of e)this.showPendingNotification(t.id,t.changes)}else"operation-status"===e&&"completed"===t.status&&t.config&&this.store.delete(t.config.id)}))}showPendingNotification(e,t){let s=this.forms.get(e);if(!s)return;let i=s.element;if(!i)return void console.warn(`Form element not found for: ${e}`);const a=document.createElement("div");a.className="pendingChanges",a.innerHTML=`\n\t\t\t<p>We noticed unsaved changes from last time. Would you like to restore them?</p>\n        <button class="restore" type="button" data-form-id="${e}">Restore</button>\n        <button class="discard" type="button" data-form-id="${e}">Discard</button>`,i.insertBefore(a,s.ui.status.status),a.querySelector(".restore").addEventListener("click",(async()=>{this.isRestoring=!0;let e={fields:t};this.populate.populate(i,e),this.a11y.announce("Previous changes restored"),this.isRestoring=!1,a.remove()})),a.querySelector(".discard").addEventListener("click",(async()=>{await this.store.delete(e),this.a11y.announce("Previous changes discarded"),a.remove()}))}initValidators(){this.validators={email:{pattern:/^[^\s@]+@[^\s@]+\.[^\s@]+$/,message:"Please enter a valid email address"},url:{pattern:/^https?:\/\/.+\..+/,message:"Please enter a valid URL starting with https://"},phone:{pattern:/^[\d\s\-+().]+$/,message:"Please enter a valid phone number"},number:{test:(e,t)=>{const s=parseFloat(e);if(isNaN(s))return"Please enter a valid number";const i=t.dataset.min,a=t.dataset.max;return void 0!==i&&s<parseFloat(i)?`Value must be at least ${i}`:!(void 0!==a&&s>parseFloat(a))||`Value must be at most ${a}`}},text:{test:(e,t)=>{const s=t.dataset.minlength,i=t.dataset.maxlength;return s&&e.length<parseInt(s)?`Must be at least ${s} characters`:!(i&&e.length>parseInt(i))||`Must be no more than ${i} characters`}}}}validateField(e){const t=this.performValidation(e);return this.updateValidationUI(e,t),t.isValid}performValidation(e){const t=e.closest(".field"),s=this.getFieldCheckedValue(e);if(!s&&!e.required)return{isValid:!0,message:""};if(e.required)if("checkbox"===e.type){if(!e.checked)return{isValid:!1,message:"This field is required"}}else if("radio"===e.type){const t=document.querySelectorAll(`input[name="${e.name}"]`);if(!Array.from(t).some((e=>e.checked)))return{isValid:!1,message:"Please select an option"}}else if(!s)return{isValid:!1,message:"This field is required"};if(e.checkValidity&&!e.checkValidity())return{isValid:!1,message:e.validationMessage};if(s&&Object.hasOwn(t.dataset,"pattern")){if(!new RegExp(t.dataset.pattern).test(s))return{isValid:!1,message:t.dataset.validationMessage||"Invalid format"}}if(Object.hasOwn(t.dataset,"validate")||e.type){const i=this.validators[t.dataset.validate||e.type];if(i&&i.pattern&&!i.pattern.test(s))return{isValid:!1,message:i.message};if(i&&i.test){const e=i.test(s,t);if(!0!==e)return{isValid:!1,message:e}}}return{isValid:!0,message:""}}updateValidationUI(e,t){t.isValid?this.showSuccess(e,t.message):this.showError(e,t.message)}handleClick(e){let t=this.getForm(e.target);if(!t)return;const s=window.targetCheck(e,"[data-action]");if(s){switch(s.dataset.action){case"clear-form":this.store.delete(t.id),t.element.reset(),t.ui.status.status.hidden=!0,this.a11y.announce("Form cleared, starting fresh");break;case"dismiss-restore":t.ui.status.status.hidden=!0}}}handleChange(e){if(e.target.closest("[data-ignore]")||this.isRestoring)return;let t=this.getField(e.target);if(this.dependencies.has(t.dataset.field)){this.dependencies.get(t.dataset.field).items.forEach((e=>{this.checkFieldDependency(e,t.dataset.field)}))}if(Object.hasOwn(t.dataset,"repeater-id")||Object.hasOwn(t.dataset,"tag-list-id"))return void this.updateCollectionField(t);let s=this.getForm(e.target);this.updateItem(t.dataset.field,this.getFieldValue(e.target),s)}handleBlur(e){if(e.target.closest("[data-ignore]")||this.isRestoring)return;let t=this.getForm(e.target);if(!t)return;let s=this.getField(e.target).dataset.field;window.debouncer.cancel(`form:${t.id}:validate:${s}`),this.validateField(e.target),this.updateItem(s,this.getFieldValue(e.target),t)}handleInput(e){let t=this.getForm(e.target);if(!t)return;let s=this.getField(e.target);if(!s)return;const i=e.target,a=s.dataset.field;this.showFormStatus(t.id,"pending"),window.debouncer.schedule(`form:${t.id}:validate:${a}`,(()=>this.validateField(i)),500)}async handleSubmit(e){let t=this.getForm(e.target);if(t){if(this.subscribers.size>0)if(e.preventDefault(),t.options.cache){this.cancelBackup(),await this.backup();const e=await this.store.get(t.id);this.notify("form-submit",{config:t,data:e.changes})}else this.notify("form-submit",{config:t,data:this.changes.get(t.id)?.changes??{}});if(t.options.showSummary){const e=await this.store.get(t.id);this.showSummary({config:t,changes:e?.changes})}}}updateItem(e,t,s){this.changes.has(s.id)||this.changes.set(s.id,{id:s.id,timestamp:Date.now(),src:window.location.pathname,changes:{}});let i=this.changes.get(s.id);i.changes[e]=t,this.changes.set(s.id,i),s.options.cache&&this.scheduleBackup()}scheduleBackup(){window.debouncer.schedule("form_changes",(async()=>{this.changes.size>0&&await this.backup()}),2e3)}cancelBackup(){window.debouncer.cancel("form_changes")}async backup(){const e=new Map;for(let[t,s]of this.changes.entries()){const i=await this.store.get(t);i?e.set(t,{...i,...s,changes:{...i.changes,...s.changes},timestamp:Date.now()}):e.set(t,s)}await this.store.saveMany(e);for(let e of this.changes.keys())this.showFormStatus(e,"autosaved");this.changes.clear()}saveCache(e){if(!this.changes.has(e))return;let t=this.changes.get(e);0!==t.size&&(this.store.save(t).then((()=>{})),this.changes.delete(e))}registerForm(e,t){if(Object.hasOwn(e.dataset,"formId")&&this.forms.has(e.dataset.formId))return;Object.hasOwn(e.dataset,"formId")||(e.dataset.formId=window.generateID("form_"));const s=e.dataset.formId;this.addFormListeners(e);const i={element:e,id:s,status:"",options:{autoUpload:t.autoUpload??!1,imageMeta:t.imageMeta??!0,delay:t.delay??1500,endpoint:t.save??e.dataset.save??"",showStatus:t.showStatus??!0,showSummary:t.showSummary??!1,cache:t.cache??!0,ignore:t.ignore??[]},ui:window.uiFromSelectors(this.selectors.forms,e)};return this.initializeFields(e,i),this.forms.set(s,i),i}clearForm(e){const t=this.forms.get(e);if(!t)return;t.unsubscribeTabs&&t.unsubscribeTabs(),t.tabs&&window.jvbTabs.removeTab(t.element),t.cache&&this.changes.has(e)&&this.saveCache(e);for(let[t,s]of this.inputs.entries())s.form===e&&this.inputs.delete(t);if(this.dependencies.forEach(((t,s)=>{t.items=t.items.filter((t=>t.form!==e)),0===t.items.length&&this.dependencies.delete(s)})),Object.hasOwn(t,"hasQuill")&&this.quillInstances.has(e)){this.quillInstances.get(e).forEach((e=>{e.disable(),e.off("text-change"),e.off("selection-change");const t=e.container.parentElement,s=t?.querySelector(".ql-toolbar");if(s&&s.remove(),e.setText(""),t&&t.classList.contains("editor-container")){const e=t.nextElementSibling;"TEXTAREA"===e?.tagName&&(e.style.display=""),t.remove()}})),this.quillInstances.delete(e)}let s={repeater:this.repeaters,tagList:this.tagLists,charLimit:this.charLimits,quantity:this.quantityFields};for(let[t,i]of Object.entries(s)){if(0===i.size)continue;let s=Array.from(i.values()).filter((t=>t.form===e));s.length>0&&s.forEach((e=>{switch(t){case"repeater":this.removeRepeaterListeners(e.element);break;case"tagList":this.removeTagListListeners(e.element);break;case"charLimit":this.removeCharacterLimitListeners(e.element);break;case"quantity":this.removeQuantityListeners(e.element)}i.has(e.id)&&i.delete(e.id)}))}this.removeFormListeners(t.element),this.forms.delete(e),window.debouncer.cancel("form_changes")}defineSummaryTemplate(){this.summaryTemplate=!0;let e=this;this.templates.define("formSummary",{refs:{result:".result",h3:"h3",p:"p"},setup({el:t,refs:s,manyRefs:i,data:a}){const r=["sendAll",...a.config.options.ignore??[]];for(let[i,n]of Object.entries(a.changes)){if(r.includes(i)||e.isEmptyValue(n))continue;let a=Array.from(e.inputs.values()).find((e=>e.field?.dataset.field===i));if(!a)continue;let l=s.result.cloneNode(!0),o=l.querySelector("h3"),d=l.querySelector("p");const c=a.field?.querySelector("legend");o.textContent=c?c.textContent.replace("*","").trim():a.ui.label?.textContent.replace("*","").trim();const u=e.formatValueForSummary(n,a);u instanceof HTMLElement?d.replaceWith(u):d.textContent=u,t.append(l)}let n=a.config?.element?.querySelectorAll("[data-upload-field]");n&&n.forEach((e=>{let i=e.querySelector("h2")?.textContent??"Upload:",a=e.querySelectorAll(".item-grid.preview img"),r=s.result.cloneNode(!0);if(a){let e=s.result.cloneNode(!0),n=r.querySelector("h3"),l=r.querySelector("p");l?.remove(),n&&(n.textContent=i),a.forEach((t=>{t=t.cloneNode(!0),e.append(t)})),t.append(e)}})),s.result?.remove(),a.config.element.after(t),window.fade(a.config.element,!1)}})}initializeFields(e,t=null){const s={"[data-editor]":()=>this.checkForQuill(e,t),"div.quantity":()=>this.checkForQuantity(e),".repeater":()=>this.checkForRepeaters(e,t),".field.tag-list":()=>this.checkForTagLists(e),"[data-depends-on]":()=>this.checkForConditionalFields(e),"[data-limit]":()=>this.checkForCharacterLimits(e),"[data-uploader],[data-upload-field]":()=>this.checkForImageUploads(e,t),"nav.tabs":()=>this.checkForTabs(e,t),'[data-type="selector"]':()=>this.checkForSelectors(e)};for(const[t,i]of Object.entries(s))e.querySelector(t)&&i();Array.from(e.querySelectorAll(this.inputSelectors)).map((e=>{this.getItem(e,t?.id)}))}checkForQuill(e,t){if(!e.querySelector("[data-editor]"))return;t&&!Object.hasOwn(t,"hasQuill")&&(t.hasQuill=!0,this.forms.set(t.id,t)),this.quillInstances.has(t.id)||this.quillInstances.set(t.id,new Set);window.jvbQuill(e).forEach((e=>{this.quillInstances.get(t.id).add(e)}))}checkForQuantity(e){e.querySelector(this.selectors.number.number)&&e.querySelectorAll(this.selectors.number.number).forEach((t=>{let s={id:window.generateID("quant"),form:e.dataset.formId,ui:window.uiFromSelectors(this.selectors.number,t),element:t};t.dataset.numId=s.id,this.quantityFields.set(s.id,s),this.addQuantityListeners(t)}))}addQuantityListeners(e){e.addEventListener("click",this.quantityClick)}removeQuantityListeners(e){e.removeEventListener("click",this.quantityClick)}handleQuantityClick(e){let t=this.quantityFields.get(e.target.closest("[data-num-id]")?.dataset.numId);if(!t)return;let s=0;if(t.increase.contains(e.target)?s++:t.decrease.contains(e.target)&&s--,0===s)return;this.getField(e.target);let i=t.input.step;i=Math.max(i,1),e.ctrlKey&&e.shiftKey?i*=50:e.ctrlKey?i*=5:e.shiftKey&&(i*=10);let a=""===t.input.value?0:parseFloat(t.input.value);t.input.value=a+i*s,a=parseFloat(t.input.value),t.input.min&&a<t.input.min?(t.input.value=t.input.min,t.decrease.disabled=!0):t.input.max&&a>t.input.max?(t.input.value=t.input.max,t.increase.disabled=!0):(t.decrease.disabled&&(t.decrease.disabled=!1),t.increase.disabled&&(t.increase.disabled=!1))}checkForRepeaters(e){e.querySelector(this.selectors.repeater.repeater)&&e.querySelectorAll(this.selectors.repeater.repeater).forEach((t=>{let s={id:t.querySelector("template").className??window.generateID("repeater"),ui:window.uiFromSelectors(this.selectors.repeater,t),form:e.dataset.formId,element:t,field:this.getField(t),sortable:!1};if(!s.ui.add)return;let i=t.querySelector("template");this.templates.define(i.className,{manyRefs:{inputs:this.inputSelectors},setup({el:e,refs:t,manyRefs:i,data:a}){let r=s.ui.items?.children?.length??0;e.dataset.index=r,i.inputs?.forEach((t=>{window.prefixInput(t,`${a.repeater.dataset.fieldName}:${r}:`,e)}))}}),window.Sortable&&(s.sortable=new Sortable(t,{handle:this.selectors.repeater.header,animation:150,onEnd:()=>{this.reindexList(t)}})),t.dataset.repeaterId=s.id,this.addRepeaterListeners(t),this.repeaters.set(s.id,s)}))}addRepeaterListeners(e){e.addEventListener("click",this.repeaterClick)}removeRepeaterListeners(e){e.removeEventListener("click",this.repeaterClick)}handleRepeaterClick(e){e.target.matches(this.selectors.repeater.add)?(console.log("Add Repeater Row"),this.addRepeaterRow(e.target.closest("[data-repeater-id]"))):e.target.matches(this.selectors.repeater.remove)&&(console.log("Remove Repeater Row"),this.removeRepeaterRow(e.target.closest("[data-index]")))}addRepeaterRow(e){let t={};t.repeater=e,e.append(this.templates.create(e.dataset.repeaterId,t)),this.initializeFields(e,this.getField(e).config??{}),this.a11y.announce("Row added")}removeRepeaterRow(e){let t=e.closest("[data-repeater-id]");e.remove(),this.reindexList(t),this.a11y.announce("Row removed")}checkForTagLists(e){e.querySelectorAll(this.selectors.tagList.tagList)?.forEach((t=>{let s={id:t.querySelector("template").className??window.generateID("tagList"),ui:window.uiFromSelectors(this.selectors.tagList,t),element:t,form:e.dataset.formId,format:t.dataset.tagFormat??"first_field"};if(console.log("Registering Tag List with config",s),!s.ui.input||!s.ui.add||!s.ui.items)return;t.dataset.tagListId=s.id;let i=t.querySelector("template");this.templates.define(i.className,{refs:{label:this.selectors.tagList.label},manyRefs:{inputs:this.inputSelectors},setup({el:e,refs:t,manyRefs:i,data:a}){let r=s.ui.items?.children?.length??0;e.dataset.index=r,i.inputs?.forEach((t=>{let s=t.closest(".tag-item");window.prefixInput(t,`${e.dataset.fieldName}:${r}:`,s)})),t.label&&(t.label.textContent=a.label)}}),s.ui.inputs=Array.from(t.querySelectorAll(this.selectors.tagList.inputs)),s.ui.value=Array.from(t.querySelectorAll(this.selectors.tagList.value)),this.tagLists.set(s.id,s),console.log("Adding tag list listeners to ",t),this.addTagListListeners(t)}))}addTagListListeners(e){e.addEventListener("click",this.tagListClick),e.addEventListener("keypress",this.tagListInput,{passive:!0})}removeTagListListeners(e){e.removeEventListener("click",this.tagListClick),e.removeEventListener("keypress",this.tagListInput)}handleTagListClick(e){window.targetCheck(e,this.selectors.tagList.add)?this.addTagListItem(e.target.closest("[data-tag-list-id]")):e.target.matches(this.selectors.tagList.remove)&&this.removeTagListItem(e.target.closest(this.selectors.tagList.remove))}addTagListItem(e){let t=this.tagLists.get(e.dataset.tagListId);if(!t)return;let s,i={},a=!1;for(let e of t.ui.inputs){this.validateField(e);const t=e.name.replace("new_",""),s=this.getFieldValue(e);s&&(a=!0),i[t]=s,["checkbox","radio"].includes(e.type)?e.checked=!1:e.value="",this.clearValidation(e)}if(!a)return this.a11y.announce("Please fill in at least one field"),void t.ui.inputs[0].focus();switch(t.format){case"first_field":s=Object.values(i)[0];break;case"all_fields":s=Object.values(i).join(", ");break;default:if(t.format.includes("{")){let e=t.format;for(const[t,s]of Object.entries(i))e=e.replace(`{${t}}`,s)}else s=i[t.format]??Object.values(i)[0]}let r=this.templates.create(e.dataset.tagListId,{label:s});const n=t.ui.items?.children?.length??0;r?.querySelectorAll("input[type=hidden]")?.forEach((e=>{const s=e.dataset.field;e.name=`${t.element.field}:${n}:${s}`,e.value=i[s]||""})),t.ui.items.append(r),t.ui.inputs[0]?.focus(),this.updateCollectionField(e),this.a11y.announce("Item added")}removeTagListItem(e){let t=e.closest("[data-tag-list-id]");e.remove(),this.reindexList(t),this.a11y.announce("Item removed")}handleTagListInput(e){let t=e.target,s=t.closest("[data-tag-list-id]");if(!s)return;let i=this.tagLists.get(s.dataset.tagListId);if(i&&"Enter"===e.key)if(t===i.ui.inputs[i.ui.inputs.length-1])e.preventDefault(),this.addTagListItem(t.closest("[data-tag-list-id]"));else{e.preventDefault();let s=i.ui.inputs.indexOf(t);i.ui.inputs[s+1].focus()}}checkForConditionalFields(e){e.querySelectorAll(this.selectors.dependsOn).forEach((t=>{const s=t.dataset.dependsOn,i=t.dataset.dependsValue,a=t.dataset.dependsOperatior??"==";if(!this.dependencies.has(s)){let e=document.querySelector(`[field="${s}"]`);e&&this.dependencies.set(s,{element:e,items:[]})}let r=this.dependencies.get(s);r.items.push({field:t,form:e.dataset.formId,requiredValue:i,operator:a}),this.dependencies.set(s,r),this.checkFieldDependency(r,s)}))}checkFieldDependency(e,t){const s=this.dependencies.get(t);if(!s)return;const i=this.getFieldCheckedValue(s.element),a=this.evaluateCondition(i,e.requiredValue,e.operator);this.toggleFieldVisibility(e.field,a)}evaluateCondition(e,t,s){const i=String(e||""),a=String(t||"");switch(s){case"==":default:return i===a;case"!=":return i!==a;case">":return parseFloat(i)>parseFloat(a);case"<":return parseFloat(i)<parseFloat(a);case">=":return parseFloat(i)>=parseFloat(a);case"<=":return parseFloat(i)<=parseFloat(a);case"contains":return i.includes(a);case"empty":return""===i;case"not_empty":return""!==i}}toggleFieldVisibility(e,t){const s=e.closest(".field, fieldset");s&&(s.hidden=!t,s.querySelectorAll("input, select, textarea").forEach((e=>{e.disabled=!t,!t&&e.hasAttribute("required")?(e.dataset.wasRequired="true",e.removeAttribute("required")):t&&"true"===e.dataset.wasRequired&&(e.setAttribute("required",""),delete e.dataset.wasRequired)})))}checkForCharacterLimits(e){e.querySelector(this.selectors.limits.hasLimit)&&(this.countUpdaters=this.updateCount.bind(this),e.querySelectorAll(`${this.selectors.limits.hasLimit}`).forEach((t=>{let s=window.generateID("limit");t.dataset.charLimitId=s;let i={element:t,form:e.dataset.formId,ui:window.uiFromSelectors(this.selectors.limits,t.closest(".field"))};i.ui.limit.textContent=t.dataset.limit,this.charLimits.set(s,i),this.addCharacterLimitListeners(t)})))}addCharacterLimitListeners(e){e.addEventListener("input",this.countUpdaters,{passive:!0})}removeCharacterLimitListeners(e){e.removeEventListener("input",this.countUpdaters,{passive:!0})}updateCount(e){let t=e.target,s=this.charLimits.get(t.dataset.charLimitId);if(!s)return;let i=t.value.length,a=t.dataset.limit;s.ui.current&&(s.ui.current.textContent=i,s.ui.current.classList.toggle("exceeded",i>=a)),i>a&&(t.value=t.value.slice(0,a))}checkForImageUploads(e,t){window.jvbUploads.scanFields(e,t.options.autoUpload,t.options.imageMeta)}checkForTabs(e,t){window.jvbTabs&&e.querySelector("nav.tabs")&&(t.tabs=window.jvbTabs.registerTab(e,{preCheck:(e,s)=>this.validateStep(e,t)}),t.ui.tabs=window.uiFromSelectors(this.selectors.tabs,e),t.ui.tabs.sections=Array.from(e.querySelectorAll(this.selectors.tabs.sections)),t.ui.tabs.inputs={},t.ui.tabs.sections.forEach((e=>{t.ui.tabs.inputs[e.dataset.tab]=Array.from(e.querySelectorAll(this.inputs))})),t.ui.tabs.buttons=Array.from(e.querySelectorAll(this.selectors.tabs.buttons)),t.unsubscribeTabs=window.jvbTabs.subscribe(((e,s)=>{if("tab-switched"===e&&t.ui.tabs.progress){const e=t.ui.tabs.sections.filter((e=>e.dataset.tab===s.current))[0]??!1;if(!e)return;const i=e.dataset.step,a=t.ui.sections.length;window.showProgress(t.ui.tabs.progress,i,a)}})),this.forms.set(t.id,t))}validateStep(e,t){const s=e.closest("[data-form-id]")?.dataset.formId;if(!s)return!0;if(!this.forms.get(s))return!0;return Array.from(this.inputs.values()).filter((t=>t&&t.form===s&&t.section===e.dataset.tab&&!t.element.closest("[hidden]"))).every((e=>!0===this.validateField(e.element)))}checkForSelectors(e){window.jvbSelector&&window.jvbSelector.scanExistingFields(e)}reindexList(e){const t=e.dataset.field||e.dataset.repeaterId||e.dataset.tagListId;Array.from(e.children).forEach(((e,s)=>{e.dataset.index=`${s}`;e.querySelectorAll("input, select, textarea").forEach((i=>{if("file"===i.type)return;i.dataset.field||i.name.split(":").pop();window.prefixInput(i,`${t}:${s}:`,e)}))})),this.updateCollectionField(e)}updateCollectionField(e){const t=e.closest("[data-field]");if(!t)return;const s=t.dataset.fieldType;if(!["repeater","tag-list"].includes(s))return;const i=this.getForm(e);if(!i)return;const a=this.getFieldValue(t.querySelector("input, select, textarea"));this.updateItem(t.dataset.field,a,i)}clearValidation(e){let t=this.getField(e);if(!t)return;let s=this.getItem(e);s&&(t.classList.remove("has-error","has-success"),s.ui.success&&(s.ui.success.hidden=!0),s.ui.error&&(s.ui.error.hidden=!0),s.ui.message&&(s.ui.message.hidden=!0,s.ui.message.textContent=""))}showError(e,t="Invalid field"){let s=this.getField(e);if(!s)return;let i=this.getItem(e);i&&(s.classList.remove("has-success"),s.classList.add("has-error"),i.ui.success&&(i.ui.success.hidden=!0),i.ui.error&&(i.ui.error.hidden=!0),i.ui.message&&(i.ui.message.hidden=!1,i.ui.message.textContent=t))}showSuccess(e,t=""){let s=this.getField(e);if(!s)return;let i=this.getItem(e);i&&(s.classList.remove("has-error"),s.classList.add("has-success"),i.ui.success&&(i.ui.success.hidden=!1),i.ui.error&&(i.ui.error.hidden=!0),i.ui.message&&(i.ui.message.hidden=""===t,i.ui.message.textContent=t))}handleFormSuccess(e,t){if(e.querySelectorAll(".error-message").forEach((e=>e.remove())),e.querySelectorAll(".field-error").forEach((e=>e.classList.remove("field-error"))),e.classList.add("form-success"),t.message){const s=document.createElement("div");s.className="form-success-message success-message",s.textContent=t.message,e.insertBefore(s,e.firstChild);const i=window.getIcon?.("check-circle");i&&(i.classList.add("success-icon"),s.prepend(i))}if(t.title||t.description){const s=document.createElement("div");if(s.className="success-box",t.title){const e=document.createElement("h3");e.textContent=t.title,s.appendChild(e)}if(t.description){(Array.isArray(t.description)?t.description:[t.description]).forEach((e=>{const t=document.createElement("p");t.textContent=e,s.appendChild(t)}))}e.insertBefore(s,e.firstChild)}if(e.dataset.formId){this.store.delete(e.dataset.formId).catch((e=>{console.warn("Failed to clear form cache:",e)}));const t=this.forms.get(e.dataset.formId);t&&(t.isDirty=!1,t.lastSaved=Date.now(),t.data={})}window.jvbA11y&&window.jvbA11y.announce(t.message||"Form submitted successfully")}handleFormError(e,t){if(e.querySelectorAll(".error-message").forEach((e=>e.remove())),e.querySelectorAll(".field-error, .has-error").forEach((e=>{e.classList.remove("field-error","has-error")})),e.querySelectorAll(".field").forEach((e=>{this.clearValidation(e)})),t.field){const s=e.querySelector(`[data-field="${t.field}"]`);if(s){this.showError(s,t.message),this.touchedFields.add(t.field),s.scrollIntoView({behavior:"smooth",block:"center"});const e=s.querySelector("input, textarea, select");e&&e.focus()}}else{const s=document.createElement("div");s.className="form-error error-message",s.textContent=t.message;const i=window.getIcon?.("close-circle");i&&(i.classList.add("error-icon"),s.prepend(i)),e.insertBefore(s,e.firstChild),e.scrollIntoView({behavior:"smooth",block:"start"})}if(window.jvbA11y){const e=t.field?`Error in ${t.field}: ${t.message}`:`Form error: ${t.message}`;window.jvbA11y.announce(e)}e.dispatchEvent(new CustomEvent("jvb-form-error",{detail:t}))}showFormStatus(e,t,s=""){let i=this.forms.get(e);i&&i.options.showStatus&&i.ui?.status?.status&&i.status!==t&&(i.status=t,i.ui.status.status.hidden=!1,i.ui.status.status.classList.toggle("loading",["uploading","saving"].includes(t)),i.ui.status.message.textContent=""===s?this.getDefaultMessage(t):s,i.ui.status.icon.className="icon icon-"+this.getDefaultIcon(t),setTimeout((()=>i.ui.status.status.hidden=!0),"submitted"===t?3e3:1e4))}getDefaultMessage(e){return{saving:"Saving changes...",autosaved:"Changes saved locally. Submit form to send to server.",uploading:"Uploading your form to server",submitted:"Successfully sent to server",pending:"Unsaved changes",restored:"Welcome back! We've restored your previous entry.",error:"Failed to save changes. Refresh and try again?",offline:"Changes will be saved when online"}[e]??e}getDefaultIcon(e){return{autosaved:"check-circle",submitted:"check-circle",restored:"history",error:"close-circle",offline:"cloud-slash",pending:"exclamation-mark"}[e]??""}showSummary(e){let t=this.templates.create("formSummary",e);e.config.element.after(t),window.fade(e.config.element,!1)}getForm(e){let t=e.closest("[data-form-id]").dataset.formId;if(!t)return!1;let s=this.forms.get(t);return s||!1}getField(e){return e.closest("[data-field]")}getFieldType(e){let t=this.getField(e);if(t)return t.dataset.fieldType}getFieldValue(e){let t=this.getFieldType(e),s=this.getItem(e),i=s.field?.dataset.field??!1;if(!i)return!1;switch(t){case"repeater":return this.getRepeaterValue(e,s);case"tag-list":return this.getTagListValue(e,s);case"group":break;case"location":return this.getLocationValue(e,s);case"selector":case"upload":return this.getHiddenInputValue(e,s,i);case"true-false":return"1"===e.value||"on"===e.value||"true"===e.value;case"checkbox":return e.name.endsWith("[]")?this.getCheckboxGroupValue(e,s):e.checked?e.value:"";default:return e.value}}getCheckboxGroupValue(e,t){return t.checkboxGroup||(t.checkboxGroup=t.field?.querySelectorAll(`input[type="checkbox"][name="${e.name}"]`),this.saveItem(t)),Array.from(t.checkboxGroup).filter((e=>e.checked)).map((e=>e.value))}getFieldCheckedValue(e){if("checkbox"===e.type){return"true-false"===this.getFieldType(e)?e.checked:e.checked?e.value:""}if("radio"===e.type){const t=document.querySelectorAll(`input[name="${e.name}"]`),s=Array.from(t).find((e=>e.checked));return s?s.value:""}return this.getFieldValue(e)}isEmptyValue(e){return null==e||""===e||(!(!Array.isArray(e)||0!==e.length)||"object"==typeof e&&0===Object.keys(e).length)}getRepeaterValue(e,t){t.container||(t.container=t.field?.querySelector(".repeater-items"),this.saveItem(t));let s=[];return Array.from(t.container.children).forEach((e=>{let t={};e.querySelectorAll("[data-field]").forEach((e=>{t[e.dataset.field]=this.getFieldValue(e)})),s.push(t)})),s}getTagListValue(e,t){t.container||(t.container=t.field?.querySelector(".tag-items"),this.saveItem(t));let s=[];return Array.from(t.container.children).forEach((e=>{let t=e.querySelectorAll('input[type="hidden"]'),i={};t.forEach((e=>{i[e.dataset.field]=e.value})),s.push(i)})),s}getLocationValue(e,t){t.values||(t.values=Array.from(t.field?.querySelectorAll("[data-location-field]")),this.saveItem(t));let s={};return t.values.forEach((e=>{s[e.dataset.locationField]=e.value})),s}getHiddenInputValue(e,t,s){return t.value||(t.value=t.field?.querySelector(`input[type=hidden][name="${s}"]`),this.saveItem(t)),t.value.value}formatValueForSummary(e,t){const s=this.getFieldType(t.element);if(this.isEmptyValue(e))return"";switch(s){case"repeater":return this.formatRepeaterForSummary(e,t);case"tag-list":return this.formatTagListForSummary(e,t);case"location":return this.formatLocationForSummary(e);case"true-false":return e?"Yes":"No";case"checkbox":return Array.isArray(e)?this.formatCheckboxGroupForSummary(e,t):this.getDisplayLabel(t,e);case"selector":case"upload":return this.formatHiddenFieldForSummary(e,t,s);default:return"string"==typeof e?this.getDisplayLabel(t,e):"string"==typeof e&&e.includes("\n")?this.convertLineBreaks(e):e}}formatCheckboxGroupForSummary(e,t){return e.map((e=>this.getDisplayLabel(t,e))).join(", ")}convertLineBreaks(e){const t=document.createElement("span");return t.innerHTML=e.split("\n").join("<br>"),t}formatRepeaterForSummary(e,t){const s=document.createElement("div");return s.className="summary-repeater",e.forEach(((e,i)=>{const a=document.createElement("div");a.className="summary-repeater-row";const r=document.createElement("strong");r.textContent=`Entry ${i+1}:`,a.appendChild(r);const n=document.createElement("ul");n.className="summary-repeater-fields";for(const[s,i]of Object.entries(e)){if(this.isEmptyValue(i))continue;const e=document.createElement("li"),a=t.field?.querySelector(`[data-field="${s}"]`),r=a?.closest(".field")?.querySelector("label")?.textContent.replace("*","").trim()||s;e.innerHTML=`<span class="field-label">${r}:</span> <span class="field-value">${i}</span>`,n.appendChild(e)}a.appendChild(n),s.appendChild(a)})),s}formatTagListForSummary(e,t){const s=document.createElement("div");s.className="summary-taglist";const i=document.createElement("ul");return i.className="summary-tags",e.forEach((e=>{const t=document.createElement("li");t.className="summary-tag";const s=Object.values(e).find((e=>!this.isEmptyValue(e)))||"",a=Object.entries(e).filter((([e,t])=>!this.isEmptyValue(t)));a.length>1?t.textContent=a.map((([e,t])=>t)).join(", "):t.textContent=s,i.appendChild(t)})),s.appendChild(i),s}formatLocationForSummary(e){const t=[];return e.street&&t.push(e.street),e.city&&t.push(e.city),e.province&&t.push(e.province),e.postal_code&&t.push(e.postal_code),e.country&&t.push(e.country),t.length>0?t.join(", "):e.address||""}formatHiddenFieldForSummary(e,t,s){if("upload"===s){const s=t.field?.querySelector("[data-upload-field]");if(s){const e=s.querySelectorAll(".item-grid.preview img");if(e.length>0){const t=document.createElement("div");return t.className="summary-uploads",e.forEach((e=>{const s=e.cloneNode(!0);s.style.maxWidth="100px",s.style.maxHeight="100px",t.appendChild(s)})),t}}return`${e.split(",").length} file(s) uploaded`}return e}getDisplayLabel(e,t){if(!e.element)return t;const s=e.element.type;if("radio"===s){const s=e.field.querySelectorAll(`input[type="radio"][name="${e.element.name}"]`),i=Array.from(s).find((e=>e.value===t));if(i){const t=i.closest("label")||e.field.querySelector(`label[for="${i.id}"]`);if(t)return t.textContent.replace("*","").trim()}}if("checkbox"===s&&"true-false"!==this.getFieldType(e.element)){const s=e.field.querySelector(`input[type="checkbox"][value="${t}"]`);if(s){const t=s.closest("label")||e.field.querySelector(`label[for="${s.id}"]`);if(t){const e=t.querySelector("span");return e?e.textContent.trim():t.textContent.replace("*","").trim()}}}return t}getItem(e,t=null){const s=Object.hasOwn(e.dataset,"ref");let i=s?e.dataset.ref:window.generateID("input");if(s||(e.dataset.ref=i),!this.inputs.has(i)){t||(t=e.closest("[data-form-id]")?.dataset.formId??!1);let s=this.getField(e);this.inputs.set(i,{id:i,element:e,form:t,field:s,section:e.closest("[data-tab]")?.dataset.tab??!1,ui:window.uiFromSelectors(this.selectors.fields,s)})}return this.inputs.get(i)}saveItem(e){this.inputs.set(e.id,e)}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((s=>{try{s(e,t)}catch(e){console.error("HandleSelection subscriber error:",e)}}))}destroy(){this.forms.size>0&&(Array.from(this.forms.values()).forEach((e=>{this.removeFormListeners(e)})),this.forms.clear()),this.repeaters.size>0&&(Array.from(this.repeaters.values()).forEach((e=>{this.removeRepeaterListeners(e.element),e.sortable?.destroy()})),this.repeaters.clear()),this.quantityFields.size>0&&(Array.from(this.quantityFields.values()).forEach((e=>{this.removeQuantityListeners(e.element)})),this.quantityFields.clear()),this.tagLists.size>0&&(Array.from(this.tagLists.values()).forEach((e=>{this.removeTagListListeners(e.element)})),this.tagLists.clear()),this.charLimits.size>0&&Array.from(this.charLimits.values()).forEach((e=>{e.removeEventListener("input",this.countUpdaters)})),this.inputs.clear(),this.forms.clear(),this.charLimits.clear()}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbForm=new e)}))}))})();
\ No newline at end of file
+(()=>{class e{constructor(){this.a11y=window.jvbA11y,this.error=window.jvbError,this.queue=window.jvbQueue,this.populate=window.jvbPopulate,this.changes=new Map,this.forms=new Map,this.inputs=new Map,this.repeaters=new Map,this.tagLists=new Map,this.charLimits=new Map,this.quantityFields=new Map,this.quillInstances=new Map,this.dependencies=new Map,this.subscribers=new Set,this.isRestoring=!1,this.hasListeners=!1,this.summaryTemplate=!1,this.init()}init(){this.templates=window.jvbTemplates,this.defineSummaryTemplate(),this.initElements(),this.initListeners(),this.initStore(),this.initValidators()}initElements(){this.inputSelectors="input, textarea, select",this.selectors={tabs:{nav:"nav.tabs",sections:".tab.content",progress:{progress:".progress",fill:".progress .fill",details:".progress .details",icon:".progress .icon"},buttons:"nav.tabs button"},dependsOn:"[data-depends-on]",forms:{status:{status:".fstatus",message:".fstatus .message",icon:".fstatus .icon",actions:".fstatus .actions"}},inputs:this.inputSelectors,fields:{field:".field",label:"label",success:".success",error:".error",message:".validation-message"},repeater:{repeater:".repeater",header:".repeater-row-header",remove:".remove-row",add:".add-repeater-row",template:"template",items:".repeater-items",inputs:this.inputSelectors},tagList:{tagList:".field.tag-list",input:".row",add:".add-tag",remove:".remove-tag",label:".tag-label",items:".tag-items",item:".tag-item",inputs:this.inputSelectors,value:'input[type="hidden"]'},tag:{label:".tag-label"},number:{number:".field div.quantity",increase:"button.increase",decrease:"button.decrease",input:'input[type="number"]'},limits:{hasLimit:"[data-limit]",limit:".limit",current:".current"}}}initListeners(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.blurHandler=this.handleBlur.bind(this),this.inputHandler=this.handleInput.bind(this),this.submitHandler=this.handleSubmit.bind(this),this.quantityClick=this.handleQuantityClick.bind(this),this.repeaterClick=this.handleRepeaterClick.bind(this),this.tagListClick=this.handleTagListClick.bind(this),this.tagListInput=this.handleTagListInput.bind(this)}addFormListeners(e){e.addEventListener("click",this.clickHandler),e.addEventListener("change",this.changeHandler),e.addEventListener("input",this.inputHandler),e.addEventListener("blur",this.blurHandler),e.addEventListener("submit",this.submitHandler)}removeFormListeners(e){e.removeEventListener("click",this.clickHandler),e.removeEventListener("change",this.changeHandler),e.removeEventListener("input",this.inputHandler),e.removeEventListener("blur",this.blurHandler),e.removeEventListener("submit",this.submitHandler)}initStore(){const e=window.jvbStore.register("forms",{storeName:"forms",keyPath:"id",indexes:[{name:"src",keyPath:"src"},{name:"timestamp",keyPath:"timestamp"},{name:"formType",keyPath:"type"}],TTL:1008e4});this.store=e.forms,this.store.subscribe(((e,t)=>{if("data-ready"===e){let e=this.store.getFiltered().filter((e=>e.src===window.location.pathname));for(let t of e)this.showPendingNotification(t.id,t.changes)}else"operation-status"===e&&"completed"===t.status&&t.config&&this.store.delete(t.config.id)}))}showPendingNotification(e,t){let s=this.forms.get(e);if(!s)return;let i=s.element;if(!i)return void console.warn(`Form element not found for: ${e}`);const a=document.createElement("div");a.className="pendingChanges",a.innerHTML=`\n\t\t\t<p>We noticed unsaved changes from last time. Would you like to restore them?</p>\n        <button class="restore" type="button" data-form-id="${e}">Restore</button>\n        <button class="discard" type="button" data-form-id="${e}">Discard</button>`,i.insertBefore(a,s.ui.status.status),a.querySelector(".restore").addEventListener("click",(async()=>{this.isRestoring=!0;let e={fields:t};this.populate.populate(i,e),this.a11y.announce("Previous changes restored"),this.isRestoring=!1,a.remove()})),a.querySelector(".discard").addEventListener("click",(async()=>{await this.store.delete(e),this.a11y.announce("Previous changes discarded"),a.remove()}))}initValidators(){this.validators={email:{pattern:/^[^\s@]+@[^\s@]+\.[^\s@]+$/,message:"Please enter a valid email address"},url:{pattern:/^https?:\/\/.+\..+/,message:"Please enter a valid URL starting with https://"},phone:{pattern:/^[\d\s\-+().]+$/,message:"Please enter a valid phone number"},number:{test:(e,t)=>{const s=parseFloat(e);if(isNaN(s))return"Please enter a valid number";const i=t.dataset.min,a=t.dataset.max;return void 0!==i&&s<parseFloat(i)?`Value must be at least ${i}`:!(void 0!==a&&s>parseFloat(a))||`Value must be at most ${a}`}},text:{test:(e,t)=>{const s=t.dataset.minlength,i=t.dataset.maxlength;return s&&e.length<parseInt(s)?`Must be at least ${s} characters`:!(i&&e.length>parseInt(i))||`Must be no more than ${i} characters`}}}}validateField(e){const t=this.performValidation(e);return this.updateValidationUI(e,t),t.isValid}performValidation(e){const t=e.closest(".field"),s=this.getFieldCheckedValue(e);if(!s&&!e.required)return{isValid:!0,message:""};if(e.required)if("checkbox"===e.type){if(!e.checked)return{isValid:!1,message:"This field is required"}}else if("radio"===e.type){const t=document.querySelectorAll(`input[name="${e.name}"]`);if(!Array.from(t).some((e=>e.checked)))return{isValid:!1,message:"Please select an option"}}else if(!s)return{isValid:!1,message:"This field is required"};if(e.checkValidity&&!e.checkValidity())return{isValid:!1,message:e.validationMessage};if(s&&Object.hasOwn(t.dataset,"pattern")){if(!new RegExp(t.dataset.pattern).test(s))return{isValid:!1,message:t.dataset.validationMessage||"Invalid format"}}if(Object.hasOwn(t.dataset,"validate")||e.type){const i=this.validators[t.dataset.validate||e.type];if(i&&i.pattern&&!i.pattern.test(s))return{isValid:!1,message:i.message};if(i&&i.test){const e=i.test(s,t);if(!0!==e)return{isValid:!1,message:e}}}return{isValid:!0,message:""}}updateValidationUI(e,t){t.isValid?this.showSuccess(e,t.message):this.showError(e,t.message)}handleClick(e){let t=this.getForm(e.target);if(!t)return;const s=window.targetCheck(e,"[data-action]");if(s){switch(s.dataset.action){case"clear-form":this.store.delete(t.id),t.element.reset(),t.ui.status.status.hidden=!0,this.a11y.announce("Form cleared, starting fresh");break;case"dismiss-restore":t.ui.status.status.hidden=!0}}}handleChange(e){if(e.target.closest("[data-ignore]")||this.isRestoring)return;let t=this.getField(e.target);if(this.dependencies.has(t.dataset.field)){this.dependencies.get(t.dataset.field).items.forEach((e=>{this.checkFieldDependency(e,t.dataset.field)}))}if("repeater"===t.dataset.fieldType||"tag-list"===t.dataset.fieldType)return void this.updateCollectionField(t);let s=this.getForm(e.target);this.updateItem(t.dataset.field,this.getFieldValue(e.target),s)}handleBlur(e){if(e.target.closest("[data-ignore]")||this.isRestoring)return;let t=this.getForm(e.target);if(!t)return;let s=this.getField(e.target).dataset.field;window.debouncer.cancel(`form:${t.id}:validate:${s}`),this.validateField(e.target),this.updateItem(s,this.getFieldValue(e.target),t)}handleInput(e){let t=this.getForm(e.target);if(!t)return;let s=this.getField(e.target);if(!s)return;const i=e.target,a=s.dataset.field;this.showFormStatus(t.id,"pending"),window.debouncer.schedule(`form:${t.id}:validate:${a}`,(()=>this.validateField(i)),500)}async handleSubmit(e){let t=this.getForm(e.target);if(t){if(this.subscribers.size>0)if(e.preventDefault(),t.options.cache){this.cancelBackup(),await this.backup();const e=await this.store.get(t.id);this.notify("form-submit",{config:t,data:e.changes})}else this.notify("form-submit",{config:t,data:this.changes.get(t.id)?.changes??{}});if(t.options.showSummary){const e=await this.store.get(t.id);this.showSummary({config:t,changes:e?.changes})}}}updateItem(e,t,s){this.changes.has(s.id)||this.changes.set(s.id,{id:s.id,timestamp:Date.now(),src:window.location.pathname,changes:{}});let i=this.changes.get(s.id);i.changes[e]=t,this.changes.set(s.id,i),s.options.cache&&this.scheduleBackup()}scheduleBackup(){window.debouncer.schedule("form_changes",(async()=>{this.changes.size>0&&await this.backup()}),2e3)}cancelBackup(){window.debouncer.cancel("form_changes")}async backup(){const e=new Map;for(let[t,s]of this.changes.entries()){const i=await this.store.get(t);i?e.set(t,{...i,...s,changes:{...i.changes,...s.changes},timestamp:Date.now()}):e.set(t,s)}await this.store.saveMany(e);for(let e of this.changes.keys())this.showFormStatus(e,"autosaved");this.changes.clear()}saveCache(e){if(!this.changes.has(e))return;let t=this.changes.get(e);0!==t.size&&(this.store.save(t).then((()=>{})),this.changes.delete(e))}registerForm(e,t){if(Object.hasOwn(e.dataset,"formId")&&this.forms.has(e.dataset.formId))return;Object.hasOwn(e.dataset,"formId")||(e.dataset.formId=window.generateID("form_"));const s=e.dataset.formId;this.addFormListeners(e);const i={element:e,id:s,status:"",options:{autoUpload:t.autoUpload??!1,imageMeta:t.imageMeta??!0,delay:t.delay??1500,endpoint:t.save??e.dataset.save??"",showStatus:t.showStatus??!0,showSummary:t.showSummary??!1,cache:t.cache??!0,ignore:t.ignore??[]},ui:window.uiFromSelectors(this.selectors.forms,e)};return this.initializeFields(e,i),this.forms.set(s,i),i}clearForm(e){const t=this.forms.get(e);if(!t)return;t.unsubscribeTabs&&t.unsubscribeTabs(),t.tabs&&window.jvbTabs.removeTab(t.element),t.cache&&this.changes.has(e)&&this.saveCache(e);for(let[t,s]of this.inputs.entries())s.form===e&&this.inputs.delete(t);if(this.dependencies.forEach(((t,s)=>{t.items=t.items.filter((t=>t.form!==e)),0===t.items.length&&this.dependencies.delete(s)})),Object.hasOwn(t,"hasQuill")&&this.quillInstances.has(e)){this.quillInstances.get(e).forEach((e=>{e.disable(),e.off("text-change"),e.off("selection-change");const t=e.container.parentElement,s=t?.querySelector(".ql-toolbar");if(s&&s.remove(),e.setText(""),t&&t.classList.contains("editor-container")){const e=t.nextElementSibling;"TEXTAREA"===e?.tagName&&(e.style.display=""),t.remove()}})),this.quillInstances.delete(e)}let s={repeater:this.repeaters,tagList:this.tagLists,charLimit:this.charLimits,quantity:this.quantityFields};for(let[t,i]of Object.entries(s)){if(0===i.size)continue;let s=Array.from(i.values()).filter((t=>t.form===e));s.length>0&&s.forEach((e=>{switch(t){case"repeater":this.removeRepeaterListeners(e.element);break;case"tagList":this.removeTagListListeners(e.element);break;case"charLimit":this.removeCharacterLimitListeners(e.element);break;case"quantity":this.removeQuantityListeners(e.element)}i.has(e.id)&&i.delete(e.id)}))}this.removeFormListeners(t.element),this.forms.delete(e),window.debouncer.cancel("form_changes")}defineSummaryTemplate(){this.summaryTemplate=!0;let e=this;this.templates.define("formSummary",{refs:{result:".result",h3:"h3",p:"p"},setup({el:t,refs:s,manyRefs:i,data:a}){const r=["sendAll",...a.config.options.ignore??[]];for(let[i,n]of Object.entries(a.changes)){if(r.includes(i)||e.isEmptyValue(n))continue;let a=Array.from(e.inputs.values()).find((e=>e.field?.dataset.field===i));if(!a)continue;let l=s.result.cloneNode(!0),o=l.querySelector("h3"),d=l.querySelector("p");const c=a.field?.querySelector("legend");o.textContent=c?c.textContent.replace("*","").trim():a.ui.label?.textContent.replace("*","").trim();const u=e.formatValueForSummary(n,a);u instanceof HTMLElement?d.replaceWith(u):d.textContent=u,t.append(l)}let n=a.config?.element?.querySelectorAll("[data-upload-field]");n&&n.forEach((e=>{let i=e.querySelector("h2")?.textContent??"Upload:",a=e.querySelectorAll(".item-grid.preview img"),r=s.result.cloneNode(!0);if(a){let e=s.result.cloneNode(!0),n=r.querySelector("h3"),l=r.querySelector("p");l?.remove(),n&&(n.textContent=i),a.forEach((t=>{t=t.cloneNode(!0),e.append(t)})),t.append(e)}})),s.result?.remove(),a.config.element.after(t),window.fade(a.config.element,!1)}})}initializeFields(e,t=null){const s={"[data-editor]":()=>this.checkForQuill(e,t),"div.quantity":()=>this.checkForQuantity(e),".repeater":()=>this.checkForRepeaters(e,t),".field.tag-list":()=>this.checkForTagLists(e),"[data-depends-on]":()=>this.checkForConditionalFields(e),"[data-limit]":()=>this.checkForCharacterLimits(e),"[data-uploader],[data-upload-field]":()=>this.checkForImageUploads(e,t),"nav.tabs":()=>this.checkForTabs(e,t),'[data-type="selector"]':()=>this.checkForSelectors(e)};for(const[t,i]of Object.entries(s))e.querySelector(t)&&i();Array.from(e.querySelectorAll(this.inputSelectors)).map((e=>{this.getItem(e,t?.id)}))}checkForQuill(e,t){if(!e.querySelector("[data-editor]"))return;t&&!Object.hasOwn(t,"hasQuill")&&(t.hasQuill=!0,this.forms.set(t.id,t)),this.quillInstances.has(t.id)||this.quillInstances.set(t.id,new Set);window.jvbQuill(e).forEach((e=>{this.quillInstances.get(t.id).add(e)}))}checkForQuantity(e){e.querySelector(this.selectors.number.number)&&e.querySelectorAll(this.selectors.number.number).forEach((t=>{let s={id:window.generateID("quant"),form:e.dataset.formId,ui:window.uiFromSelectors(this.selectors.number,t),element:t};t.dataset.numId=s.id,this.quantityFields.set(s.id,s),this.addQuantityListeners(t)}))}addQuantityListeners(e){e.addEventListener("click",this.quantityClick)}removeQuantityListeners(e){e.removeEventListener("click",this.quantityClick)}handleQuantityClick(e){let t=this.quantityFields.get(e.target.closest("[data-num-id]")?.dataset.numId);if(!t)return;let s=0;if(t.ui.increase.contains(e.target)?s++:t.ui.decrease.contains(e.target)&&s--,0===s)return;this.getField(e.target);let i=t.ui.input.step;i=Math.max(i,1),e.ctrlKey&&e.shiftKey?i*=50:e.ctrlKey?i*=5:e.shiftKey&&(i*=10);let a=""===t.ui.input.value?0:parseFloat(t.ui.input.value);t.ui.input.value=a+i*s,a=parseFloat(t.ui.input.value),t.ui.input.min&&a<t.ui.input.min?(t.ui.input.value=t.ui.input.min,t.ui.decrease.disabled=!0):t.ui.input.max&&a>t.ui.input.max?(t.ui.input.value=t.ui.input.max,t.ui.increase.disabled=!0):(t.ui.decrease.disabled&&(t.ui.decrease.disabled=!1),t.ui.increase.disabled&&(t.ui.increase.disabled=!1))}checkForRepeaters(e){e.querySelector(this.selectors.repeater.repeater)&&e.querySelectorAll(this.selectors.repeater.repeater).forEach((t=>{let s={id:t.querySelector("template").className??window.generateID("repeater"),ui:window.uiFromSelectors(this.selectors.repeater,t),form:e.dataset.formId,element:t,field:this.getField(t),sortable:!1};if(!s.ui.add)return;let i=t.querySelector("template");this.templates.define(i.className,{manyRefs:{inputs:this.inputSelectors},setup({el:e,refs:t,manyRefs:i,data:a}){let r=s.ui.items?.children?.length??0;e.dataset.index=r,i.inputs?.forEach((t=>{window.prefixInput(t,`${a.repeater.dataset.field}:${r}:`,e)}))}}),window.Sortable&&(s.sortable=new Sortable(t,{handle:this.selectors.repeater.header,animation:150,onEnd:()=>{this.reindexList(t)}})),t.dataset.repeaterId=s.id,this.addRepeaterListeners(t),this.repeaters.set(s.id,s)}))}addRepeaterListeners(e){e.addEventListener("click",this.repeaterClick)}removeRepeaterListeners(e){e.removeEventListener("click",this.repeaterClick)}handleRepeaterClick(e){e.target.matches(this.selectors.repeater.add)?this.addRepeaterRow(e.target.closest("[data-repeater-id]")):e.target.matches(this.selectors.repeater.remove)&&this.removeRepeaterRow(e.target.closest("[data-index]"))}addRepeaterRow(e){let t={};t.repeater=e,e.append(this.templates.create(e.dataset.repeaterId,t)),this.initializeFields(e,this.getField(e).config??{}),this.a11y.announce("Row added")}removeRepeaterRow(e){let t=e.closest("[data-repeater-id]");e.remove(),this.reindexList(t),this.a11y.announce("Row removed")}checkForTagLists(e){e.querySelectorAll(this.selectors.tagList.tagList)?.forEach((t=>{let s={id:t.querySelector("template").className??window.generateID("tagList"),ui:window.uiFromSelectors(this.selectors.tagList,t),element:t,form:e.dataset.formId,format:t.dataset.tagFormat??"first_field"};if(!s.ui.input||!s.ui.add||!s.ui.items)return;t.dataset.tagListId=s.id,s.fieldName=t.dataset.field;let i=t.querySelector("template");this.templates.define(i.className,{refs:{label:this.selectors.tagList.label},manyRefs:{inputs:this.inputSelectors},setup({el:e,refs:t,manyRefs:i,data:a}){let r=s.ui.items?.children?.length??0;e.dataset.index=r,i.inputs?.forEach((e=>{let t=e.closest(".tag-item");window.prefixInput(e,`${a.fieldName}:${r}:`,t)})),t.label&&(t.label.textContent=a.label)}}),s.ui.inputs=Array.from(t.querySelectorAll(this.selectors.tagList.inputs)),s.ui.value=Array.from(t.querySelectorAll(this.selectors.tagList.value)),this.tagLists.set(s.id,s),this.addTagListListeners(t)}))}addTagListListeners(e){e.addEventListener("click",this.tagListClick),e.addEventListener("keypress",this.tagListInput,{passive:!0})}removeTagListListeners(e){e.removeEventListener("click",this.tagListClick),e.removeEventListener("keypress",this.tagListInput)}handleTagListClick(e){window.targetCheck(e,this.selectors.tagList.add)?this.addTagListItem(e.target.closest("[data-tag-list-id]")):window.targetCheck(e,this.selectors.tagList.remove)&&this.removeTagListItem(e.target.closest(this.selectors.tagList.item))}addTagListItem(e){let t=this.tagLists.get(e.dataset.tagListId);if(!t)return;let s,i={},a=!1,r=!0;for(let e of t.ui.inputs){const t=e.required||"true"===e.dataset.required,s=this.getFieldValue(e);s&&(a=!0);const n=this.validateField(e);t&&!s?(this.showError(e,"This field is required"),r=!1):n||(r=!1);const l=e.name.replace("new_","");i[l]=s}if(!r){this.a11y.announce("Please correct the errors before adding");const e=t.ui.inputs.find((e=>(e.required||"true"===e.dataset.required)&&!this.getFieldValue(e)));return void(e&&e.focus())}if(!a)return this.a11y.announce("Please fill in at least one field"),void t.ui.inputs[0].focus();switch(t.format){case"first_field":s=Object.values(i)[0];break;case"all_fields":s=Object.values(i).join(", ");break;default:if(t.format.includes("{")){s=t.format;for(const[e,t]of Object.entries(i))s=s.replace(`{${e}}`,t)}else s=i[t.format]??Object.values(i)[0]}let n=this.templates.create(e.dataset.tagListId,{label:s,fieldName:t.fieldName});const l=t.ui.items?.children?.length??0;n?.querySelectorAll("input[type=hidden]")?.forEach((e=>{const s=e.dataset.field;e.name=`${t.fieldName}:${l}:${s}`,e.id=`${t.fieldName}:${l}:${s}`,e.value=i[s]||""})),t.ui.items.append(n);for(let e of t.ui.inputs)["checkbox","radio"].includes(e.type)?e.checked=!1:e.value="",this.clearValidation(e);t.ui.inputs[0]?.focus(),this.updateCollectionField(e),this.a11y.announce("Item added")}removeTagListItem(e){let t=e.closest("[data-tag-list-id]");t&&(e.remove(),this.reindexList(t),this.updateCollectionField(t),this.a11y.announce("Item removed"))}handleTagListInput(e){let t=e.target,s=t.closest("[data-tag-list-id]");if(!s)return;let i=this.tagLists.get(s.dataset.tagListId);if(i&&"Enter"===e.key)if(t===i.ui.inputs[i.ui.inputs.length-1])e.preventDefault(),this.addTagListItem(t.closest("[data-tag-list-id]"));else{e.preventDefault();let s=i.ui.inputs.indexOf(t);i.ui.inputs[s+1].focus()}}checkForConditionalFields(e){e.querySelectorAll(this.selectors.dependsOn).forEach((t=>{const s=t.dataset.dependsOn,i=t.dataset.dependsValue,a=t.dataset.dependsOperatior??"==";if(!this.dependencies.has(s)){let e=document.querySelector(`[field="${s}"]`);e&&this.dependencies.set(s,{element:e,items:[]})}let r=this.dependencies.get(s);r.items.push({field:t,form:e.dataset.formId,requiredValue:i,operator:a}),this.dependencies.set(s,r),this.checkFieldDependency(r,s)}))}checkFieldDependency(e,t){const s=this.dependencies.get(t);if(!s)return;const i=this.getFieldCheckedValue(s.element),a=this.evaluateCondition(i,e.requiredValue,e.operator);this.toggleFieldVisibility(e.field,a)}evaluateCondition(e,t,s){const i=String(e||""),a=String(t||"");switch(s){case"==":default:return i===a;case"!=":return i!==a;case">":return parseFloat(i)>parseFloat(a);case"<":return parseFloat(i)<parseFloat(a);case">=":return parseFloat(i)>=parseFloat(a);case"<=":return parseFloat(i)<=parseFloat(a);case"contains":return i.includes(a);case"empty":return""===i;case"not_empty":return""!==i}}toggleFieldVisibility(e,t){const s=e.closest(".field, fieldset");s&&(s.hidden=!t,s.querySelectorAll("input, select, textarea").forEach((e=>{e.disabled=!t,!t&&e.hasAttribute("required")?(e.dataset.wasRequired="true",e.removeAttribute("required")):t&&"true"===e.dataset.wasRequired&&(e.setAttribute("required",""),delete e.dataset.wasRequired)})))}checkForCharacterLimits(e){e.querySelector(this.selectors.limits.hasLimit)&&(this.countUpdaters=this.updateCount.bind(this),e.querySelectorAll(`${this.selectors.limits.hasLimit}`).forEach((t=>{let s=window.generateID("limit");t.dataset.charLimitId=s;let i={element:t,form:e.dataset.formId,ui:window.uiFromSelectors(this.selectors.limits,t.closest(".field"))};i.ui.limit.textContent=t.dataset.limit,this.charLimits.set(s,i),this.addCharacterLimitListeners(t)})))}addCharacterLimitListeners(e){e.addEventListener("input",this.countUpdaters,{passive:!0})}removeCharacterLimitListeners(e){e.removeEventListener("input",this.countUpdaters,{passive:!0})}updateCount(e){let t=e.target,s=this.charLimits.get(t.dataset.charLimitId);if(!s)return;let i=t.value.length,a=t.dataset.limit;s.ui.current&&(s.ui.current.textContent=i,s.ui.current.classList.toggle("exceeded",i>=a)),i>a&&(t.value=t.value.slice(0,a))}checkForImageUploads(e,t){window.jvbUploads.scanFields(e,t.options.autoUpload,t.options.imageMeta)}checkForTabs(e,t){window.jvbTabs&&e.querySelector("nav.tabs")&&(t.tabs=window.jvbTabs.registerTab(e,{preCheck:(e,s)=>this.validateStep(e,t)}),t.ui.tabs=window.uiFromSelectors(this.selectors.tabs,e),t.ui.tabs.sections=Array.from(e.querySelectorAll(this.selectors.tabs.sections)),t.ui.tabs.inputs={},t.ui.tabs.sections.forEach((e=>{t.ui.tabs.inputs[e.dataset.tab]=Array.from(e.querySelectorAll(this.inputs))})),t.ui.tabs.buttons=Array.from(e.querySelectorAll(this.selectors.tabs.buttons)),t.unsubscribeTabs=window.jvbTabs.subscribe(((e,s)=>{if("tab-switched"===e&&t.ui.tabs.progress){const e=t.ui.tabs.sections.filter((e=>e.dataset.tab===s.current))[0]??!1;if(!e)return;const i=e.dataset.step,a=t.ui.sections.length;window.showProgress(t.ui.tabs.progress,i,a)}})),this.forms.set(t.id,t))}validateStep(e,t){const s=e.closest("[data-form-id]")?.dataset.formId;if(!s)return!0;if(!this.forms.get(s))return!0;return Array.from(this.inputs.values()).filter((t=>t&&t.form===s&&t.section===e.dataset.tab&&!t.element.closest("[hidden]"))).every((e=>!0===this.validateField(e.element)))}checkForSelectors(e){window.jvbSelector&&window.jvbSelector.scanExistingFields(e)}reindexList(e){const t=e.dataset.field||e.dataset.repeaterId||e.dataset.tagListId;Array.from(e.children).forEach(((e,s)=>{e.dataset.index=`${s}`;e.querySelectorAll("input, select, textarea").forEach((i=>{if("file"===i.type)return;i.dataset.field||i.name.split(":").pop();window.prefixInput(i,`${t}:${s}:`,e)}))})),this.updateCollectionField(e)}updateCollectionField(e){const t=e.closest("[data-field]");if(!t)return;const s=t.dataset.fieldType;if(!["repeater","tag-list"].includes(s))return;const i=this.getForm(e);if(!i)return;const a=this.getFieldValue(t.querySelector("input, select, textarea"));this.updateItem(t.dataset.field,a,i)}clearValidation(e){let t=this.getField(e);if(!t)return;let s=this.getItem(e);s&&(t.classList.remove("has-error","has-success"),s.ui.success&&(s.ui.success.hidden=!0),s.ui.error&&(s.ui.error.hidden=!0),s.ui.message&&(s.ui.message.hidden=!0,s.ui.message.textContent=""))}showError(e,t="Invalid field"){let s=this.getField(e);if(!s)return;let i=this.getItem(e);i&&(s.classList.remove("has-success"),s.classList.add("has-error"),i.ui.success&&(i.ui.success.hidden=!0),i.ui.error&&(i.ui.error.hidden=!0),i.ui.message&&(i.ui.message.hidden=!1,i.ui.message.textContent=t))}showSuccess(e,t=""){let s=this.getField(e);if(!s)return;let i=this.getItem(e);i&&(s.classList.remove("has-error"),s.classList.add("has-success"),i.ui.success&&(i.ui.success.hidden=!1),i.ui.error&&(i.ui.error.hidden=!0),i.ui.message&&(i.ui.message.hidden=""===t,i.ui.message.textContent=t))}handleFormSuccess(e,t){if(e.querySelectorAll(".error-message").forEach((e=>e.remove())),e.querySelectorAll(".field-error").forEach((e=>e.classList.remove("field-error"))),e.classList.add("form-success"),t.message){const s=document.createElement("div");s.className="form-success-message success-message",s.textContent=t.message,e.insertBefore(s,e.firstChild);const i=window.getIcon?.("check-circle");i&&(i.classList.add("success-icon"),s.prepend(i))}if(t.title||t.description){const s=document.createElement("div");if(s.className="success-box",t.title){const e=document.createElement("h3");e.textContent=t.title,s.appendChild(e)}if(t.description){(Array.isArray(t.description)?t.description:[t.description]).forEach((e=>{const t=document.createElement("p");t.textContent=e,s.appendChild(t)}))}e.insertBefore(s,e.firstChild)}if(e.dataset.formId){this.store.delete(e.dataset.formId).catch((e=>{console.warn("Failed to clear form cache:",e)}));const t=this.forms.get(e.dataset.formId);t&&(t.isDirty=!1,t.lastSaved=Date.now(),t.data={})}window.jvbA11y&&window.jvbA11y.announce(t.message||"Form submitted successfully")}handleFormError(e,t){if(e.querySelectorAll(".error-message").forEach((e=>e.remove())),e.querySelectorAll(".field-error, .has-error").forEach((e=>{e.classList.remove("field-error","has-error")})),e.querySelectorAll(".field").forEach((e=>{this.clearValidation(e)})),t.field){const s=e.querySelector(`[data-field="${t.field}"]`);if(s){this.showError(s,t.message),s.scrollIntoView({behavior:"smooth",block:"center"});const e=s.querySelector("input, textarea, select");e&&e.focus()}}else{const s=document.createElement("div");s.className="form-error error-message",s.textContent=t.message;const i=window.getIcon?.("close-circle");i&&(i.classList.add("error-icon"),s.prepend(i)),e.insertBefore(s,e.firstChild),e.scrollIntoView({behavior:"smooth",block:"start"})}if(window.jvbA11y){const e=t.field?`Error in ${t.field}: ${t.message}`:`Form error: ${t.message}`;window.jvbA11y.announce(e)}e.dispatchEvent(new CustomEvent("jvb-form-error",{detail:t}))}showFormStatus(e,t,s=""){let i=this.forms.get(e);i&&i.options.showStatus&&i.ui?.status?.status&&i.status!==t&&(i.status=t,i.ui.status.status.hidden=!1,i.ui.status.status.classList.toggle("loading",["uploading","saving"].includes(t)),i.ui.status.message.textContent=""===s?this.getDefaultMessage(t):s,i.ui.status.icon.className="icon icon-"+this.getDefaultIcon(t),setTimeout((()=>i.ui.status.status.hidden=!0),"submitted"===t?3e3:1e4))}getDefaultMessage(e){return{saving:"Saving changes...",autosaved:"Changes saved locally. Submit form to send to server.",uploading:"Uploading your form to server",submitted:"Successfully sent to server",pending:"Unsaved changes",restored:"Welcome back! We've restored your previous entry.",error:"Failed to save changes. Refresh and try again?",offline:"Changes will be saved when online"}[e]??e}getDefaultIcon(e){return{autosaved:"check-circle",submitted:"check-circle",restored:"history",error:"close-circle",offline:"cloud-slash",pending:"exclamation-mark"}[e]??""}showSummary(e){let t=this.templates.create("formSummary",e);e.config.element.after(t),window.fade(e.config.element,!1)}getForm(e){let t=e.closest("[data-form-id]");if(!t)return!1;let s=t.dataset.formId;if(!s)return!1;let i=this.forms.get(s);return i||!1}getField(e){return e.closest("[data-field]")}getFieldType(e){let t=this.getField(e);if(t)return t.dataset.fieldType}getFieldValue(e){let t=this.getFieldType(e),s=this.getItem(e),i=s.field?.dataset.field??!1;if(!i)return!1;switch(t){case"repeater":return this.getRepeaterValue(e,s);case"tag-list":return this.getTagListValue(e,s);case"group":break;case"location":return this.getLocationValue(e,s);case"selector":case"upload":return this.getHiddenInputValue(e,s,i);case"true-false":return"1"===e.value||"on"===e.value||"true"===e.value;case"checkbox":return e.name.endsWith("[]")?this.getCheckboxGroupValue(e,s):e.checked?e.value:"";default:return e.value}}getCheckboxGroupValue(e,t){return t.checkboxGroup||(t.checkboxGroup=t.field?.querySelectorAll(`input[type="checkbox"][name="${e.name}"]`),this.saveItem(t)),Array.from(t.checkboxGroup).filter((e=>e.checked)).map((e=>e.value))}getFieldCheckedValue(e){if("checkbox"===e.type){return"true-false"===this.getFieldType(e)?e.checked:e.checked?e.value:""}if("radio"===e.type){const t=document.querySelectorAll(`input[name="${e.name}"]`),s=Array.from(t).find((e=>e.checked));return s?s.value:""}return this.getFieldValue(e)}isEmptyValue(e){return null==e||""===e||(!(!Array.isArray(e)||0!==e.length)||"object"==typeof e&&0===Object.keys(e).length)}getRepeaterValue(e,t){t.container||(t.container=t.field?.querySelector(".repeater-items"),this.saveItem(t));let s=[];return Array.from(t.container.children).forEach((e=>{let t={};e.querySelectorAll("[data-field]").forEach((e=>{t[e.dataset.field]=this.getFieldValue(e)})),s.push(t)})),s}getTagListValue(e,t){t.container||(t.container=t.field?.querySelector(".tag-items"),this.saveItem(t));let s=[];return Array.from(t.container.children).forEach((e=>{let t=e.querySelectorAll('input[type="hidden"]'),i={};t.forEach((e=>{i[e.dataset.field]=e.value})),s.push(i)})),s}getLocationValue(e,t){t.values||(t.values=Array.from(t.field?.querySelectorAll("[data-location-field]")),this.saveItem(t));let s={};return t.values.forEach((e=>{s[e.dataset.locationField]=e.value})),s}getHiddenInputValue(e,t,s){return t.value||(t.value=t.field?.querySelector(`input[type=hidden][name="${s}"]`),this.saveItem(t)),t.value.value}formatValueForSummary(e,t){const s=this.getFieldType(t.element);if(this.isEmptyValue(e))return"";switch(s){case"repeater":return this.formatRepeaterForSummary(e,t);case"tag-list":return this.formatTagListForSummary(e,t);case"location":return this.formatLocationForSummary(e);case"true-false":return e?"Yes":"No";case"checkbox":return Array.isArray(e)?this.formatCheckboxGroupForSummary(e,t):this.getDisplayLabel(t,e);case"selector":case"upload":return this.formatHiddenFieldForSummary(e,t,s);default:return"string"==typeof e?this.getDisplayLabel(t,e):"string"==typeof e&&e.includes("\n")?this.convertLineBreaks(e):e}}formatCheckboxGroupForSummary(e,t){return e.map((e=>this.getDisplayLabel(t,e))).join(", ")}convertLineBreaks(e){const t=document.createElement("span");return t.innerHTML=e.split("\n").join("<br>"),t}formatRepeaterForSummary(e,t){const s=document.createElement("div");return s.className="summary-repeater",e.forEach(((e,i)=>{const a=document.createElement("div");a.className="summary-repeater-row";const r=document.createElement("strong");r.textContent=`Entry ${i+1}:`,a.appendChild(r);const n=document.createElement("ul");n.className="summary-repeater-fields";for(const[s,i]of Object.entries(e)){if(this.isEmptyValue(i))continue;const e=document.createElement("li"),a=t.field?.querySelector(`[data-field="${s}"]`),r=a?.closest(".field")?.querySelector("label")?.textContent.replace("*","").trim()||s;e.innerHTML=`<span class="field-label">${r}:</span> <span class="field-value">${i}</span>`,n.appendChild(e)}a.appendChild(n),s.appendChild(a)})),s}formatTagListForSummary(e,t){const s=document.createElement("div");s.className="summary-taglist";const i=document.createElement("ul");return i.className="summary-tags",e.forEach((e=>{const t=document.createElement("li");t.className="summary-tag";const s=Object.values(e).find((e=>!this.isEmptyValue(e)))||"",a=Object.entries(e).filter((([e,t])=>!this.isEmptyValue(t)));a.length>1?t.textContent=a.map((([e,t])=>t)).join(", "):t.textContent=s,i.appendChild(t)})),s.appendChild(i),s}formatLocationForSummary(e){const t=[];return e.street&&t.push(e.street),e.city&&t.push(e.city),e.province&&t.push(e.province),e.postal_code&&t.push(e.postal_code),e.country&&t.push(e.country),t.length>0?t.join(", "):e.address||""}formatHiddenFieldForSummary(e,t,s){if("upload"===s){const s=t.field?.querySelector("[data-upload-field]");if(s){const e=s.querySelectorAll(".item-grid.preview img");if(e.length>0){const t=document.createElement("div");return t.className="summary-uploads",e.forEach((e=>{const s=e.cloneNode(!0);s.style.maxWidth="100px",s.style.maxHeight="100px",t.appendChild(s)})),t}}return`${e.split(",").length} file(s) uploaded`}return e}getDisplayLabel(e,t){if(!e.element)return t;const s=e.element.type;if("radio"===s){const s=e.field.querySelectorAll(`input[type="radio"][name="${e.element.name}"]`),i=Array.from(s).find((e=>e.value===t));if(i){const t=i.closest("label")||e.field.querySelector(`label[for="${i.id}"]`);if(t)return t.textContent.replace("*","").trim()}}if("checkbox"===s&&"true-false"!==this.getFieldType(e.element)){const s=e.field.querySelector(`input[type="checkbox"][value="${t}"]`);if(s){const t=s.closest("label")||e.field.querySelector(`label[for="${s.id}"]`);if(t){const e=t.querySelector("span");return e?e.textContent.trim():t.textContent.replace("*","").trim()}}}return t}getItem(e,t=null){const s=Object.hasOwn(e.dataset,"ref");let i=s?e.dataset.ref:window.generateID("input");if(s||(e.dataset.ref=i),!this.inputs.has(i)){t||(t=e.closest("[data-form-id]")?.dataset.formId??!1);let s=this.getField(e);this.inputs.set(i,{id:i,element:e,form:t,field:s,section:e.closest("[data-tab]")?.dataset.tab??!1,ui:window.uiFromSelectors(this.selectors.fields,s)})}return this.inputs.get(i)}saveItem(e){this.inputs.set(e.id,e)}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((s=>{try{s(e,t)}catch(e){console.error("HandleSelection subscriber error:",e)}}))}destroy(){this.forms.size>0&&(Array.from(this.forms.values()).forEach((e=>{this.removeFormListeners(e)})),this.forms.clear()),this.repeaters.size>0&&(Array.from(this.repeaters.values()).forEach((e=>{this.removeRepeaterListeners(e.element),e.sortable?.destroy()})),this.repeaters.clear()),this.quantityFields.size>0&&(Array.from(this.quantityFields.values()).forEach((e=>{this.removeQuantityListeners(e.element)})),this.quantityFields.clear()),this.tagLists.size>0&&(Array.from(this.tagLists.values()).forEach((e=>{this.removeTagListListeners(e.element)})),this.tagLists.clear()),this.charLimits.size>0&&Array.from(this.charLimits.values()).forEach((e=>{e.removeEventListener("input",this.countUpdaters)})),this.inputs.clear(),this.forms.clear(),this.charLimits.clear()}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbForm=new e)}))}))})();
\ No newline at end of file
diff --git a/assets/js/min/helcim.min.js b/assets/js/min/helcim.min.js
new file mode 100644
index 0000000..27ef5ba
--- /dev/null
+++ b/assets/js/min/helcim.min.js
@@ -0,0 +1 @@
+(()=>{class e extends window.jvbCheckout{constructor(e={}){super({...window.helcimConfig,...e}),this.pendingSecretToken=null}async init(){"function"!=typeof window.appendHelcimPayIframe&&console.warn("HelcimPay.js SDK not loaded — payment will initialize on first checkout"),this.isInitialized=!0,window.addEventListener("message",(e=>this.handleHelcimMessage(e))),document.dispatchEvent(new CustomEvent("checkoutReady",{detail:{checkout:this,provider:"helcim"}}))}async processPayment(e){if(this.selectedCardId)return this.submitToServer({card_id:this.selectedCardId,is_saved:!0},e);const t=await this.initializeCheckoutSession(e);if(!t.success)throw new Error(t.message||"Failed to initialize checkout");return this.pendingSecretToken=t.secretToken,this.pendingOrderData=e,window.appendHelcimPayIframe(t.checkoutToken,{type:"modal"}),new Promise(((e,t)=>{this._paymentResolve=e,this._paymentReject=t}))}async initializeCheckoutSession(e){return(await fetch(this.config.api_url+"initialize-checkout",{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":this.config.nonce},body:JSON.stringify({amount:e.total/100,customer:e.customer,items:e.items,cart_id:this.getCartId()})})).json()}handleHelcimMessage(e){const t=e.data;t&&"object"==typeof t&&("SUCCESS"===t.eventStatus?this.handleHelcimSuccess(t):"ABORTED"===t.eventStatus?this.handleHelcimCancelled():"FAILED"===t.eventStatus&&this.handleHelcimError(t))}async handleHelcimSuccess(e){try{const t=await this.submitToServer({transaction_id:e.transactionId,secret_token:this.pendingSecretToken,event_data:e},this.pendingOrderData);this.clearPending(),this._paymentResolve?.(t)}catch(e){this.clearPending(),this._paymentReject?.(e)}}handleHelcimCancelled(){this.clearPending(),window.jvbLoading?.hideLoading?.(),this.a11y.announce("Payment cancelled"),this._paymentReject?.(new Error("Payment cancelled by user"))}handleHelcimError(e){this.clearPending(),window.jvbLoading?.hideLoading?.();const t=e.errorMessage||"Payment failed";this._paymentReject?.(new Error(t))}clearPending(){this.pendingSecretToken=null,this.pendingOrderData=null}async submitToServer(e,t){if(!this.isOpen)throw new Error("Store is currently closed");const i=e.is_saved?"process-saved-payment":"validate-transaction",n=await fetch(this.config.api_url+i,{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":this.config.nonce},body:JSON.stringify({...e,cart_id:this.getCartId(),amount:t.total,items:t.items,customer:{email:this.isLoggedIn?this.userEmail:t.customer.email,name:t.customer.name,phone:t.customer.phone},note:t.note,pickup_time:t.pickup_time})}),s=await n.json();if(!n.ok)throw new Error(s.message||"Payment processing failed");return this.clearCart(),s}async loadSavedCards(){try{const e=await fetch(this.config.api_url+"saved-cards",{method:"GET",headers:{"X-WP-Nonce":this.config.nonce}}),t=await e.json();t.success&&t.cards&&(this.savedCards=t.cards,this.renderSavedCards())}catch(e){console.error("Failed to load saved cards:",e)}}async loadInvoices(){try{const e=await fetch(this.config.api_url+"invoices",{headers:{"X-WP-Nonce":this.config.nonce}}),t=await e.json();if(t.success)return t.invoices||[]}catch(e){console.error("Failed to load invoices:",e)}return[]}async payInvoice(e){const t=await fetch(this.config.api_url+"initialize-checkout",{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":this.config.nonce},body:JSON.stringify({invoice_id:e})}).then((e=>e.json()));if(!t.success)throw new Error(t.message||"Failed to initialize invoice payment");return this.pendingSecretToken=t.secretToken,this.pendingOrderData={total:0,items:[],customer:{}},window.appendHelcimPayIframe(t.checkoutToken,{type:"modal"}),new Promise(((e,t)=>{this._paymentResolve=e,this._paymentReject=t}))}}document.addEventListener("DOMContentLoaded",(()=>{document.querySelector('#checkout[data-provider="helcim"]')&&(window.jvbHelcim=new e)}))})();
\ No newline at end of file
diff --git a/assets/js/min/populate.min.js b/assets/js/min/populate.min.js
index 250edba..5cc75f4 100644
--- a/assets/js/min/populate.min.js
+++ b/assets/js/min/populate.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(){this.templates=window.jvbTemplates,this.formHelper=window.jvbForm,this.defineTemplates(),this.data=null,this.form=null}populate(e,t={}){if(this.data=t,this.mergeRootData(),this.form=e,this.formHelper||(this.formHelper=window.jvbForm),this.formHelper){if(Object.hasOwn(this.data,"fields")&&0!==Object.keys(this.data.fields).length)for(let[t,i]of Object.entries(this.data.fields)){let a=e.querySelector(`[data-field="${t}"]`);a&&this.populateField(a,t,i)}}else requestAnimationFrame((()=>{this.populate(e,t)}))}mergeRootData(){["status","date","modified"].forEach((e=>{this.data.fields[`post_${e}`]=this.data[e]}))}populateField(e,t,i){let a=this.formHelper.getFieldType(e);if(!a||this.isEmptyValue(t)||this.isEmptyValue(i))return;const l={repeater:this.populateRepeater.bind(this),"tag-list":this.populateTagList.bind(this),location:this.populateLocation.bind(this),selector:this.populateTaxonomy.bind(this),user:this.populateUser.bind(this),upload:this.populateUpload.bind(this),set:this.populateMultiValue.bind(this),checkbox:this.populateMultiValue.bind(this),select:this.populateSingleValue.bind(this),radio:this.populateSingleValue.bind(this),"true-false":this.populateBoolean.bind(this),date:this.populateDate.bind(this),time:this.populateDate.bind(this),datetime:this.populateDate.bind(this),number:this.populateNumber.bind(this),textarea:this.populateTextarea.bind(this)};Object.hasOwn(l,a)?l[a](e,t,i):this.populateText(e,t,i)}populateRepeater(e,t,i){if(!i||!Array.isArray(i))return;const a=e.querySelector(".repeater-items");let l=e.querySelector("template")?.className??!1;a&&l&&(window.removeChildren(a),i.forEach(((e,t)=>{e.index=t;const i=this.templates.create(l,e);let o=i.querySelectorAll(".field");this.populate(o,e),a.append(i)})))}populateTagList(e,t,i){if(!i||!Array.isArray(i))return;const a=e.querySelector(".tag-items");let l=e.querySelector("template")?.className??!1;a&&l&&(window.removeChildren(a),i.forEach(((e,t)=>{e.index=t;const i=this.templates.create(l,e);let o=i.querySelectorAll(".field");this.populate(o,e),a.append(i)})))}populateLocation(e,t,i){["address","lat","lng","street","city","province","postal_code","country"].forEach((t=>{if(Object.hasOwn(i,t)){let a=e.querySelector(`[data-location-field="${t}"]`);a&&(a.value=String(i[t]||""))}}))}populateTaxonomy(e,t,i){let a=this.splitIDs(i);if(0===a.length)return;const l=e.querySelector(`input[type="hidden"][name="${t}"]`);l&&(l.value=a.join(","),window.jvbSelector&&requestAnimationFrame((()=>{window.jvbSelector.updateFieldFromInput(l)})))}populateUser(e,t,i){this.populateTaxonomy(e,t,i)}populateUpload(e,t,i){if("timeline"===t||e.dataset.subtype&&"timeline"===e.dataset.subtype)return void this.populateTimelineGallery(e,t,i);if(this.isEmptyValue(i))return;const a=this.splitIDs(i);if(0===a.length)return;const l=e.querySelector('input[type="hidden"]');l&&(l.value=a.join(","));const o=e.querySelector(".item-grid");e.querySelector(".file-upload-container").hidden=a.length>0,e.querySelector(".progress")?.remove(),o&&(window.removeChildren(o),a.forEach((e=>{let t=this.data.images[e]??{};t.field={config:{showMeta:!0}},t.id=e,o.append(this.templates.create("uploadItem",t))}))),this.populateUploadMeta(e,t,i)}populateUploadMeta(e,t,i){const a=e.querySelector('[data-field="image_data"]');if(!a)return;let l=this.data.images[i]??!1;if(!l)return;a.dataset.attachmentId=l.id,a.setAttribute("data-ignore","");const o=["image-title","image-alt-text","image-caption"];for(const e of o){const t=a.querySelector(`[data-field="${e}"] input, [data-field="${e}"] textarea`);t&&""!==l[e]&&(t.value=l[e])}}populateTimelineGallery(e,t,i){if(!i||!Array.isArray(i)||0===i.length)return;let a=e.querySelector(".item-grid");if(e.querySelector(".file-upload-container").hidden=i.length>0,a){window.removeChildren(a),e.querySelector(".progress")?.remove();for(let e of i){let t=this.templates.create("timelineItem",e);t&&a.append(t)}}}populateMultiValue(e,t,i){if("string"==typeof i)try{i=JSON.parse(i)}catch(e){i=i.split(",").map((e=>e.trim()))}Array.isArray(i)||(i=[String(i)]);let a=e.querySelector(`select[name="${t}"]`);if(a&&a.multiple)for(let e of a.options)e.selected=i.includes(e.value);else e.querySelectorAll(`[type="checkbox"][name=${t}]`).forEach((e=>{e.checked=i.includes(e.value)}))}populateSingleValue(e,t,i){i=String(i||"");let a=e.querySelector(`select[name="${t}"]`);if(a)return void(a.value=i);let l=e.querySelector(`[name="${t}"][value="${i}"]`);l&&(l.checked=!0)}populateBoolean(e,t,i){const a=e.querySelector(`[name="${t}"], input[type="checkbox"]`);a&&(a.checked=Boolean(i))}populateDate(e,t,i){const a=e.querySelector(`[name="${t}"], input`);if(a){"object"==typeof i&&Object.hasOwn(i,"date")&&(i=i.date);try{const e=new Date(i);if(!isNaN(e.getTime()))switch(a.type){case"date":a.value=e.toISOString().split("T")[0];break;case"time":a.value=e.toTimeString().slice(0,5);break;case"datetime-local":a.value=e.toISOString().slice(0,16);break;default:a.value=i}}catch(e){a.value=i}}}populateNumber(e,t,i){const a=e.querySelector(`[name="${t}"], input[type="number"]`);a&&(a.value=Number(i)||0)}populateTextarea(e,t,i){let a=e.querySelector("textarea");a.dataset.editor?(a.value=String(i||""),a.dispatchEvent(new Event("change",{bubbles:!0}))):this.populateText(e,t,i)}populateText(e,t,i){let a=e.querySelector(`[name="${t}"], input, textarea`);a&&"file"!==a.type&&(a.value=String(i||""))}getFormHelper(){window.requestAnimationFrame((()=>{this.formHelper=window.jvbForm}))}splitIDs(e){return String(e).split(",").map((e=>parseInt(e.trim()))).filter((e=>!isNaN(e)&&e>0))}isEmptyValue(e){return null==e||""===e||(!(!Array.isArray(e)||0!==e.length)||"object"==typeof e&&0===Object.keys(e).length)}defineTemplates(){const e=this.templates,t=this;e.define("timelineItem",{refs:{select:'[name="select-item"]',video:"video",file:".select-item span",img:"img",details:"details[data-field]",imgAlt:'[name="image-alt-text"]',imgTitle:'[name="image-title"]',imgDesc:'[name="image-caption"]'},manyRefs:{fields:".field"},setup({el:e,refs:i,manyRefs:a,data:l}){if(e.dataset.itemId=l.id,i.select){let e=i.select.closest(".preview");window.prefixInput(i.select,`${l.id}-`,e)}i.video&&i.video.remove(),i.file&&i.file.remove();let o=t.data.images[l.post_thumbnail]??!1;if(i.img&&o&&(i.img.src=o.medium||o.small||o.large||"",i.img.title=o["image-title"]??"",i.img.alt=o["image-alt-text"]??""),i.details){let e=t.data.images[l.post_thumbnail];i.details.setAttribute("data-ignore",""),i.details.dataset.attachmentId=l.post_thumbnail,Object.hasOwn(e,"image-alt-text")&&i.alt&&(i.alt.value=e["image-alt-text"]),(Object.hasOwn(e,"image-title")||Object.hasOwn(l,"file"))&&i.title&&(i.title.value=e["image-title"]||l.file.name),Object.hasOwn(e,"image-caption")&&i.description&&(i.description.value=e["image-caption"])}if(a.fields)for(let e of a.fields){if("group"===e.dataset.fieldType)continue;if("post_thumbnail"===e.dataset.field){e.remove();continue}let i=e.dataset.field,a=l[i]??"";t.isEmptyValue(a)||t.populateField(e,i,a);const o=e.querySelector('input:not([type="file"])');o&&window.prefixInput(o,`[${l.id}]`,e)}}})}}document.addEventListener("DOMContentLoaded",(function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbPopulate=new e)}))}))})();
\ No newline at end of file
+(()=>{class e{constructor(){this.templates=window.jvbTemplates,this.formHelper=window.jvbForm,this.defineTemplates(),this.data=null,this.form=null}populate(e,t={}){if(this.data=t,this.mergeRootData(),this.form=e,this.formHelper||(this.formHelper=window.jvbForm),this.formHelper){if(Object.hasOwn(this.data,"fields")&&0!==Object.keys(this.data.fields).length)for(let[t,i]of Object.entries(this.data.fields)){let a=e.querySelector(`[data-field="${t}"]`);a&&this.populateField(a,t,i)}}else requestAnimationFrame((()=>{this.populate(e,t)}))}mergeRootData(){["status","date","modified"].forEach((e=>{this.data.fields[`post_${e}`]=this.data[e]}))}populateField(e,t,i){let a=this.formHelper.getFieldType(e);if(!a||this.isEmptyValue(t)||this.isEmptyValue(i))return;const l={repeater:this.populateRepeater.bind(this),"tag-list":this.populateTagList.bind(this),location:this.populateLocation.bind(this),selector:this.populateTaxonomy.bind(this),user:this.populateUser.bind(this),upload:this.populateUpload.bind(this),set:this.populateMultiValue.bind(this),checkbox:this.populateMultiValue.bind(this),select:this.populateSingleValue.bind(this),radio:this.populateSingleValue.bind(this),"true-false":this.populateBoolean.bind(this),date:this.populateDate.bind(this),time:this.populateDate.bind(this),datetime:this.populateDate.bind(this),number:this.populateNumber.bind(this),textarea:this.populateTextarea.bind(this)};Object.hasOwn(l,a)?l[a](e,t,i):this.populateText(e,t,i)}populateRepeater(e,t,i){if(!i||!Array.isArray(i))return;const a=e.querySelector(".repeater-items");let l=e.querySelector("template")?.className??!1;a&&l&&(window.removeChildren(a),i.forEach(((e,t)=>{e.index=t;const i=this.templates.create(l,e);if(i){for(let[t,a]of Object.entries(e)){if("index"===t)continue;let e=i.querySelector(`[data-field="${t}"]`);e&&this.populateField(e,t,a)}a.append(i)}})))}populateTagList(e,t,i){if(!i||!Array.isArray(i))return;const a=e.querySelector(".tag-items");let l=e.querySelector("template")?.className??!1;a&&l&&(window.removeChildren(a),i.forEach(((i,s)=>{const r=this.templates.create(l,{label:this.getTagLabel(i,e.dataset.tagFormat??"first_field"),fieldName:t,...i});r&&(r.querySelectorAll('input[type="hidden"]').forEach((e=>{const t=e.dataset.field;t&&void 0!==i[t]&&(e.value=i[t])})),a.append(r))})))}getTagLabel(e,t){const i=Object.values(e).filter((e=>!this.isEmptyValue(e)));switch(t){case"first_field":return i[0]??"New Item";case"all_fields":return i.join(", ")||"New Item";default:if(t.includes("{")){let i=t;for(const[t,a]of Object.entries(e))i=i.replace(`{${t}}`,a);return i}return e[t]??i[0]??"New Item"}}populateLocation(e,t,i){["address","lat","lng","street","city","province","postal_code","country"].forEach((t=>{if(Object.hasOwn(i,t)){let a=e.querySelector(`[data-location-field="${t}"]`);a&&(a.value=String(i[t]||""))}}))}populateTaxonomy(e,t,i){let a=this.splitIDs(i);if(0===a.length)return;const l=e.querySelector(`input[type="hidden"][name="${t}"]`);l&&(l.value=a.join(","),window.jvbSelector&&requestAnimationFrame((()=>{window.jvbSelector.updateFieldFromInput(l)})))}populateUser(e,t,i){this.populateTaxonomy(e,t,i)}populateUpload(e,t,i){if("timeline"===t||e.dataset.subtype&&"timeline"===e.dataset.subtype)return void this.populateTimelineGallery(e,t,i);if(this.isEmptyValue(i))return;const a=this.splitIDs(i);if(0===a.length)return;const l=e.querySelector('input[type="hidden"]');l&&(l.value=a.join(","));const s=e.querySelector(".item-grid");e.querySelector(".file-upload-container").hidden=a.length>0,e.querySelector(".progress")?.remove(),s&&(window.removeChildren(s),a.forEach((e=>{let t=this.data.images[e]??{};t.field={config:{showMeta:!0}},t.id=e,s.append(this.templates.create("uploadItem",t))}))),this.populateUploadMeta(e,t,i)}populateUploadMeta(e,t,i){const a=e.querySelector('[data-field="image_data"]');if(!a)return;let l=this.data.images[i]??!1;if(!l)return;a.dataset.attachmentId=l.id,a.setAttribute("data-ignore","");const s=["image-title","image-alt-text","image-caption"];for(const e of s){const t=a.querySelector(`[data-field="${e}"] input, [data-field="${e}"] textarea`);t&&""!==l[e]&&(t.value=l[e])}}populateTimelineGallery(e,t,i){if(!i||!Array.isArray(i)||0===i.length)return;let a=e.querySelector(".item-grid");if(e.querySelector(".file-upload-container").hidden=i.length>0,a){window.removeChildren(a),e.querySelector(".progress")?.remove();for(let e of i){let t=this.templates.create("timelineItem",e);t&&a.append(t)}}}populateMultiValue(e,t,i){if("string"==typeof i)try{i=JSON.parse(i)}catch(e){i=i.split(",").map((e=>e.trim()))}Array.isArray(i)||(i=[String(i)]);let a=e.querySelector(`select[name="${t}"]`);if(a&&a.multiple)for(let e of a.options)e.selected=i.includes(e.value);else e.querySelectorAll(`[type="checkbox"][name=${t}]`).forEach((e=>{e.checked=i.includes(e.value)}))}populateSingleValue(e,t,i){i=String(i||"");let a=e.querySelector(`select[name="${t}"]`);if(a)return void(a.value=i);let l=e.querySelector(`[name="${t}"][value="${i}"]`);l&&(l.checked=!0)}populateBoolean(e,t,i){const a=e.querySelector(`[name="${t}"], input[type="checkbox"]`);a&&(a.checked=Boolean(i))}populateDate(e,t,i){const a=e.querySelector(`[name="${t}"], input`);if(a){"object"==typeof i&&Object.hasOwn(i,"date")&&(i=i.date);try{const e=new Date(i);if(!isNaN(e.getTime()))switch(a.type){case"date":a.value=e.toISOString().split("T")[0];break;case"time":a.value=e.toTimeString().slice(0,5);break;case"datetime-local":a.value=e.toISOString().slice(0,16);break;default:a.value=i}}catch(e){a.value=i}}}populateNumber(e,t,i){const a=e.querySelector(`[name="${t}"], input[type="number"]`);a&&(a.value=Number(i)||0)}populateTextarea(e,t,i){let a=e.querySelector("textarea");a.dataset.editor?(a.value=String(i||""),a.dispatchEvent(new Event("change",{bubbles:!0}))):this.populateText(e,t,i)}populateText(e,t,i){let a=e.querySelector(`[name="${t}"], input, textarea`);a&&"file"!==a.type&&(a.value=String(i||""))}getFormHelper(){window.requestAnimationFrame((()=>{this.formHelper=window.jvbForm}))}splitIDs(e){return String(e).split(",").map((e=>parseInt(e.trim()))).filter((e=>!isNaN(e)&&e>0))}isEmptyValue(e){return null==e||""===e||(!(!Array.isArray(e)||0!==e.length)||"object"==typeof e&&0===Object.keys(e).length)}defineTemplates(){const e=this.templates,t=this;e.define("timelineItem",{refs:{select:'[name="select-item"]',video:"video",file:".select-item span",img:"img",details:"details[data-field]",imgAlt:'[name="image-alt-text"]',imgTitle:'[name="image-title"]',imgDesc:'[name="image-caption"]'},manyRefs:{fields:".field"},setup({el:e,refs:i,manyRefs:a,data:l}){if(e.dataset.itemId=l.id,i.select){let e=i.select.closest(".preview");window.prefixInput(i.select,`${l.id}-`,e)}i.video&&i.video.remove(),i.file&&i.file.remove();let s=t.data.images[l.post_thumbnail]??!1;if(i.img&&s&&(i.img.src=s.medium||s.small||s.large||"",i.img.title=s["image-title"]??"",i.img.alt=s["image-alt-text"]??""),i.details){let e=t.data.images[l.post_thumbnail];i.details.setAttribute("data-ignore",""),i.details.dataset.attachmentId=l.post_thumbnail,Object.hasOwn(e,"image-alt-text")&&i.alt&&(i.alt.value=e["image-alt-text"]),(Object.hasOwn(e,"image-title")||Object.hasOwn(l,"file"))&&i.title&&(i.title.value=e["image-title"]||l.file.name),Object.hasOwn(e,"image-caption")&&i.description&&(i.description.value=e["image-caption"])}if(a.fields)for(let e of a.fields){if("group"===e.dataset.fieldType)continue;if("post_thumbnail"===e.dataset.field){e.remove();continue}let i=e.dataset.field,a=l[i]??"";t.isEmptyValue(a)||t.populateField(e,i,a);const s=e.querySelector('input:not([type="file"])');s&&window.prefixInput(s,`[${l.id}]`,e)}}})}}document.addEventListener("DOMContentLoaded",(function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbPopulate=new e)}))}))})();
\ No newline at end of file
diff --git a/assets/js/min/square.min.js b/assets/js/min/square.min.js
index 3c662a4..df7dd32 100644
--- a/assets/js/min/square.min.js
+++ b/assets/js/min/square.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(e={}){this.config={...squareConfig,...e},this.payments=null,this.card=null,this.isInitialized=!1,this.cartItems=new Map,this.checkout=document.querySelector("aside#cart"),this.isOpen="1"!==this.config.isOpen||!1,this.isLoggedIn=this.config.is_logged_in||!1,this.userEmail=this.config.user_email||"",this.savedCards=[],this.selectedCardId=null,this.cartId=null,this.cache=new window.jvbCache("cart",{TTL:864e5}),this.a11y=window.jvbA11y,this.initCart(),this.checkout&&(this.initElements(),this.init(),this.initListeners(),this.isLoggedIn&&this.loadSavedCards()),this.stepMultiplier=1,this.popup=new window.jvbPopup({popup:this.checkout,toggle:this.toggle,name:"Cart",onOpen:this.maybeAddEmptyState.bind(this)}),console.log(this.popup)}async initCart(){this.cartItems=await this.cache.get("cart")??new Map,console.log("cart",this.cartItems),this.cartItems.size>0&&this.notifyRestoredCart()}handleClick(e){if(window.targetCheck(e,"button")&&window.targetCheck(e,"div.quantity")){let t=window.targetCheck(e,"div.quantity");this.handleNumberClick(e,t)}else if(window.targetCheck(e,"[data-add-to-cart]")){let t=window.targetCheck(e,"[data-add-to-cart]");this.handleAddToCart(t)}else if(window.targetCheck(e,"[data-remove-from-cart]")){let t=window.targetCheck(e,"[data-remove-from-cart]");this.handleRemoveFromCart(t)}else window.targetCheck(e,"[data-clear-cart]")&&this.clearCart()}handleChange(e,t){console.log("Checkout change");let a=window.targetCheck(e,".quantity-input");if(a){let t=e.target.closest(".quantity"),i=a.value;if(window.targetCheck(e,".cart-items")){let e=document.querySelector(`.menu-section [data-id="${t.dataset.id}"] input`);e&&(e.value=a.value)}i>0?this.handleAddToCart(t):this.handleRemoveFromCart(t)}}handleNumberClick(e,t){console.log(t),e.preventDefault();let a=0;if(e.target.closest(".increase")?a+=1:e.target.closest(".decrease")&&(a-=1),0!==a){let[e,i]=[parseInt(t.dataset.step),t.querySelector("input")],s=""===i.value?0:parseInt(i.value);i.value=s+e*a*this.stepMultiplier,i.dispatchEvent(new Event("change",{bubbles:!0})),this.handleNumberLimits(t)}}handleNumberLimits(e){let[t,a,i,s,r]=[e.dataset.min,e.dataset.max,e.querySelector("input"),e.querySelector(".increase"),e.querySelector(".decrease")],o=parseInt(i.value);o<t?(i.value=t,r.disabled=!0):o>a?(i.value=a,s.disabled=!1):s.disabled?s.disabled=!1:r.disabled&&(r.disabled=!1)}maybeAddEmptyState(){let e=this.itemsList.querySelector(".empty");if(e&&e.remove(),0===this.cartItems.size){this.checkoutPanel.disabled=!0,this.checkoutPanel.title="Add some things to your cart first!";let e=window.getTemplate("emptyCart");this.itemsList.append(e),this.table.closest("table").hidden=!0,this.total.hidden=!0,this.a11y.announce("Nothing in Cart")}else this.checkoutPanel.disabled=!1,this.table.closest("table").hidden=!1,this.total.hidden=!1,this.checkoutPanel.title="Checkout"}handleEscape(e){"Escape"===e.key?this.stepMultiplier=1:e.ctrlKey&&e.shiftKey?this.stepMultiplier=Math.max(100*parseInt(this.stepMultiplier),1e3):e.shiftKey&&(this.stepMultiplier=Math.max(10*parseInt(this.stepMultiplier),1e3))}handleAddToCart(e){let t=e.dataset.id;this.createItemElement(e);let a=parseFloat(e.dataset.price),i=parseInt(e.querySelector(".quantity-input")?.value)??1,s=parseFloat(a*i);this.cartItems.set(t,{post_id:t,name:e.dataset.name,price:a,quantity:i,total:s,square_catalog_id:e.dataset.squareCatalogId}),this.saveCart()}notifyRestoredCart(){let e=window.getTemplate("restoredCart");this.checkout.querySelector(".tab-content[data-tab=cartItems]").insertBefore(e,this.itemsList),this.cartItems.forEach((e=>{console.log(e);let t=window.getTemplate("cartItem"),a=t.querySelector(".quantity"),i=e.price,s=e.quantity;[a.dataset.id,t.querySelector("label").textContent,t.querySelector(".price").textContent,a.dataset.price,a.dataset.squareCatalogId,t.querySelector('[name="quantity"]').value,t.querySelector(".total").textContent]=[e.post_id,e.name,window.formatPrice(i),i,e.square_catalog_id,s,window.formatPrice(s*i)],this.table.append(t)})),this.updateTotal()}handleRemoveFromCart(e){if(confirm("This will remove this item from the cart. Continue?")){e.querySelector("[data-id]")||(e=e.closest(".item")?.querySelector(".quantity.field"));let t=e.dataset.id;this.cartItems.delete(t),this.table.querySelector(`[data-id="${t}"]`)?.closest("tr").remove();let a=document.querySelector(`[data-id="${t}"] input`);a&&(a.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 e=0;this.cartItems.forEach((t=>{console.log(t),e+=t.total}));let t=.05*e;e=window.formatPrice(e+t),t=window.formatPrice(t),window.eraseText(this.totalTax),window.eraseText(this.grandTotal),window.typeText(this.totalTax,t),window.typeText(this.grandTotal,e),this.totalTax.classList.remove("typeText")}createItemElement(e){let t=this.itemsList.querySelector(`[data-id="${e.dataset.id}"]`),a=!1,i=e.dataset.price,s=e.querySelector('[name="quantity"]')?.value??1;if(t)t=t.closest("tr");else{a=!0,t=window.getTemplate("cartItem");let s=t.querySelector(".quantity");[s.dataset.id,t.querySelector("label").textContent,t.querySelector(".price").textContent,s.dataset.price,s.dataset.squareCatalogId]=[e.dataset.id,e.dataset.name,window.formatPrice(i),i,e.dataset.squareCatalogId]}[t.querySelector('[name="quantity"]').value,t.querySelector(".total").textContent]=[s,window.formatPrice(s*i)],a&&(t.classList.add("adding"),this.table.append(t),setTimeout((()=>{t.classList.remove("adding")}),500))}async init(){if(window.Square)try{this.payments=window.Square.payments(this.config.application_id,this.config.location_id),await this.initializePaymentMethods(),this.isInitialized=!0,document.dispatchEvent(new CustomEvent("squareCheckoutReady",{detail:{checkout:this}}))}catch(e){console.error("Failed to initialize Square payments:",e),this.handleError(e)}else console.error("Square Web Payments SDK not loaded")}initElements(){this.toggle=document.querySelector(".toggle-cart"),this.isOpen||(this.toggle.disabled=!0,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:!1}),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(){if(document.getElementById("square-card-container"))try{this.card=await this.payments.card({style:this.getCardStyle()}),await this.card.attach("#square-card-container"),this.card.addEventListener("cardBrandChanged",(e=>{console.log("Card brand:",e.detail.cardBrand)}))}catch(e){throw console.error("Failed to initialize card:",e),e}}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(e){if(!this.isOpen)return;if(e.preventDefault(),!this.isInitialized)return void this.handleError("Checkout not initialized");const t=e.target,a=this.extractOrderData(t);try{window.jvbLoading.showLoading("Processing payment...");const e=await this.processPayment(a);this.handleSuccess(e,t)}catch(e){this.handleError(e)}finally{window.jvbLoading.hideLoading()}}extractOrderData(e){const t=Array.from(this.cartItems.values()).map((e=>({catalog_object_id:e.square_catalog_id,quantity:String(e.quantity),price:e.price,note:e.note||""}))),a=t.reduce(((e,t)=>e+t.price*t.quantity),0);return{total:Math.round(100*a),items:t,customer:{email:this.isLoggedIn?this.userEmail:e.querySelector('[name="email"]')?.value||"",name:e.querySelector('[name="name"]')?.value||"",phone:e.querySelector('[name="phone"]')?.value||""},note:e.querySelector('[name="special_instructions"]')?.value||"",pickup_time:e.querySelector('[name="pickup_time"]')?.value||""}}async processPayment(e){try{let t=null;if(this.selectedCardId)t=this.selectedCardId;else{const a=await this.card.tokenize({verificationDetails:{amount:String(e.total),currencyCode:this.config.currency||"CAD",intent:"CHARGE",customerInitiated:!0,billingContact:{givenName:e.customer.name.split(" ")[0],familyName:e.customer.name.split(" ").slice(1).join(" "),email:e.customer.email,phone:e.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"}}});if("OK"!==a.status){const e=a.errors?.map((e=>e.message)).join(", ")||"Unknown error";throw new Error(`Card tokenization failed: ${e}`)}t=a.token,a.details?.userChallenged&&console.log("3D Secure verification completed")}return await this.submitToServer(t,e,!!this.selectedCardId)}catch(e){throw console.error("Payment processing failed:",e),e}}async submitToServer(e,t,a=!1){if(!this.isOpen)throw new Error("Store is currently closed");const i=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:e,is_saved_card:a,cart_id:this.getCartId(),amount:t.total,items:t.items,customer:{email:this.isLoggedIn?this.userEmail:t.customer.email,name:t.customer.name,phone:t.customer.phone},note:t.note,pickup_time:t.pickup_time})}),s=await i.json();if(!i.ok)throw new Error(s.message||"Payment processing failed");return this.clearCart(),s}getCartId(){return this.cartId||(this.cartId=crypto.randomUUID(),this.cache.set("cart_id",this.cartId)),this.cartId}trackOrder(e){this.orderId=e,this.scheduleOrderCheck(),this.checkout.querySelector("button[data-tab=order]").hidden=!1}scheduleOrderCheck(){window.debouncer.schedule("order",(()=>{this.checkOrderStatus()}),3e4)}async checkOrderStatus(){const e=await fetch(`/wp-json/jvb/v1/square/order-status/${this.orderId}`),t=await e.json();"ready"!==t.status&&this.scheduleOrderCheck(),this.updateOrderStatus(t)}updateOrderStatus(e){this.checkout.querySelectorAll(".status-item").forEach((t=>{t.dataset.status===e.status&&t.classList.add("active")})),this.checkout.querySelector("#eta").textContent=e.eta||"In progress"}async loadSavedCards(){try{const e=await fetch(this.config.api_url+"saved-cards",{method:"GET",headers:{"X-WP-Nonce":this.config.nonce}}),t=await e.json();t.success&&t.cards&&(this.savedCards=t.cards,this.renderSavedCards())}catch(e){console.error("Failed to load saved cards:",e)}}renderSavedCards(){const e=document.getElementById("saved-cards");if(!e||0===this.savedCards.length)return;const t=`\n            <div class="saved-cards-section">\n                <h4>Saved Payment Methods</h4>\n                ${this.savedCards.map((e=>`\n                    <label class="saved-card">\n                        <input type="radio" name="payment-method" value="saved" data-card-id="${e.id}">\n                        <span class="card-info">\n                            <strong>${e.card_brand}</strong> ending in ${e.last_4}\n                            <small>Exp: ${e.exp_month}/${e.exp_year}</small>\n                        </span>\n                    </label>\n                `)).join("")}\n                <label class="saved-card">\n                    <input type="radio" name="payment-method" value="new" checked>\n                    <span>Use a new card</span>\n                </label>\n            </div>\n        `;e.innerHTML=t,e.querySelectorAll('input[name="payment-method"]').forEach((e=>{e.addEventListener("change",(e=>{const t="new"===e.target.value,a=document.getElementById("square-card-container");a&&(a.style.display=t?"block":"none"),this.selectedCardId=t?null:e.target.dataset.cardId}))}))}handleSuccess(e,t){document.dispatchEvent(new CustomEvent("squareCheckoutSuccess",{detail:{result:e,form:t}}));const a=t.dataset.successUrl||`/order-confirmation/?order=${e.wp_order_id}`;window.location.href=a}handleError(e){console.error("Square checkout error:",e),document.dispatchEvent(new CustomEvent("squareCheckoutError",{detail:{error:e}})),window.jvbNotifications?.show?.(e.message||"Payment failed","error")}}document.addEventListener("DOMContentLoaded",(()=>{window.squareCheckout=new e}))})();
\ No newline at end of file
+(()=>{class e extends window.jvbCheckout{constructor(e={}){super({...window.squareConfig,...e}),this.payments=null,this.card=null}async init(){if(window.Square)try{this.payments=window.Square.payments(this.config.application_id,this.config.location_id),await this.initializePaymentMethods(),this.isInitialized=!0,document.dispatchEvent(new CustomEvent("checkoutReady",{detail:{checkout:this,provider:"square"}}))}catch(e){console.error("Failed to initialize Square payments:",e),this.handleError(e)}else console.error("Square Web Payments SDK not loaded")}async initializePaymentMethods(){if(document.getElementById("payment-container"))try{this.card=await this.payments.card({style:this.getCardStyle()}),await this.card.attach("#payment-container"),this.card.addEventListener("cardBrandChanged",(e=>{console.log("Card brand:",e.detail.cardBrand)}))}catch(e){throw console.error("Failed to initialize card:",e),e}}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 processPayment(e){try{let t=null;if(this.selectedCardId)t=this.selectedCardId;else{const i=await this.card.tokenize({verificationDetails:{amount:String(e.total),currencyCode:this.config.currency||"CAD",intent:"CHARGE",customerInitiated:!0,billingContact:{givenName:e.customer.name.split(" ")[0],familyName:e.customer.name.split(" ").slice(1).join(" "),email:e.customer.email,phone:e.customer.phone}}});if("OK"!==i.status){const e=i.errors?.map((e=>e.message)).join(", ")||"Unknown error";throw new Error(`Card tokenization failed: ${e}`)}t=i.token,i.details?.userChallenged&&console.log("3D Secure verification completed")}return await this.submitToServer(t,e,!!this.selectedCardId)}catch(e){throw console.error("Payment processing failed:",e),e}}async submitToServer(e,t,i=!1){if(!this.isOpen)throw new Error("Store is currently closed");const a=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:e,is_saved_card:i,cart_id:this.getCartId(),amount:t.total,items:t.items,customer:{email:this.isLoggedIn?this.userEmail:t.customer.email,name:t.customer.name,phone:t.customer.phone},note:t.note,pickup_time:t.pickup_time})}),r=await a.json();if(!a.ok)throw new Error(r.message||"Payment processing failed");return this.clearCart(),r}extractOrderData(e){const t=super.extractOrderData(e);return t.items=t.items.map((e=>({catalog_object_id:e.catalog_id,quantity:e.quantity,price:e.price,note:e.note}))),t}async loadSavedCards(){try{const e=await fetch(this.config.api_url+"saved-cards",{method:"GET",headers:{"X-WP-Nonce":this.config.nonce}}),t=await e.json();t.success&&t.cards&&(this.savedCards=t.cards,this.renderSavedCards())}catch(e){console.error("Failed to load saved cards:",e)}}}document.addEventListener("DOMContentLoaded",(()=>{document.querySelector('#checkout[data-provider="square"]')&&(window.squareCheckout=new e)}))})();
\ No newline at end of file
diff --git a/inc/admin/Integrations.php b/inc/admin/Integrations.php
index 4f47744..cc11446 100644
--- a/inc/admin/Integrations.php
+++ b/inc/admin/Integrations.php
@@ -1,6 +1,8 @@
 <?php
 namespace JVBase\admin;
 
+use JVBase\managers\IconsManager;
+
 if (!defined('ABSPATH')) {
 	exit;
 }
@@ -69,6 +71,10 @@
 			true
 		);
 
+		IconsManager::for()->enqueueIconStyles();
+		IconsManager::for('dash')->enqueueIconStyles();
+		IconsManager::for('form')->enqueueIconStyles();
+
 		$queue = [
 			'api' => rest_url('jvb/v1/'),
 			'redirect' => wp_login_url(home_url(add_query_arg(null, null))),
diff --git a/inc/importers/JaneAppSalesImporter.php b/inc/importers/JaneAppSalesImporter.php
index 53c54a1..a071406 100644
--- a/inc/importers/JaneAppSalesImporter.php
+++ b/inc/importers/JaneAppSalesImporter.php
@@ -2,6 +2,7 @@
 namespace JVBase\importers;
 
 use WP_Error;
+use JVBase\managers\ReferralManager;
 
 if (!defined('ABSPATH')) {
 	exit;
diff --git a/inc/integrations/Helcim.php b/inc/integrations/Helcim.php
index a70ac16..d9e4e97 100644
--- a/inc/integrations/Helcim.php
+++ b/inc/integrations/Helcim.php
@@ -1,1633 +1,1345 @@
 <?php
 namespace JVBase\integrations;
 
-use JVBase\meta\Meta;
 use Exception;
 use WP_Error;
-use WP_REST_Request;
-use WP_REST_Response;
+use WP_Post;
+use JVBase\ui\Checkout;
+use JVBase\managers\queue\TypeConfig;
+use JVBase\managers\queue\executors\IntegrationExecutor;
 
 if (!defined('ABSPATH')) {
 	exit;
 }
 
 /**
- * Helcim Integration for JVBase
- * Handles bidirectional sync, customer management, and order processing
+ * Helcim Integration Class
+ *
+ * Handles HelcimPay.js checkout, invoice retrieval, customer/card management,
+ * and bidirectional product sync via API token authentication.
+ *
+ * Helcim is the source of truth for invoices and orders.
+ * Products sync bidirectionally through the standard integration field flow.
+ *
+ * @since 1.0.0
  */
 class Helcim extends Integrations
 {
-	// Helcim-specific configuration
-	private string $api_token;
-	private string $account_id;
-	private string $terminal_id;
-	private string $webhook_secret;
+	protected string $service_name = 'helcim';
+	protected string|array $apiBase = 'https://api.helcim.com/v2';
 
-	// Field mapping cache
-	private array $field_mappings = [];
-	private array $category_cache = [];
+	protected bool $isOAuthService = false;
 
-	// Order processing
-	private bool $is_test_mode = false;
-	private array $payment_form_settings = [];
-
-	// User security
-	private const PASSWORD_RESET_INTERVAL = 3; // Reset password every 3 logins
+	/**
+	 * Helcim API rate limits
+	 * @see https://devdocs.helcim.com/docs/api-rate-limits
+	 */
+	protected array $rate_limits = [
+		'per_second' => 5,
+		'per_minute' => 60,
+		'per_hour'   => 1000
+	];
 
 	public function __construct(?int $userID = null)
 	{
-		$this->service_name = 'helcim';
 		$this->title = 'Helcim';
-		$this->icon = 'currency-circle-dollar';
+		$this->icon  = 'credit-card';
 
-		// Helcim API endpoints
-		$this->apiBase = [
-			'production' => 'https://api.helcim.com/v2',
-			'sandbox' => 'https://api-sandbox.helcim.com/v2'
+		$this->fields = [
+			'api_token' => [
+				'type'     => 'text',
+				'subtype'  => 'password',
+				'label'    => 'API Token',
+				'hint'     => 'Found in Helcim Dashboard → Settings → API Access',
+				'required' => true,
+			],
+			'currency' => [
+				'type'    => 'select',
+				'label'   => 'Currency',
+				'options' => [
+					'CAD' => 'CAD',
+					'USD' => 'USD',
+				],
+				'default' => 'CAD',
+			],
 		];
 
-		$this->apiEndpoints = [
-			'commerce/invoice',
-			'commerce/transaction',
-			'commerce/customer',
-			'commerce/card-batch',
-			'commerce/terminal',
-			'commerce/product',
-			'commerce/order',
-			'payment/purchase',
-			'payment/verify',
-			'payment/capture',
-			'payment/refund',
-			'customer/create',
-			'customer/update',
-			'customer/get',
-			'inventory/product',
-			'inventory/batch'
+		$this->advanced = [
+			'fee_saver' => [
+				'type'  => 'true_false',
+				'label' => 'Fee Saver',
+				'hint'  => 'Pass processing fees to customers (not compatible with Google Pay)',
+			],
+			'allow_ach' => [
+				'type'  => 'true_false',
+				'label' => 'Allow ACH/Bank Payments',
+				'hint'  => 'Enable bank account payments alongside credit card',
+			],
+		];
+
+		$this->instructions = [
+			'Go to <a href="https://my.helcim.com" target="_blank">Helcim Dashboard</a>',
+			'Navigate to Settings → API Access',
+			'Create a new API Access Configuration',
+			'Enable permissions: General (Customers, Invoices, Products), Transaction Processing',
+			'Copy the API Token and paste it below',
 		];
 
 		$this->canSync = [
 			'create' => true,
 			'update' => true,
-			'delete' => true
+			'delete' => false,
 		];
 
-		$this->fields = [
-			'test_mode' => [
-				'type'	=> 'select',
-				'label'	=> 'Environment',
-				'options'	=> [
-					'1'	=> 'Test Mode',
-					'0'	=> 'Production'
-				],
-				'default'	=> '1'
-			],
-			'api_token'	=> [
-				'type'	=> 'text',
-				'subtype'	=> 'password',
-				'required'	=> true,
-				'hint'	=> 'Your Helcim API Token'
-			],
-			'account_id'	=> [
-				'type'	=> 'text',
-				'required'	=> true,
-				'label'	=> 'Account ID',
-				'hint'	=> 'Your Helcim Account ID',
-			],
-			'webhook_secret'	=> [
-				'type'	=> 'text',
-				'subtype'	=> 'password',
-				'label'	=> 'Webhook Secret',
-				'hint'	=> 'For webhook verification',
-				'required'	=> true,
-			]
-		];
-
-		$this->advanced = [
-
-		];
-
-		$this->instructions = [
-			'Once you are set up, add this URL to your Helcim webhook settings: <code>'.esc_html(rest_url('jvb/v1/webhooks/helcim')).'</code>'
-		];
-
-		$this->defaults = [
-
-		];
-
-		$this->handleWebhooks = true;
+		$this->handleWebhooks = false;
 
 		parent::__construct($userID);
 
-		$this->actions = array_merge(
-			$this->actions,
-			[
-				'import_from_helcim'	=> 'handleImportFromHelcim',
-				'sync_to_helcim'		=> 'handleSyncToHelcim'
-			]
-		);
-		// Initialize field mappings
-		$this->initializeFieldMappings();
+		// Helcim-specific actions (processAction dispatches these)
+		$this->actions = array_merge($this->actions, [
+			'initialize_checkout' => 'initializeCheckout',
+			'get_invoices'        => 'handleGetInvoices',
+			'get_invoice'         => 'handleGetInvoice',
+			'get_customer_cards'  => 'handleGetCustomerCards',
+		]);
+
+		$this->buttons = array_merge($this->buttons, [
+			'import_from_helcim' => 'Import Products from Helcim',
+			'sync_to_helcim'    => 'Sync Site to Helcim',
+		]);
 	}
 
-	/**
-	 * Initialize service-specific settings
-	 */
+	/*****************************************************************
+	 * ABSTRACT IMPLEMENTATIONS
+	 *****************************************************************/
+
 	protected function initialize(): void
 	{
-		$this->api_token = $this->credentials['api_token'] ?? '';
-		$this->account_id = $this->credentials['account_id'] ?? '';
-		$this->terminal_id = $this->credentials['terminal_id'] ?? '';
-		$this->webhook_secret = $this->credentials['webhook_secret'] ?? '';
-		$this->is_test_mode = (bool)($this->credentials['test_mode'] ?? false);
+		if (empty($this->credentials)) {
+			$this->loadCredentials();
+		}
 
-		// Set the appropriate API base
-		$this->apiBase = $this->is_test_mode ? $this->apiBase['sandbox'] : $this->apiBase['production'];
-
-		// Load payment form settings
-		$this->payment_form_settings = [
-			'card' => $this->credentials['enable_card_payments'] ?? true,
-			'ach' => $this->credentials['enable_ach_payments'] ?? false,
-			'apple_pay' => $this->credentials['enable_apple_pay'] ?? false,
-			'google_pay' => $this->credentials['enable_google_pay'] ?? false,
+		$this->apiEndpoints = [
+			'connection-test',
+			'helcim-pay/initialize',
+			'invoices',
+			'customers',
+			'payment/purchase',
+			'payment/preauth',
+			'payment/capture',
+			'payment/refund',
+			'payment/verify',
+			'card-transactions',
+			'card-batches',
 		];
 	}
 
+	protected function getRequestHeaders(): array
+	{
+		return [
+			'api-token'    => $this->credentials['api_token'] ?? '',
+			'Content-Type' => 'application/json',
+			'Accept'       => 'application/json',
+		];
+	}
+
+	protected function performConnectionTest(): bool
+	{
+		try {
+			$response = $this->getRequest('connection-test', [], null, 'none', true);
+			return !is_wp_error($response) && !$this->isErrorResponse($response ?? []);
+		} catch (Exception $e) {
+			$this->logError('Connection test failed', ['error' => $e->getMessage()]);
+			return false;
+		}
+	}
+
+	/*****************************************************************
+	 * CONTENT TYPES — product field definitions
+	 *****************************************************************/
+
+	protected function setContentTypes(): void
+	{
+		$this->has_content = true;
+		$this->defaultContent = 'REGULAR';
+		$types = ['REGULAR', 'SERVICE', 'DIGITAL', 'FOOD_AND_BEV', 'EVENT', 'SUBSCRIPTION', 'DONATION'];
+
+		foreach ($types as $type) {
+			$t = $type === 'REGULAR' ? null : $type;
+			$this->contentTypes[$type] = $this->getHelcimMeta($t);
+		}
+	}
+
 	/**
-	 * Register additional WordPress hooks
+	 * Get Helcim product meta fields by type.
+	 *
+	 * Used by FieldRegistry when 'use_helcim' => true is set
+	 * in a JVB_CONTENT definition.
 	 */
+	public function getHelcimMeta(?string $type = null): array
+	{
+		$fields = [
+			// Basic Product Fields
+			'price' => [
+				'type'        => 'number',
+				'bulkEdit'    => true,
+				'label'       => 'Price',
+				'step'        => 0.01,
+				'max'         => 99999,
+				'description' => 'Product price'
+			],
+
+			'product_type' => [
+				'type'  => 'select',
+				'label' => 'Product Type',
+				'options' => [
+					'REGULAR'       => 'Regular Product',
+					'SERVICE'       => 'Service',
+					'DIGITAL'       => 'Digital Product',
+					'FOOD_AND_BEV'  => 'Food & Beverage',
+					'EVENT'         => 'Event/Ticket',
+					'SUBSCRIPTION'  => 'Subscription',
+					'DONATION'      => 'Donation'
+				],
+				'default' => $type ?? 'REGULAR'
+			],
+
+			'cart_quantity' => [
+				'type'   => 'number',
+				'label'  => 'Quantity',
+				'hidden' => true,
+			],
+
+			// Tax & Shipping
+			'tax_exempt' => [
+				'type'    => 'true_false',
+				'label'   => 'Tax Exempt',
+				'section' => 'helcim-tax'
+			],
+
+			'shipping_required' => [
+				'type'    => 'true_false',
+				'label'   => 'Shipping Required',
+				'section' => 'helcim-shipping'
+			],
+
+			'shipping_weight' => [
+				'type'        => 'number',
+				'label'       => 'Shipping Weight (kg)',
+				'step'        => 0.01,
+				'section'     => 'helcim-shipping',
+				'condition'   => [
+					'field'    => 'shipping_required',
+					'operator' => '==',
+					'value'    => true
+				]
+			],
+
+			// Availability
+			'available_online' => [
+				'type'    => 'true_false',
+				'label'   => 'Available Online',
+				'section' => 'helcim-availability',
+				'default' => true
+			],
+
+			'available_for_pickup' => [
+				'type'    => 'true_false',
+				'label'   => 'Available for Pickup',
+				'section' => 'helcim-availability',
+				'default' => true
+			],
+
+			'available_for_delivery' => [
+				'type'    => 'true_false',
+				'label'   => 'Available for Delivery',
+				'section' => 'helcim-availability',
+				'default' => false
+			],
+
+			'_helcim_sku' => [
+				'type'        => 'text',
+				'label'       => 'SKU',
+				'description' => 'Stock keeping unit',
+				'section'     => 'helcim-config'
+			],
+
+			// Product Variations
+			'product_variations' => [
+				'type'        => 'repeater',
+				'label'       => 'Product Variations',
+				'description' => 'Different versions of this product',
+				'add_label'   => 'Add Variation',
+				'section'     => 'variations',
+				'fields'      => $this->getHelcimVariationMeta($type)
+			],
+
+			// Product Options
+			'options' => [
+				'type'   => 'group',
+				'label'  => 'Product Options',
+				'section'=> 'helcim-options',
+				'fields' => [
+					'max_order' => [
+						'type'    => 'number',
+						'label'   => 'Maximum per order',
+						'default' => 50
+					],
+					'min_order' => [
+						'type'    => 'number',
+						'label'   => 'Minimum per order',
+						'default' => 0,
+					],
+					'step' => [
+						'type'    => 'number',
+						'label'   => 'Order increment',
+						'default' => 1,
+					],
+					'preparation_time' => [
+						'type'        => 'number',
+						'label'       => 'Preparation time (minutes)',
+						'description' => 'Time needed to prepare this item',
+						'condition'   => [
+							'field'    => 'product_type',
+							'operator' => 'in',
+							'value'    => ['FOOD_AND_BEV', 'SERVICE']
+						]
+					]
+				]
+			],
+
+			// Subscription Fields
+			'subscription_settings' => [
+				'type'      => 'group',
+				'label'     => 'Subscription Settings',
+				'section'   => 'helcim-subscription',
+				'condition' => [
+					'field'    => 'product_type',
+					'operator' => '==',
+					'value'    => 'SUBSCRIPTION'
+				],
+				'fields' => [
+					'billing_cycle' => [
+						'type'    => 'select',
+						'label'   => 'Billing Cycle',
+						'options' => [
+							'daily'     => 'Daily',
+							'weekly'    => 'Weekly',
+							'monthly'   => 'Monthly',
+							'quarterly' => 'Quarterly',
+							'yearly'    => 'Yearly'
+						],
+						'default' => 'monthly'
+					],
+					'trial_period' => [
+						'type'        => 'number',
+						'label'       => 'Trial Period (days)',
+						'description' => 'Free trial period before billing starts',
+						'default'     => 0
+					],
+					'setup_fee' => [
+						'type'        => 'number',
+						'label'       => 'Setup Fee',
+						'step'        => 0.01,
+						'description' => 'One-time setup fee'
+					]
+				]
+			],
+
+			// Food & Beverage Specific
+			'food_settings' => [
+				'type'      => 'group',
+				'label'     => 'Food & Beverage Settings',
+				'section'   => 'helcim-food',
+				'condition' => [
+					'field'    => 'product_type',
+					'operator' => '==',
+					'value'    => 'FOOD_AND_BEV'
+				],
+				'fields' => [
+					'ingredients' => [
+						'type'        => 'textarea',
+						'label'       => 'Ingredients',
+						'description' => 'List ingredients (comma separated)'
+					],
+					'allergens' => [
+						'type'        => 'checkbox_list',
+						'label'       => 'Allergens',
+						'options'     => [
+							'gluten'   => 'Contains Gluten',
+							'dairy'    => 'Contains Dairy',
+							'nuts'     => 'Contains Nuts',
+							'soy'      => 'Contains Soy',
+							'eggs'     => 'Contains Eggs',
+							'seafood'  => 'Contains Seafood'
+						]
+					],
+					'dietary_options' => [
+						'type'    => 'checkbox_list',
+						'label'   => 'Dietary Options',
+						'options' => [
+							'vegetarian'   => 'Vegetarian',
+							'vegan'        => 'Vegan',
+							'gluten_free'  => 'Gluten Free',
+							'dairy_free'   => 'Dairy Free',
+							'keto'         => 'Keto Friendly',
+							'halal'        => 'Halal',
+							'kosher'       => 'Kosher'
+						]
+					],
+					'spice_level' => [
+						'type'    => 'range',
+						'label'   => 'Spice Level',
+						'min'     => 0,
+						'max'     => 5,
+						'default' => 0
+					],
+					'serving_size' => [
+						'type'        => 'text',
+						'label'       => 'Serving Size',
+						'description' => 'e.g., "Serves 2-3"'
+					]
+				]
+			],
+
+			// Service Specific
+			'service_settings' => [
+				'type'      => 'group',
+				'label'     => 'Service Settings',
+				'section'   => 'helcim-service',
+				'condition' => [
+					'field'    => 'product_type',
+					'operator' => '==',
+					'value'    => 'SERVICE'
+				],
+				'fields' => [
+					'service_duration' => [
+						'type'        => 'number',
+						'label'       => 'Duration (minutes)',
+						'description' => 'Service duration in minutes'
+					],
+					'booking_required' => [
+						'type'  => 'true_false',
+						'label' => 'Booking Required'
+					],
+					'capacity' => [
+						'type'        => 'number',
+						'label'       => 'Service Capacity',
+						'description' => 'Maximum number of customers per service'
+					],
+					'staff_required' => [
+						'type'        => 'number',
+						'label'       => 'Staff Required',
+						'description' => 'Number of staff needed',
+						'default'     => 1
+					]
+				]
+			],
+
+			// Event Specific
+			'event_settings' => [
+				'type'      => 'group',
+				'label'     => 'Event Settings',
+				'section'   => 'helcim-event',
+				'condition' => [
+					'field'    => 'product_type',
+					'operator' => '==',
+					'value'    => 'EVENT'
+				],
+				'fields' => [
+					'event_date' => [
+						'type'  => 'datetime',
+						'label' => 'Event Date & Time'
+					],
+					'event_location' => [
+						'type'  => 'text',
+						'label' => 'Event Location'
+					],
+					'max_attendees' => [
+						'type'  => 'number',
+						'label' => 'Maximum Attendees'
+					],
+					'early_bird_price' => [
+						'type'        => 'number',
+						'label'       => 'Early Bird Price',
+						'step'        => 0.01,
+						'description' => 'Discounted price for early registrations'
+					],
+					'early_bird_deadline' => [
+						'type'  => 'date',
+						'label' => 'Early Bird Deadline'
+					]
+				]
+			]
+		];
+
+		// Add inventory fields if configured
+		if ($config['hasInventory'] ?? false) {
+			$fields['_helcim_inventory'] = [
+				'type'     => 'number',
+				'label'    => 'Inventory',
+				'bulkEdit' => true,
+				'section'  => 'inventory'
+			];
+
+			$fields['track_inventory'] = [
+				'type'    => 'true_false',
+				'label'   => 'Track Inventory',
+				'section' => 'inventory',
+				'default' => true
+			];
+
+			$fields['low_stock_threshold'] = [
+				'type'        => 'number',
+				'label'       => 'Low Stock Alert',
+				'description' => 'Alert when stock falls below this level',
+				'section'     => 'inventory',
+				'default'     => 5
+			];
+
+			$fields['product_variations']['fields']['inventory'] = [
+				'type'        => 'number',
+				'label'       => 'Stock Quantity',
+				'description' => 'Current stock for this variation'
+			];
+		}
+
+		return $fields;
+	}
+
+	public function getHelcimVariationMeta(?string $type = null):array
+	{
+
+		$base = [
+			'name' => [
+				'type'        => 'text',
+				'label'       => 'Variation Name',
+				'description' => 'e.g., "Small", "Large", "Red"'
+			],
+			'price' => [
+				'type'        => 'number',
+				'label'       => 'Price',
+				'step'        => 0.01,
+				'max'         => 99999,
+				'description' => 'Price for this variation'
+			],
+			'sku' => [
+				'type'        => 'text',
+				'label'       => 'SKU',
+				'description' => 'Stock keeping unit for this variation'
+			],
+			'track_inventory' => [
+				'type'  => 'true_false',
+				'label' => 'Track Inventory',
+			],
+			'_helcim_variation_id' => [
+				'type'        => 'text',
+				'label'       => 'Helcim Variation ID',
+				'description' => 'Helcim ID for this variation',
+				'hidden'      => true
+			],
+			'_helcim_last_sync' => [
+				'type'   => 'datetime',
+				'label'  => 'Last Sync',
+				'hidden' => true
+			],
+			'options' => [
+				'type'        => 'group',
+				'label'       => 'Variation Options',
+				'collapsible' => true,
+				'fields'      => [
+					'color' => [
+						'type'        => 'color',
+						'label'       => 'Color',
+						'description' => 'Visual color for this variation'
+					],
+					'size' => [
+						'type'    => 'select',
+						'label'   => 'Size',
+						'options' => [
+							''      => 'N/A',
+							'xs'    => 'Extra Small',
+							's'     => 'Small',
+							'm'     => 'Medium',
+							'l'     => 'Large',
+							'xl'    => 'Extra Large',
+							'xxl'   => '2X Large',
+							'custom'=> 'Custom'
+						]
+					],
+					'custom_size' => [
+						'type'        => 'text',
+						'label'       => 'Custom Size',
+						'condition'   => [
+							'field'    => 'size',
+							'operator' => '==',
+							'value'    => 'custom'
+						]
+					],
+					'weight' => [
+						'type'        => 'number',
+						'label'       => 'Weight (kg)',
+						'step'        => 0.01,
+						'description' => 'Weight of this variation'
+					],
+					'dimensions' => [
+						'type'   => 'group',
+						'label'  => 'Dimensions',
+						'fields' => [
+							'length' => [
+								'type'  => 'number',
+								'label' => 'Length (cm)',
+								'step'  => 0.1
+							],
+							'width' => [
+								'type'  => 'number',
+								'label' => 'Width (cm)',
+								'step'  => 0.1
+							],
+							'height' => [
+								'type'  => 'number',
+								'label' => 'Height (cm)',
+								'step'  => 0.1
+							]
+						]
+					]
+				]
+			]
+		];
+
+		$extras = [
+			'SERVICE' => [
+				'service_duration'	=> [
+					'type'        => 'number',
+					'label'       => 'Duration (minutes)',
+					'description' => 'Duration for this service variation'
+				],
+				'available_for_booking'	=> [
+					'type'  => 'true_false',
+					'label' => 'Available for Booking'
+				]
+			],
+
+			'FOOD_AND_BEV'	=> [
+				'portion_size'	=> [
+					'type'    => 'select',
+					'label'   => 'Portion Size',
+					'options' => [
+						'small'    => 'Small',
+						'regular'  => 'Regular',
+						'large'    => 'Large',
+						'family'   => 'Family Size'
+					]
+				],
+				'calories'	=> [
+					'type'        => 'number',
+					'label'       => 'Calories',
+					'description' => 'Calorie count for this variation'
+				]
+			],
+
+			'DIGITAL'	=> [
+				'download_limit'	=> [
+					'type'        => 'number',
+					'label'       => 'Download Limit',
+					'description' => 'Maximum number of downloads',
+					'default'     => -1
+				],
+				'expiry_days'	=> [
+					'type'        => 'number',
+					'label'       => 'Access Duration (days)',
+					'description' => 'Days until download expires',
+					'default'     => 0
+				]
+			]
+		];
+		if ($type && array_key_exists($type, $extras)){
+			$base = array_merge($base, $extras[$type]);
+		}
+		return $base;
+	}
+
+	/*****************************************************************
+	 * HELCIMPAY.JS — Frontend Scripts & Checkout Initialization
+	 *****************************************************************/
+
 	protected function registerAdditionalHooks(): void
 	{
-		$this->ensureInitialized();
 		if (!$this->isSetUp()) {
 			return;
 		}
-		// User login tracking for security
-		add_action('wp_login', [$this, 'trackUserLogin'], 10, 2);
 
-		add_action('wp_footer', [$this, 'outputCheckout']);
-
-		// Enqueue checkout scripts
 		add_action('wp_enqueue_scripts', [$this, 'enqueueScripts']);
 
-		// REST API endpoints for checkout
-		add_action('rest_api_init', [$this, 'registerRestRoutes']);
-	}
+		// Shared checkout UI (replaces provider-specific outputCheckout)
+		add_filter('jvbAdditionalActions', [Checkout::class, 'render']);
 
-	/**
-	 * Register REST API routes
-	 */
-	public function registerRestRoutes(): void
-	{
-		register_rest_route('jvb/v1', '/helcim/checkout', [
-			'methods' => 'POST',
-			'callback' => [$this, 'handleCheckout'],
-			'permission_callback' => '__return_true'
-		]);
-
-		register_rest_route('jvb/v1', '/helcim/customer', [
-			'methods' => 'POST',
-			'callback' => [$this, 'handleCustomerLookup'],
-			'permission_callback' => '__return_true'
-		]);
-
-		register_rest_route('jvb/v1', '/helcim/order-status/(?P<order_id>[a-zA-Z0-9-]+)', [
-			'methods' => 'GET',
-			'callback' => [$this, 'handleOrderStatus'],
-			'permission_callback' => '__return_true'
-		]);
-
-		register_rest_route('jvb/v1', '/helcim/create-account', [
-			'methods' => 'POST',
-			'callback' => [$this, 'handleAccountCreation'],
-			'permission_callback' => '__return_true'
-		]);
-	}
-
-	/**
-	 * Initialize field mappings for all content types
-	 */
-	private function initializeFieldMappings(): void
-	{
-		foreach (JVB_CONTENT as $key => $config) {
-			if (isset($config['integrations']['helcim'])) {
-				$post_type = jvbCheckBase($key);
-				$this->field_mappings[$post_type] = $this->getFieldMapping($post_type);
+		// Checkout description filter
+		add_filter('jvb_checkout_description', function (string $desc, string $provider) {
+			if ($provider === 'helcim') {
+				return 'Securely checkout with your name, email, and payments processed by Helcim.';
 			}
-		}
+			return $desc;
+		}, 10, 2);
+
+		// Register queue operation types with IntegrationExecutor
+		$this->registerQueueTypes();
+
+		// Register webhook endpoint (handled by parent)
+		$this->registerWebhookEndpoint();
 	}
 
-	/**
-	 * Get field mapping for a post type
-	 */
-	public function getFieldMapping(string $post_type): array
-	{
-		return apply_filters(BASE . '_helcim_field_mapping', [
-			'name' => 'title',
-			'description' => 'content',
-			'price' => 'price',
-			'sku' => '_helcim_sku',
-			'product_code' => '_helcim_product_code',
-			'inventory' => '_helcim_inventory',
-			'product_type' => 'product_type',
-			'tax_exempt' => 'tax_exempt',
-			'shipping_required' => 'shipping_required'
-		], $post_type);
-	}
-
-	/**
-	 * Output checkout form
-	 */
-	public function outputCheckout(): void
-	{
-		if (is_singular(BASE.'dash') || is_post_type_archive(BASE.'dash')) {
-			return;
-		}
-		?>
-		<button type="button" class="toggle-cart row" title="Your Cart" data-action="toggle-cart" aria-label="Open Cart" aria-controls="checkout" aria-expanded="false" hidden>
-			<?= jvbIcon('shopping-cart')?><span class="abs"></span><span class="abs count"></span>
-		</button>
-		<aside id="cart" class="main">
-			<form id="checkout" data-form-id="checkout" data-save="checkout">
-				<?php
-				$tabs = [
-					'cartItems' => [
-						'title'	=> 'Your Order',
-						'icon'	=> 'cart',
-						'description' => 'Review and modify your order items',
-						'content'	=> $this->cartContent()
-					],
-					'account' => [
-						'title' => 'Account',
-						'icon' => 'user',
-						'description' => $this->getAccountTabDescription(),
-						'content' => $this->renderAccountSection()
-					],
-					'checkout'	=> [
-						'title'	=> 'Checkout',
-						'icon'	=> 'checkout',
-						'description' => 'Complete your order with Helcim secure payments',
-						'content'	=> $this->renderCheckoutSection()
-					],
-					'order'	=> [
-						'title'	=> 'Order Status',
-						'icon' => 'truck',
-						'hidden'	=> true,
-						'description' => 'Track your order status',
-						'content'	=> $this->renderOrderStatus()
-					]
-				];
-				jvbRenderTabs($tabs);
-				?>
-				<div class="cart-total row end">
-					<p class="tax">Tax: <span></span></p>
-					<p class="total">GRAND TOTAL: <span></span></p>
-				</div>
-			</form>
-		</aside>
-		<?php
-		$this->outputCheckoutTemplates();
-	}
-
-	/**
-	 * Get account tab description based on login status
-	 */
-	private function getAccountTabDescription(): string
-	{
-		if (is_user_logged_in()) {
-			return 'Manage your account and view order history';
-		}
-		return 'Login or create an account for faster checkout';
-	}
-
-	/**
-	 * Render account section
-	 */
-	private function renderAccountSection(): string
-	{
-		ob_start();
-		?>
-		<div class="account-section">
-			<?php if (is_user_logged_in()): ?>
-				<?php $this->renderLoggedInAccount(); ?>
-			<?php else: ?>
-				<?php $this->renderGuestAccount(); ?>
-			<?php endif; ?>
-		</div>
-		<?php
-		return ob_get_clean();
-	}
-
-	/**
-	 * Render logged in account view
-	 */
-	private function renderLoggedInAccount(): void
-	{
-		$user = wp_get_current_user();
-		$customer_id = get_user_meta($user->ID, BASE . '_helcim_customer_id', true);
-		?>
-		<div class="logged-in-account">
-			<p>Welcome back, <?= esc_html($user->display_name) ?>!</p>
-
-			<div class="account-actions">
-				<button type="button" class="button" onclick="helcimCheckout.loadSavedCards()">
-					<?= jvbIcon('credit-card') ?> Saved Cards
-				</button>
-				<button type="button" class="button" onclick="helcimCheckout.loadOrderHistory()">
-					<?= jvbIcon('receipt') ?> Order History
-				</button>
-				<button type="button" class="button" onclick="helcimCheckout.loadFavorites()">
-					<?= jvbIcon('heart') ?> Favorites
-				</button>
-			</div>
-
-			<div id="account-content"></div>
-		</div>
-		<?php
-	}
-
-	/**
-	 * Render guest account view
-	 */
-	private function renderGuestAccount(): void
-	{
-		?>
-		<div class="guest-account">
-			<div class="login-section">
-				<h3>Returning Customer?</h3>
-				<p>Login with your email to access saved cards and order history</p>
-
-				<div class="email-login">
-					<input type="email"
-						   id="login-email"
-						   placeholder="Enter your email"
-						   autocomplete="email">
-					<button type="button"
-							class="button primary"
-							onclick="helcimCheckout.loginWithEmail()">
-						Continue
-					</button>
-				</div>
-
-				<div id="login-status"></div>
-			</div>
-
-			<div class="guest-checkout">
-				<h3>New Customer?</h3>
-				<p>You can checkout as a guest or create an account after your order</p>
-
-				<label>
-					<input type="checkbox" id="create-account-offer">
-					Offer to create account after checkout
-				</label>
-			</div>
-		</div>
-		<?php
-	}
-
-	/**
-	 * Render checkout section
-	 */
-	private function renderCheckoutSection(): string
-	{
-		ob_start();
-		?>
-		<div class="checkout-section">
-			<h3>Customer Information</h3>
-
-			<input type="text" name="name" placeholder="Full Name" required autocomplete="name">
-			<input type="email" name="email" placeholder="Email" required autocomplete="email">
-			<input type="tel" name="phone" placeholder="Phone" required autocomplete="tel">
-
-			<h3>Pickup/Delivery Details</h3>
-			<select name="fulfillment_type" required>
-				<option value="pickup">Pickup</option>
-				<option value="delivery">Delivery</option>
-			</select>
-
-			<div class="pickup-details" data-show-if="fulfillment_type:pickup">
-				<input type="datetime-local" name="pickup_time" required>
-			</div>
-
-			<div class="delivery-details" data-show-if="fulfillment_type:delivery" style="display:none;">
-				<input type="text" name="delivery_address" placeholder="Delivery Address" autocomplete="street-address">
-				<input type="text" name="delivery_instructions" placeholder="Delivery Instructions">
-			</div>
-
-			<textarea name="special_instructions" placeholder="Special instructions or dietary notes"></textarea>
-
-			<h3>Payment Information</h3>
-			<div id="saved-cards"></div>
-			<div id="helcim-card-container"></div>
-
-			<button type="submit" class="button primary checkout-button">
-				Place Order
-			</button>
-		</div>
-		<?php
-		return ob_get_clean();
-	}
-
-	/**
-	 * Render order status section
-	 */
-	protected function renderOrderStatus(): string
-	{
-		ob_start();
-		?>
-		<div class="order-confirmation">
-			<h2>Order Confirmed!</h2>
-			<div id="order-status" data-order="">
-				<p>Order #<span class="order-num"></span></p>
-				<div class="status-timeline">
-					<div class="status-item active" data-status="received">Order Received</div>
-					<div class="status-item" data-status="preparing">Preparing</div>
-					<div class="status-item" data-status="ready">Ready</div>
-					<div class="status-item" data-status="complete">Complete</div>
-				</div>
-				<div class="order-eta">
-					Estimated time: <span id="eta">Calculating...</span>
-				</div>
-			</div>
-		</div>
-		<?php
-		return ob_get_clean();
-	}
-
-	/**
-	 * Output checkout templates
-	 */
-	private function outputCheckoutTemplates(): void
-	{
-		?>
-		<template class="cartItem">
-			<tr class="item">
-				<td class="item">
-					<label for="quantity"></label>
-					<div class="quantity field" data-min="0" data-max="50" data-step="1" data-price="" data-id="">
-						<button type="button" class="decrease" aria-label="Decrease quantity">
-							<?= jvbIcon('minus-square') ?>
-						</button>
-						<input type="number" name="quantity" value="1" min="0" max="50">
-						<button type="button" class="increase" aria-label="Increase quantity">
-							<?= jvbIcon('plus') ?>
-						</button>
-					</div>
-				</td>
-				<td class="price"></td>
-				<td class="total"></td>
-				<td>
-					<button type="button" class="remove" aria-label="Remove item">
-						<?= jvbIcon('trash') ?>
-					</button>
-				</td>
-			</tr>
-		</template>
-
-		<template class="savedCard">
-			<label class="saved-card-option">
-				<input type="radio" name="payment_method" value="">
-				<span class="card-details">
-					<span class="card-brand"></span>
-					•••• <span class="last-4"></span>
-					<span class="exp-date"></span>
-				</span>
-			</label>
-		</template>
-		<?php
-	}
-
-	/**
-	 * Cart content section
-	 */
-	private function cartContent(): string
-	{
-		ob_start();
-		?>
-		<div class="cart-items">
-			<table>
-				<thead>
-				<tr>
-					<th>Item</th>
-					<th>Price</th>
-					<th>Total</th>
-					<th></th>
-				</tr>
-				</thead>
-				<tbody></tbody>
-			</table>
-
-			<div class="cart-actions">
-				<button type="button" class="button" onclick="helcimCheckout.clearCart()">
-					<?= jvbIcon('trash') ?> Clear Cart
-				</button>
-			</div>
-		</div>
-		<?php
-		return ob_get_clean();
-	}
-
-	/**
-	 * Enqueue checkout scripts
-	 */
 	public function enqueueScripts(): void
 	{
-		$this->ensureInitialized();
-		if (!$this->isSetUp()) {
-			return;
-		}
-		// Helcim JS SDK
-		$sdk_url = $this->is_test_mode
-			? 'https://helcim-js-sandbox.helcim.com/v1/helcim.js'
-			: 'https://js.helcim.com/v1/helcim.js';
-
+		// HelcimPay.js SDK
 		wp_enqueue_script(
-			'helcim-js-sdk',
-			$sdk_url,
+			'helcim-pay-sdk',
+			'https://secure.helcim.app/helcim-pay/services/start.js',
 			[],
 			null,
-			[
-				'strategy' => 'defer',
-				'in_footer' => true
-			]
+			true
 		);
 
-		// Register custom checkout script
+		// Base cart checkout (shared with Square)
+		wp_register_script(
+			'jvb-checkout',
+			JVB_URL . 'assets/js/min/checkout.min.js',
+			['jvb-utility', 'jvb-queue', 'jvb-a11y', 'jvb-cache', 'jvb-tabs', 'jvb-popup'],
+			'1.1.31',
+			true
+		);
+
+		// Helcim checkout (extends CartCheckout)
 		wp_register_script(
 			'jvb-helcim-checkout',
 			JVB_URL . 'assets/js/min/helcim.min.js',
-			[
-				'jvb-utility',
-				'jvb-queue',
-				'jvb-a11y',
-				'jvb-cache',
-				'jvb-tabs',
-				'jvb-modal',
-			],
-			'1.0.0',
-			[
-				'strategy' => 'defer',
-				'in_footer' => true
-			]
+			['jvb-checkout', 'helcim-pay-sdk'],
+			'1.1.31',
+			true
 		);
 
+		wp_localize_script('jvb-helcim-checkout', 'helcimConfig', [
+			'api_url'      => rest_url('jvb/v1/helcim/'),
+			'nonce'        => wp_create_nonce('wp_rest'),
+			'currency'     => $this->credentials['currency'] ?? 'CAD',
+			'is_logged_in' => is_user_logged_in(),
+			'user_email'   => is_user_logged_in() ? wp_get_current_user()->user_email : '',
+			'isOpen'       => apply_filters('jvb_store_is_open', '1'),
+		]);
+
 		wp_enqueue_script('jvb-helcim-checkout');
-
-		// Localize the checkout script with Helcim config
-		wp_localize_script(
-			'jvb-helcim-checkout',
-			'helcimConfig',
-			[
-				'isOpen' => jvbIsOpen(),
-				'apiUrl' => rest_url('jvb/v1/helcim/'),
-				'nonce' => wp_create_nonce('wp_rest'),
-				'accountId' => $this->account_id,
-				'testMode' => $this->is_test_mode
-			]
-		);
 	}
 
-	/******************************************************************
-	 * POST SYNC METHODS
-	 ******************************************************************/
+
+	protected function registerQueueTypes(): void
+	{
+		$queue    = JVB()->queue();
+		$executor = new IntegrationExecutor();
+
+		$queue->registry()->register('helcim_sync_to', new TypeConfig(
+			executor:  $executor,
+			chunkKey:  'items',
+			chunkSize: 10,
+			maxRetries: 3
+		));
+
+		$queue->registry()->register('helcim_sync_from', new TypeConfig(
+			executor:  $executor,
+			chunkKey:  'items',
+			chunkSize: 10,
+			maxRetries: 3
+		));
+
+		$queue->registry()->register('helcim_delete_from', new TypeConfig(
+			executor:  $executor,
+			chunkKey:  'external_ids',
+			chunkSize: 20,
+			maxRetries: 2
+		));
+
+		$queue->registry()->register('helcim_import', new TypeConfig(
+			executor:   $executor,
+			maxRetries: 3
+		));
+
+		$queue->registry()->register('helcim_sync_customer', new TypeConfig(
+			executor:   $executor,
+			maxRetries: 2
+		));
+	}
 
 	/**
-	 * Handle post save for Helcim sync
+	 * Initialize a HelcimPay.js checkout session.
+	 *
+	 * Server-side: POST /helcim-pay/initialize → returns checkoutToken + secretToken.
+	 * Client-side: appendHelcimPayIframe(checkoutToken) renders the payment modal.
+	 * Tokens are valid for 60 minutes.
+	 *
+	 * @param array $data [
+	 *     'amount'      => float,   // Required
+	 *     'invoiceId'   => string,  // Optional — pay a specific invoice
+	 *     'customerId'  => int,     // Optional — associate with Helcim customer
+	 *     'paymentType' => string,  // purchase|preauth|verify (default: purchase)
+	 * ]
 	 */
+	public function initializeCheckout(array $data): array
+	{
+		if (empty($data['amount']) || (float) $data['amount'] <= 0) {
+			return ['success' => false, 'message' => 'Invalid amount'];
+		}
+
+		$paymentMethod = !empty($this->credentials['allow_ach']) ? 'cc-ach' : 'cc';
+
+		$body = [
+			'paymentType'   => $data['paymentType'] ?? 'purchase',
+			'amount'        => (float) $data['amount'],
+			'currency'      => $this->credentials['currency'] ?? 'CAD',
+			'paymentMethod' => $paymentMethod,
+		];
+
+		if (!empty($data['invoiceId'])) {
+			$body['invoiceNumber'] = $data['invoiceId'];
+		}
+
+		if (!empty($data['customerId'])) {
+			$body['customerId'] = (int) $data['customerId'];
+		}
+
+		if (!empty($this->credentials['fee_saver'])) {
+			$body['hasConvenienceFee'] = true;
+		}
+
+		$response = $this->postRequest('helcim-pay/initialize', $body);
+
+		if (is_wp_error($response)) {
+			return ['success' => false, 'message' => $response->get_error_message()];
+		}
+
+		if (empty($response['checkoutToken'])) {
+			return ['success' => false, 'message' => 'Failed to initialize checkout'];
+		}
+
+		return [
+			'success'       => true,
+			'checkoutToken' => $response['checkoutToken'],
+			'secretToken'   => $response['secretToken'] ?? '',
+		];
+	}
+
+	/*****************************************************************
+	 * INVOICES — Helcim is source of truth
+	 *****************************************************************/
+
+	/**
+	 * Get invoices for a customer.
+	 *
+	 * @param array $data ['email' => string] or ['customerId' => int]
+	 */
+	public function handleGetInvoices(array $data): array
+	{
+		$customerId = $data['customerId'] ?? null;
+
+		if (empty($customerId) && !empty($data['email'])) {
+			$customerId = $this->getCustomerIdByEmail($data['email']);
+		}
+
+		if (!$customerId) {
+			return ['success' => true, 'invoices' => []];
+		}
+
+		$response = $this->getRequest('invoices', ['customerId' => $customerId], null, 'minimal');
+
+		if (is_wp_error($response) || !is_array($response)) {
+			return ['success' => false, 'message' => 'Failed to fetch invoices'];
+		}
+
+		return ['success' => true, 'invoices' => $response];
+	}
+
+	/**
+	 * Get a single invoice by ID.
+	 */
+	public function handleGetInvoice(array $data): array
+	{
+		$invoiceId = $data['invoiceId'] ?? null;
+
+		if (!$invoiceId) {
+			return ['success' => false, 'message' => 'Invoice ID required'];
+		}
+
+		$response = $this->getRequest("invoices/{$invoiceId}", [], null, 'minimal');
+
+		if (is_wp_error($response) || !is_array($response)) {
+			return ['success' => false, 'message' => 'Failed to fetch invoice'];
+		}
+
+		return ['success' => true, 'invoice' => $response];
+	}
+
+	/*****************************************************************
+	 * CUSTOMERS & CARDS
+	 *****************************************************************/
+
+	/**
+	 * Find Helcim customer ID by email.
+	 */
+	public function getCustomerIdByEmail(string $email): ?int
+	{
+		$cacheKey = 'customer_email_' . md5($email);
+		$cached   = $this->cache->get($cacheKey);
+
+		if ($cached !== false) {
+			return (int) $cached;
+		}
+
+		$response = $this->getRequest('customers', ['search' => $email], null, 'none', true);
+
+		if (is_wp_error($response) || empty($response)) {
+			return null;
+		}
+
+		$customers = is_array($response) ? $response : [];
+		$emailLower = strtolower($email);
+
+		foreach ($customers as $customer) {
+			$contactEmail = strtolower($customer['contactEmail'] ?? $customer['email'] ?? '');
+			if ($contactEmail === $emailLower) {
+				$this->cache->set($cacheKey, $customer['id'], $this->cacheStrategy['aggressive']);
+				return (int) $customer['id'];
+			}
+		}
+
+		return null;
+	}
+
+	/**
+	 * Get or create a Helcim customer.
+	 */
+	public function getOrCreateCustomer(array $info): ?int
+	{
+		if (empty($info['email'])) {
+			return null;
+		}
+
+		$existing = $this->getCustomerIdByEmail($info['email']);
+		if ($existing) {
+			return $existing;
+		}
+
+		$response = $this->postRequest('customers', [
+			'contactName'  => $info['name'] ?? '',
+			'contactEmail' => $info['email'],
+			'cellphone'    => $info['phone'] ?? '',
+		]);
+
+		if (is_wp_error($response) || empty($response['id'])) {
+			return null;
+		}
+
+		return (int) $response['id'];
+	}
+
+	/**
+	 * Get saved cards for a customer.
+	 */
+	public function handleGetCustomerCards(array $data): array
+	{
+		$customerId = $data['customerId'] ?? null;
+
+		if (empty($customerId) && !empty($data['email'])) {
+			$customerId = $this->getCustomerIdByEmail($data['email']);
+		}
+
+		if (!$customerId) {
+			return ['success' => true, 'cards' => []];
+		}
+
+		$response = $this->getRequest("customers/{$customerId}/cards", [], null, 'moderate');
+
+		if (is_wp_error($response) || !is_array($response)) {
+			return ['success' => false, 'message' => 'Failed to fetch cards'];
+		}
+
+		return ['success' => true, 'cards' => $response];
+	}
+
+	/**
+	 * Get bank accounts for a customer.
+	 */
+	public function getCustomerBankAccounts(int $customerId): array
+	{
+		$response = $this->getRequest("customers/{$customerId}/bank-accounts", [], null, 'moderate');
+		return (!is_wp_error($response) && is_array($response)) ? $response : [];
+	}
+
+	/*****************************************************************
+	 * TRANSACTIONS
+	 *****************************************************************/
+
+	public function getTransactions(array $params = []): array
+	{
+		$response = $this->getRequest('card-transactions', $params, null, 'minimal');
+		return (!is_wp_error($response) && is_array($response)) ? $response : [];
+	}
+
+	public function refundPayment(array $data): array
+	{
+		$response = $this->postRequest('payment/refund', $data);
+
+		if (is_wp_error($response)) {
+			return ['success' => false, 'message' => $response->get_error_message()];
+		}
+
+		return ['success' => true, 'transaction' => $response];
+	}
+
+	/*****************************************************************
+	 * PRODUCT SYNC
+	 *****************************************************************/
+
 	protected function handleTheSavePost(int $postID, \WP_Post $post, bool $update, array $settings): void
 	{
-		// Queue the sync operation
-		$this->queueOperation('sync_to_helcim', [
-			'items' => [$postID],
+		$fields = $this->getSyncFields($postID, 'post', ['share_to_helcim', 'schedule_helcim']);
+
+		if (empty($fields['share_to_helcim'])) {
+			return;
+		}
+
+		// Uses IntegrationExecutor via TypeRegistry instead of FilteredExecutor
+		$this->queueOperation('sync_to', [
+			'items'   => [$postID],
 			'user_id' => $this->userID,
-			'content_type' => $settings['content_type'] ?? 'REGULAR'
 		], [
 			'priority' => 'high',
-			'delay' => 30, // Small delay to batch multiple saves
+			'delay'    => 30,
 		]);
 
 		update_post_meta($postID, BASE . '_helcim_sync_status', 'queued');
 	}
 
-	/**
-	 * Handle post deletion
-	 */
-	public function handleDeletePost(int $postID): void
+	protected function handleImportFromHelcim(): array
 	{
-		$helcim_id = get_post_meta($postID, BASE . '_helcim_product_id', true);
+		$this->queueOperation('import_products', [
+			'user_id' => $this->userID,
+		], ['priority' => 'normal']);
 
-		if ($helcim_id) {
-			$this->queueOperation('delete_from_helcim', [
-				'helcim_ids' => [$helcim_id],
-				'post_id' => $postID
-			], [
-				'priority' => 'high'
-			]);
-		}
+		return ['success' => true, 'message' => 'Import from Helcim queued'];
+	}
+
+	/*****************************************************************
+	 * USER ↔ CUSTOMER LINKING
+	 *****************************************************************/
+
+	public function linkUserToCustomer(int $userId, int $helcimCustomerId): void
+	{
+		update_user_meta($userId, BASE . '_helcim_customer_id', $helcimCustomerId);
+	}
+
+	public function getUserCustomerId(int $userId): ?int
+	{
+		$id = get_user_meta($userId, BASE . '_helcim_customer_id', true);
+		return $id ? (int) $id : null;
 	}
 
 	/**
-	 * Process queued operations
+	 * Resolve customer ID from user meta, falling back to email lookup + auto-link.
 	 */
-	public function processOperation(WP_Error|array $result, object $operation, array $data): WP_Error|array
+	public function resolveCustomerId(int $userId): ?int
 	{
-		$base = strtolower($this->service_name).'_';
-		$helcim = (array_key_exists('user', $data)) ? new self((int)$data['user']) : $this;
-
-		switch ($operation->type) {
-			case $base.'sync_to_helcim':
-				return $helcim->processSyncToHelcim($data);
-
-			case $base.'delete_from_helcim':
-				return $helcim->processDeleteFromHelcim($data);
-
-			case $base.'import_catalog':
-				return $helcim->processImportCatalog($data);
-
-			case $base.'sync_customer':
-				return $helcim->processSyncCustomer($data);
-
-			default:
-				return $result;
-		}
-	}
-
-	/**
-	 * Process sync to Helcim
-	 */
-	private function processSyncToHelcim(array $data): array
-	{
-		$items = $data['items'] ?? [];
-		$content_type = $data['content_type'] ?? 'REGULAR';
-		$success_count = 0;
-		$errors = [];
-
-		foreach ($items as $post_id) {
-			try {
-				$post = get_post($post_id);
-				if (!$post) continue;
-
-				$meta = Meta::forPost($post_id);
-				$field_map = $this->field_mappings[$post->post_type] ?? [];
-
-				// Prepare product data for Helcim
-				$product_data = [
-					'name' => $post->post_title,
-					'description' => $post->post_content,
-					'productCode' => get_post_meta($post_id, BASE . '_helcim_product_code', true) ?: 'WP-' . $post_id,
-					'type' => $content_type,
-					'price' => floatval($meta->get('price')) * 100, // Convert to cents
-					'taxable' => (bool)$meta->get('is_taxable'),
-				];
-
-				// Handle variations
-				$variations = $meta->get('product_variations');
-				if (!empty($variations)) {
-					$product_data['variations'] = $this->prepareVariations($variations);
-				}
-
-				// Check if product exists
-				$helcim_id = get_post_meta($post_id, BASE . '_helcim_product_id', true);
-
-				if ($helcim_id) {
-					// Update existing product
-					$response = $this->putRequest('inventory/product/' . $helcim_id, $product_data);
-				} else {
-					// Create new product
-					$response = $this->postRequest('inventory/product', $product_data);
-
-					if (!is_wp_error($response) && isset($response['productId'])) {
-						update_post_meta($post_id, BASE . '_helcim_product_id', $response['productId']);
-						$helcim_id = $response['productId'];
-					}
-				}
-
-				if (!is_wp_error($response)) {
-					update_post_meta($post_id, BASE . '_helcim_sync_status', 'success');
-					update_post_meta($post_id, BASE . '_helcim_last_sync', current_time('mysql'));
-					$success_count++;
-				} else {
-					throw new Exception($response->get_error_message());
-				}
-
-			} catch (Exception $e) {
-				$errors[] = "Post $post_id: " . $e->getMessage();
-				update_post_meta($post_id, BASE . '_helcim_sync_status', 'failed');
-				update_post_meta($post_id, BASE . '_helcim_sync_error', $e->getMessage());
-			}
+		$id = $this->getUserCustomerId($userId);
+		if ($id) {
+			return $id;
 		}
 
-		return [
-			'success' => count($errors) === 0,
-			'result' => [
-				'synced' => $success_count,
-				'errors' => $errors
-			]
-		];
-	}
-
-	/**
-	 * Prepare variations for Helcim
-	 */
-	private function prepareVariations(array $variations): array
-	{
-		$helcim_variations = [];
-
-		foreach ($variations as $index => $variation) {
-			$helcim_variations[] = [
-				'name' => $variation['name'] ?? '',
-				'price' => floatval($variation['price'] ?? 0) * 100,
-				'sku' => $variation['sku'] ?? '',
-				'inventory' => intval($variation['inventory'] ?? 0),
-			];
-		}
-
-		return $helcim_variations;
-	}
-
-	/**
-	 * Process delete from Helcim
-	 */
-	private function processDeleteFromHelcim(array $data): array
-	{
-		$helcim_ids = $data['helcim_ids'] ?? [];
-		$success_count = 0;
-
-		foreach ($helcim_ids as $helcim_id) {
-			$response = $this->deleteRequest('inventory/product/' . $helcim_id);
-
-			if (!is_wp_error($response)) {
-				$success_count++;
-			}
-		}
-
-		return [
-			'success' => $success_count > 0,
-			'result' => ['deleted' => $success_count]
-		];
-	}
-
-	/**
-	 * Process import from Helcim catalog
-	 */
-	private function processImportCatalog(array $data): array
-	{
-		$page = 1;
-		$imported = 0;
-
-		do {
-			$response = $this->getRequest('inventory/product', [
-				'page' => $page,
-				'limit' => 100
-			]);
-
-			if (is_wp_error($response)) {
-				break;
-			}
-
-			$products = $response['products'] ?? [];
-
-			foreach ($products as $product) {
-				$this->importHelcimProduct($product);
-				$imported++;
-			}
-
-			$page++;
-			$has_more = count($products) === 100;
-
-		} while ($has_more);
-
-		return [
-			'success' => true,
-			'result' => ['imported' => $imported]
-		];
-	}
-
-	/**
-	 * Import a single Helcim product
-	 */
-	private function importHelcimProduct(array $product): void
-	{
-		// Find existing post by Helcim ID
-		$args = [
-			'post_type' => $this->syncPostTypes,
-			'meta_key' => BASE . '_helcim_product_id',
-			'meta_value' => $product['productId'],
-			'posts_per_page' => 1
-		];
-
-		$existing = get_posts($args);
-
-		if ($existing) {
-			$post_id = $existing[0]->ID;
-
-			// Update existing post
-			wp_update_post([
-				'ID' => $post_id,
-				'post_title' => $product['name'],
-				'post_content' => $product['description'] ?? ''
-			]);
-		} else {
-			// Create new post
-			$post_id = wp_insert_post([
-				'post_title' => $product['name'],
-				'post_content' => $product['description'] ?? '',
-				'post_type' => $this->syncPostTypes[0] ?? 'post',
-				'post_status' => 'publish'
-			]);
-		}
-
-		if ($post_id) {
-			// Update meta data
-			$meta = Meta::forPost($post_id);
-			$meta->setAll([
-				'price' => $product['price'] / 100, // Convert from cents
-				'_helcim_product_id' => $product['productId'],
-				'_helcim_product_code' => $product['productCode'],
-				'_helcim_last_sync' => current_time('mysql')
-			]);
-		}
-	}
-
-	/******************************************************************
-	 * CUSTOMER MANAGEMENT
-	 ******************************************************************/
-
-	/**
-	 * Track user login for security
-	 */
-	public function trackUserLogin(string $user_login, \WP_User $user): void
-	{
-		// Check if user has Helcim integration
-		$user_roles = $user->roles;
-
-		foreach ($user_roles as $role) {
-			$role_key = jvbNoBase($role);
-			if (isset(JVB_USER[$role_key]['integrations']['helcim']['is_customer'])) {
-				$login_count = (int)get_user_meta($user->ID, BASE . '_helcim_login_count', true);
-				$login_count++;
-
-				update_user_meta($user->ID, BASE . '_helcim_login_count', $login_count);
-				update_user_meta($user->ID, BASE . '_helcim_last_login', current_time('mysql'));
-
-				// Check if password reset is needed
-				if ($login_count % self::PASSWORD_RESET_INTERVAL === 0) {
-					$this->schedulePasswordReset($user->ID);
-				}
-
-				break;
-			}
-		}
-	}
-
-	/**
-	 * Schedule password reset for security
-	 */
-	private function schedulePasswordReset(int $user_id): void
-	{
-		update_user_meta($user_id, BASE . '_helcim_password_reset_required', true);
-
-		// Send notification
-		$user = get_user_by('ID', $user_id);
-		if ($user) {
-			JVB()->email()->sendEmail(
-				$user->user_email,
-				'Security: Password Reset Required',
-				'For your security, please reset your password to continue accessing your account and saved payment methods.',
-			);
-		}
-	}
-
-	/**
-	 * Handle customer lookup
-	 */
-	public function handleCustomerLookup(WP_REST_Request $request): WP_REST_Response
-	{
-		$email = sanitize_email($request->get_param('email'));
-
-		if (!$email) {
-			return new WP_REST_Response(['error' => 'Email required'], 400);
-		}
-
-		// Check WordPress user first
-		$user = get_user_by('email', $email);
-
-		if ($user) {
-			// Check if user has customer role
-			$has_customer_role = false;
-			foreach ($user->roles as $role) {
-				$role_key = jvbNoBase($role);
-				if (isset(JVB_USER[$role_key]['integrations']['helcim']['is_customer'])) {
-					$has_customer_role = true;
-					break;
-				}
-			}
-
-			if ($has_customer_role) {
-				// Get saved cards and order history
-				$customer_id = get_user_meta($user->ID, BASE . '_helcim_customer_id', true);
-
-				if ($customer_id) {
-					$customer_data = $this->getHelcimCustomer($customer_id);
-
-					return new WP_REST_Response([
-						'exists' => true,
-						'has_account' => true,
-						'customer' => [
-							'name' => $user->display_name,
-							'email' => $user->user_email
-						],
-						'cards' => $customer_data['cards'] ?? [],
-						'orders' => $this->getUserOrders($user->ID)
-					]);
-				}
-			}
-
-			return new WP_REST_Response([
-				'exists' => true,
-				'has_account' => true,
-				'no_customer_role' => true,
-				'message' => 'Account exists but not set up for orders. Would you like to enable ordering?'
-			]);
-		}
-
-		// Check Helcim for customer
-		$helcim_customer = $this->searchHelcimCustomer($email);
-
-		if ($helcim_customer) {
-			return new WP_REST_Response([
-				'exists' => true,
-				'has_account' => false,
-				'helcim_only' => true,
-				'message' => 'Found your previous orders. Create an account to access them?'
-			]);
-		}
-
-		return new WP_REST_Response([
-			'exists' => false,
-			'message' => 'New customer'
-		]);
-	}
-
-	/**
-	 * Get Helcim customer data
-	 */
-	private function getHelcimCustomer(string $customer_id): array
-	{
-		$cached = $this->cache->get('helcim_customer_' . $customer_id);
-
-		if ($cached !== false) {
-			return $cached;
-		}
-
-		$response = $this->getRequest('customer/' . $customer_id);
-
-		if (is_wp_error($response)) {
-			return [];
-		}
-
-		// Get saved cards
-		$cards_response = $this->getRequest('customer/' . $customer_id . '/cards');
-		$cards = [];
-
-		if (!is_wp_error($cards_response) && isset($cards_response['cards'])) {
-			foreach ($cards_response['cards'] as $card) {
-				$cards[] = [
-					'id' => $card['cardToken'],
-					'last_4' => $card['cardLast4'],
-					'card_brand' => $card['cardBrand'],
-					'exp_month' => $card['expiryMonth'],
-					'exp_year' => $card['expiryYear']
-				];
-			}
-		}
-
-		$customer_data = [
-			'customer' => $response,
-			'cards' => $cards
-		];
-
-		$this->cache->set('helcim_customer_' . $customer_id, $customer_data, HOUR_IN_SECONDS);
-
-		return $customer_data;
-	}
-
-	/**
-	 * Search for Helcim customer by email
-	 */
-	private function searchHelcimCustomer(string $email): ?array
-	{
-		$response = $this->getRequest('customer/search', [
-			'email' => $email
-		]);
-
-		if (!is_wp_error($response) && isset($response['customers'][0])) {
-			return $response['customers'][0];
-		}
-
-		return null;
-	}
-
-	/**
-	 * Get user's order history
-	 */
-	private function getUserOrders(int $user_id): array
-	{
-		$orders = get_user_meta($user_id, BASE . '_helcim_orders', true) ?: [];
-
-		// Get last 10 orders
-		return array_slice($orders, -10);
-	}
-
-	/**
-	 * Handle account creation
-	 */
-	public function handleAccountCreation(WP_REST_Request $request): WP_REST_Response
-	{
-		$email = sanitize_email($request->get_param('email'));
-		$name = sanitize_text_field($request->get_param('name'));
-
-		if (!$email || !is_email($email)) {
-			return new WP_REST_Response(['error' => 'Valid email required'], 400);
-		}
-
-		// Check if user already exists
-		if (email_exists($email)) {
-			return new WP_REST_Response([
-				'success' => false,
-				'exists' => true,
-				'message' => 'An account with this email already exists. Please log in instead.'
-			], 409);
-		}
-
-		// Generate username from email
-		$username = sanitize_user(current(explode('@', $email)));
-		$username = $this->generateUniqueUsername($username);
-
-		// Create user account
-		$user_id = wp_create_user(
-			$username,
-			wp_generate_password(20, true, true), // Temporary password
-			$email
-		);
-
-		if (is_wp_error($user_id)) {
-			$this->logError('Failed to create customer account', [
-				'email' => $email,
-				'error' => $user_id->get_error_message()
-			]);
-			return new WP_REST_Response(['error' => 'Failed to create account'], 500);
-		}
-
-		// Set user role
-		$user = new \WP_User($user_id);
-		$user->set_role(BASE.'foodie'); // Or appropriate role from JVB_USER
-
-		// Update display name
-		if ($name) {
-			wp_update_user([
-				'ID' => $user_id,
-				'display_name' => $name
-			]);
-		}
-
-		// Generate password reset key
-		$reset_key = get_password_reset_key($user);
-
-		if (!is_wp_error($reset_key)) {
-			$this->sendWelcomeEmail($user, $reset_key);
-		}
-
-		// Link to Helcim customer if exists
-		$helcim_customer = $this->searchHelcimCustomer($email);
-
-		if ($helcim_customer) {
-			update_user_meta($user_id, BASE . '_helcim_customer_id', $helcim_customer['customerId']);
-		} else {
-			// Create new Helcim customer
-			$customer_response = $this->postRequest('customer', [
-				'customerCode' => 'WP-' . $user_id,
-				'contactName' => $name ?: $username,
-				'email' => $email
-			]);
-
-			if (!is_wp_error($customer_response) && isset($customer_response['customerId'])) {
-				update_user_meta($user_id, BASE . '_helcim_customer_id', $customer_response['customerId']);
-			}
-		}
-
-		return new WP_REST_Response([
-			'success' => true,
-			'message' => 'Account created! Check your email to set your password.',
-			'user_id' => $user_id
-		]);
-	}
-
-	/**
-	 * Generate unique username
-	 */
-	private function generateUniqueUsername(string $base): string
-	{
-		$username = $base;
-		$counter = 1;
-
-		while (username_exists($username)) {
-			$username = $base . $counter;
-			$counter++;
-		}
-
-		return $username;
-	}
-
-	/**
-	 * Send welcome email
-	 */
-	private function sendWelcomeEmail(\WP_User $user, string $reset_key): void
-	{
-		$site_name = get_bloginfo('name');
-		$reset_url = get_home_url(null, "login?action=rp&key=$reset_key&login=" . rawurlencode($user->user_login), 'login');
-
-		$message = sprintf(
-			"Welcome to %s!\n\n" .
-			"Your account has been created. Please click the button below to set your password:\n\n" .
-			"%s\n\n" .
-			"Or, copy and paste the link below:\n\n".
-			"%s\n\n" .
-			"Once you've set your password, you can:\n" .
-			"- View your order history\n" .
-			"- Save your favorite items\n" .
-			"- Speed up checkout with saved payment methods\n\n" .
-			"If you didn't create this account, please ignore this email.\n\n" .
-			"Thanks,\n",
-			$site_name,
-			JVB()->email()->button('Reset Password', $reset_url),
-			JVB()->email()->link($reset_url),
-		);
-
-		JVB()->email()->sendEmail(
-			$user->user_email,
-			sprintf('[%s] Welcome! Set Your Password', $site_name),
-			$message
-		);
-	}
-
-	/******************************************************************
-	 * ORDER PROCESSING
-	 ******************************************************************/
-
-	/**
-	 * Handle checkout
-	 */
-	public function handleCheckout(WP_REST_Request $request): WP_REST_Response
-	{
-		$cart_items = $request->get_param('items');
-		$customer_info = $request->get_param('customer');
-		$payment_token = $request->get_param('payment_token');
-
-		if (empty($cart_items) || empty($payment_token)) {
-			return new WP_REST_Response(['error' => 'Invalid order data'], 400);
-		}
-
-		// Calculate order total
-		$order_total = $this->calculateOrderTotal($cart_items);
-
-		// Create Helcim invoice
-		$invoice_response = $this->createHelcimInvoice($cart_items, $customer_info, $order_total);
-
-		if (is_wp_error($invoice_response)) {
-			return new WP_REST_Response(['error' => $invoice_response->get_error_message()], 500);
-		}
-
-		// Process payment
-		$payment_response = $this->processHelcimPayment($payment_token, $invoice_response['invoiceId'], $order_total);
-
-		if (is_wp_error($payment_response)) {
-			return new WP_REST_Response(['error' => $payment_response->get_error_message()], 500);
-		}
-
-		// Save order to user if logged in
-		if (is_user_logged_in()) {
-			$this->saveOrderToUser(get_current_user_id(), $invoice_response['invoiceId']);
-		}
-
-		return new WP_REST_Response([
-			'success' => true,
-			'order_id' => $invoice_response['invoiceId'],
-			'receipt_url' => $payment_response['receiptUrl'] ?? '',
-			'message' => 'Order placed successfully!'
-		]);
-	}
-
-	/**
-	 * Calculate order total
-	 */
-	private function calculateOrderTotal(array $cart_items): int
-	{
-		$total = 0;
-
-		foreach ($cart_items as $item) {
-			$post_id = intval($item['id'] ?? 0);
-			if (!$post_id) continue;
-
-			$meta = Meta::forPost($post_id);
-			$price = floatval($meta->get('price'));
-			$quantity = intval($item['quantity'] ?? 1);
-
-			$total += ($price * $quantity * 100); // Convert to cents
-		}
-
-		// Add tax
-		$tax_rate = floatval(get_option(BASE . 'helcim_tax_rate', 0.05));
-		$tax = intval($total * $tax_rate);
-
-		return $total + $tax;
-	}
-
-	/**
-	 * Create Helcim invoice
-	 */
-	private function createHelcimInvoice(array $cart_items, array $customer_info, int $total): array|WP_Error
-	{
-		$line_items = [];
-
-		foreach ($cart_items as $item) {
-			$post_id = intval($item['id'] ?? 0);
-			if (!$post_id) continue;
-
-			$post = get_post($post_id);
-			$meta = Meta::forPost($post_id);
-
-			$line_items[] = [
-				'description' => $post->post_title,
-				'quantity' => intval($item['quantity'] ?? 1),
-				'price' => floatval($meta->get('price')) * 100,
-				'productCode' => get_post_meta($post_id, BASE . '_helcim_product_code', true) ?: 'WP-' . $post_id
-			];
-		}
-
-		// Get or create customer
-		$customer_id = $this->getOrCreateHelcimCustomer($customer_info);
-
-		return $this->postRequest('commerce/invoice', [
-			'customerId' => $customer_id,
-			'invoiceNumber' => 'INV-' . time(),
-			'tipAmount' => 0,
-			'depositAmount' => 0,
-			'notes' => $customer_info['notes'] ?? '',
-			'lineItems' => $line_items
-		]);
-	}
-
-	/**
-	 * Process Helcim payment
-	 */
-	private function processHelcimPayment(string $payment_token, string $invoice_id, int $amount): array|WP_Error
-	{
-		return $this->postRequest('payment/purchase', [
-			'paymentToken' => $payment_token,
-			'amount' => $amount,
-			'currency' => 'CAD',
-			'invoiceId' => $invoice_id
-		]);
-	}
-
-	/**
-	 * Get or create Helcim customer
-	 */
-	private function getOrCreateHelcimCustomer(array $customer_info): ?string
-	{
-		if (empty($customer_info['email'])) {
+		$user = get_userdata($userId);
+		if (!$user || empty($user->user_email)) {
 			return null;
 		}
 
-		// Search for existing customer
-		$existing = $this->searchHelcimCustomer($customer_info['email']);
-
-		if ($existing) {
-			return $existing['customerId'];
+		$id = $this->getCustomerIdByEmail($user->user_email);
+		if ($id) {
+			$this->linkUserToCustomer($userId, $id);
 		}
 
-		// Create new customer
-		$response = $this->postRequest('customer', [
-			'customerCode' => 'GUEST-' . time(),
-			'contactName' => $customer_info['name'] ?? '',
-			'email' => $customer_info['email'],
-			'phone' => $customer_info['phone'] ?? ''
-		]);
-
-		if (!is_wp_error($response) && isset($response['customerId'])) {
-			return $response['customerId'];
-		}
-
-		return null;
+		return $id;
 	}
 
+	/*****************************************************************
+	 * VALIDATION
+	 *****************************************************************/
+
 	/**
-	 * Save order to user meta
+	 * Validate a HelcimPay.js transaction using the secret token.
+	 *
+	 * After the frontend receives a SUCCESS message event, call this
+	 * server-side to verify the transaction hash.
 	 */
-	private function saveOrderToUser(int $user_id, string $order_id): void
+	public function validateTransaction(string $secretToken, array $transactionData): bool
 	{
-		$orders = get_user_meta($user_id, BASE . '_helcim_orders', true) ?: [];
-		$orders[] = [
-			'order_id' => $order_id,
-			'date' => current_time('mysql')
-		];
-
-		// Keep only last 50 orders
-		if (count($orders) > 50) {
-			$orders = array_slice($orders, -50);
-		}
-
-		update_user_meta($user_id, BASE . '_helcim_orders', $orders);
+		$hash = hash('sha256', $secretToken . json_encode($transactionData));
+		return hash_equals($hash, $transactionData['hash'] ?? '');
 	}
-
+	/*******************************************************************
+	 * WEBHOOKS
+	*******************************************************************/
 	/**
-	 * Handle order status
-	 */
-	public function handleOrderStatus(WP_REST_Request $request): WP_REST_Response
-	{
-		$order_id = $request->get_param('order_id');
-
-		if (!$order_id) {
-			return new WP_REST_Response(['error' => 'Order ID required'], 400);
-		}
-
-		// Check cache first
-		$cached_status = get_transient(BASE . 'helcim_order_' . $order_id);
-
-		if ($cached_status !== false) {
-			return new WP_REST_Response($cached_status);
-		}
-
-		// Fetch from Helcim
-		$response = $this->getRequest('commerce/invoice/' . $order_id);
-
-		if (is_wp_error($response)) {
-			return new WP_REST_Response(['error' => 'Could not fetch order status'], 500);
-		}
-
-		$status = [
-			'status' => $response['status'] ?? 'unknown',
-			'eta' => $response['estimatedTime'] ?? null,
-			'items' => $response['lineItems'] ?? []
-		];
-
-		// Cache for 1 minute
-		set_transient(BASE . 'helcim_order_' . $order_id, $status, MINUTE_IN_SECONDS);
-
-		return new WP_REST_Response($status);
-	}
-
-	/******************************************************************
-	 * WEBHOOK HANDLING
-	 ******************************************************************/
-
-	/**
-	 * Validate webhook signature
+	 * Validate Helcim webhook signature.
+	 *
+	 * Helcim signs webhooks with HMAC-SHA256 using:
+	 *   signedContent = "{webhook-id}.{webhook-timestamp}.{body}"
+	 *   key = base64_decode(verifierToken)
+	 *
+	 * Headers: webhook-id, webhook-timestamp, webhook-signature (v1,{base64hash})
+	 *
+	 * @see https://devdocs.helcim.com/docs/enabling-webhooks-for-transactions
 	 */
 	protected function validateWebhook(array $payload): bool
 	{
-		$signature = $_SERVER['HTTP_HELCIM_SIGNATURE'] ?? '';
+		$headers = $payload['_headers'] ?? [];
 
-		if (!$signature || !$this->webhook_secret) {
+		$webhookId        = $headers['webhook_id'][0]        ?? $headers['webhook-id']        ?? '';
+		$webhookTimestamp  = $headers['webhook_timestamp'][0] ?? $headers['webhook-timestamp'] ?? '';
+		$webhookSignature = $headers['webhook_signature'][0]  ?? $headers['webhook-signature'] ?? '';
+
+		if (empty($webhookId) || empty($webhookTimestamp) || empty($webhookSignature)) {
+			$this->logError('Webhook missing required headers', [], 'warning');
 			return false;
 		}
 
-		$body = file_get_contents('php://input');
-		$expected = hash_hmac('sha256', $body, $this->webhook_secret);
+		// Verify timestamp is within 5 minutes (prevent replay attacks)
+		$now = time();
+		if (abs($now - (int)$webhookTimestamp) > 300) {
+			$this->logError('Webhook timestamp too old', [
+				'webhook_timestamp' => $webhookTimestamp,
+				'server_time'       => $now,
+			], 'warning');
+			return false;
+		}
 
-		return hash_equals($expected, $signature);
+		$secret = $this->getAdvancedSetting('webhook_signature_key');
+		if (empty($secret)) {
+			// If no signature key configured, allow webhook but log warning
+			$this->logDebug('No webhook signature key configured — skipping verification');
+			return true;
+		}
+
+		// Reconstruct the raw body from the payload (minus our injected _headers)
+		$body = $payload['_raw_body'] ?? json_encode(
+			array_diff_key($payload, ['_headers' => 1, '_raw_body' => 1])
+		);
+
+		$signedContent = "{$webhookId}.{$webhookTimestamp}.{$body}";
+		$secretBytes   = base64_decode($secret);
+		$expectedHash  = base64_encode(
+			hash_hmac('sha256', $signedContent, $secretBytes, true)
+		);
+
+		// webhook-signature may contain multiple signatures: "v1,hash1 v2,hash2"
+		$signatures = explode(' ', $webhookSignature);
+		foreach ($signatures as $sig) {
+			$parts = explode(',', $sig, 2);
+			if (count($parts) === 2 && hash_equals($expectedHash, $parts[1])) {
+				return true;
+			}
+		}
+
+		$this->logError('Webhook signature mismatch', [
+			'webhook_id' => $webhookId,
+		], 'warning');
+
+		return false;
 	}
 
 	/**
-	 * Process webhook event
+	 * Process a validated Helcim webhook event.
+	 *
+	 * Helcim sends minimal payloads: {"id": "12345", "type": "cardTransaction"}
+	 * We fetch the full transaction details from the API.
 	 */
 	protected function processWebhook(array $payload): bool
 	{
-		$event_type = $payload['eventType'] ?? '';
-		$data = $payload['data'] ?? [];
+		$type = $payload['type'] ?? '';
+		$id   = $payload['id']  ?? '';
 
-		switch ($event_type) {
-			case 'transaction.success':
-			case 'transaction.declined':
-				return $this->handleTransactionWebhook($data);
-
-			case 'invoice.paid':
-			case 'invoice.updated':
-				return $this->handleInvoiceWebhook($data);
-
-			case 'customer.created':
-			case 'customer.updated':
-				return $this->handleCustomerWebhook($data);
-
-			default:
-				$this->logDebug('Unhandled webhook type', ['type' => $event_type]);
-				return true;
-		}
-	}
-
-	/**
-	 * Handle transaction webhook
-	 */
-	private function handleTransactionWebhook(array $data): bool
-	{
-		$transaction_id = $data['transactionId'] ?? '';
-		$status = $data['status'] ?? '';
-
-		if (!$transaction_id) {
+		if (empty($type) || empty($id)) {
+			$this->logError('Webhook missing type or id', $payload, 'warning');
 			return false;
 		}
 
-		// Update cached transaction status
-		set_transient(BASE . 'helcim_transaction_' . $transaction_id, $status, HOUR_IN_SECONDS);
-
-		// Trigger action for other integrations
-		do_action(BASE . 'helcim_transaction_updated', $transaction_id, $status, $data);
-
-		return true;
+		return match ($type) {
+			'cardTransaction' => $this->handleTransactionWebhook($id),
+			'terminalCancel'  => $this->handleTerminalCancelWebhook($payload),
+			default           => $this->handleUnknownWebhook($type, $id),
+		};
 	}
 
 	/**
-	 * Handle invoice webhook
+	 * Handle a cardTransaction webhook — fetch full transaction, update records.
 	 */
-	private function handleInvoiceWebhook(array $data): bool
+	protected function handleTransactionWebhook(string $transactionId): bool
 	{
-		$invoice_id = $data['invoiceId'] ?? '';
-		$status = $data['status'] ?? '';
+		// Fetch full transaction from Helcim API
+		$transaction = $this->getRequest("card-transactions/{$transactionId}");
 
-		if (!$invoice_id) {
+		if (is_wp_error($transaction) || empty($transaction)) {
+			$this->logError('Failed to fetch transaction for webhook', [
+				'transaction_id' => $transactionId,
+			]);
 			return false;
 		}
 
-		// Update cached order status
-		set_transient(BASE . 'helcim_order_' . $invoice_id, $status, HOUR_IN_SECONDS);
+		$status = $transaction['status'] ?? '';
 
-		// Trigger action for other integrations
-		do_action(BASE . 'helcim_order_updated', $invoice_id, $status, $data);
+		// Fire action for other parts of the system to react
+		do_action('jvb_helcim_transaction', $transaction, $status);
 
-		return true;
-	}
-
-	/**
-	 * Handle customer webhook
-	 */
-	private function handleCustomerWebhook(array $data): bool
-	{
-		$customer_id = $data['customerId'] ?? '';
-		$email = $data['email'] ?? '';
-
-		if (!$customer_id || !$email) {
-			return false;
+		// If linked to an invoice, update invoice cache
+		$invoiceNumber = $transaction['invoiceNumber'] ?? '';
+		if (!empty($invoiceNumber)) {
+			$this->cache->delete("invoice_{$invoiceNumber}");
+			do_action('jvb_helcim_invoice_updated', $invoiceNumber, $transaction);
 		}
 
-		// Find WordPress user with this Helcim customer ID
-		$users = get_users([
-			'meta_key' => BASE . '_helcim_customer_id',
-			'meta_value' => $customer_id,
-			'number' => 1
+		// Log for debugging
+		$this->logDebug('Transaction webhook processed', [
+			'transaction_id' => $transactionId,
+			'status'         => $status,
+			'amount'         => $transaction['amount'] ?? 0,
+			'type'           => $transaction['type'] ?? '',
 		]);
 
-		if (!empty($users)) {
-			$user = $users[0];
-			update_user_meta($user->ID, BASE . '_helcim_customer_updated', current_time('mysql'));
+		return true;
+	}
 
-			// Clear cached customer data
-			$this->cache->forget('helcim_customer_' . $user->ID);
-		}
+	/**
+	 * Handle Smart Terminal cancel webhook
+	 */
+	protected function handleTerminalCancelWebhook(array $payload): bool
+	{
+		do_action('jvb_helcim_terminal_cancel', $payload);
+
+		$this->logDebug('Terminal cancel webhook processed', [
+			'payload' => $payload,
+		]);
 
 		return true;
 	}
 
-	/******************************************************************
-	 * CONNECTION TESTING
-	 ******************************************************************/
+	/**
+	 * Handle unknown webhook types (future-proofing)
+	 */
+	protected function handleUnknownWebhook(string $type, string $id): bool
+	{
+		$this->logDebug('Unknown webhook type received', [
+			'type' => $type,
+			'id'   => $id,
+		]);
+
+		do_action("jvb_helcim_webhook_{$type}", $id);
+		return true;
+	}
 
 	/**
-	 * Perform connection test
+	 * Extract unique webhook ID for deduplication
 	 */
-	protected function performConnectionTest(): bool
+	protected function extractWebhookId(array $payload): ?string
 	{
-		if (empty($this->api_token) || empty($this->account_id)) {
-			throw new Exception('Missing required credentials');
+		$headers = $payload['_headers'] ?? [];
+		return $headers['webhook_id'][0] ?? $headers['webhook-id'] ?? $payload['id'] ?? null;
+	}
+
+	/**
+	 * Override the webhook request handler to capture raw body for signature verification
+	 */
+	public function handleWebhookRequest(\WP_REST_Request $request): \WP_REST_Response
+	{
+		$payload = $request->get_params();
+		$payload['_headers']  = $request->get_headers();
+		$payload['_raw_body'] = $request->get_body();
+
+		$success = $this->handleWebhook($payload);
+
+		return new \WP_REST_Response([
+			'success' => $success,
+		], $success ? 200 : 400);
+	}
+	/***********************************************************************
+	 * POST HOOKS
+	***********************************************************************/
+	/**
+	 * Sync a WordPress post to Helcim as a product.
+	 * Called by IntegrationExecutor::processSyncTo()
+	 */
+	public function syncPostToService(int $postID): array|\WP_Error
+	{
+		$post = get_post($postID);
+		if (!$post) {
+			return new \WP_Error('not_found', "Post {$postID} not found");
 		}
 
-		$response = $this->getRequest('account');
+		$helcimProductId = get_post_meta($postID, BASE . '_helcim_item_id', true);
+		$productData     = $this->buildProductPayload($postID);
+
+		if (is_wp_error($productData)) {
+			return $productData;
+		}
+
+		if ($helcimProductId) {
+			// Update existing
+			$response = $this->patchRequest("products/{$helcimProductId}", $productData);
+		} else {
+			// Create new
+			$response = $this->postRequest('products', $productData);
+		}
 
 		if (is_wp_error($response)) {
-			throw new Exception($response->get_error_message());
+			update_post_meta($postID, BASE . '_helcim_sync_status', 'error');
+			return $response;
 		}
 
-		return isset($response['accountId']);
+		// Store Helcim product ID
+		$newId = $response['id'] ?? $response['productId'] ?? $helcimProductId;
+		update_post_meta($postID, BASE . '_helcim_item_id', $newId);
+		update_post_meta($postID, BASE . '_helcim_sync_status', 'synced');
+		update_post_meta($postID, BASE . '_helcim_last_sync', current_time('mysql'));
+
+		return ['success' => true, 'helcim_id' => $newId];
 	}
 
 	/**
-	 * Get request headers
+	 * Delete a product from Helcim.
+	 * Called by IntegrationExecutor::processDeleteFrom()
 	 */
-	protected function getRequestHeaders(): array
+	public function deleteFromService(string $externalId): array|\WP_Error
 	{
-		$headers = [
-			'Content-Type' => 'application/json',
-			'Accept' => 'application/json'
-		];
+		$response = $this->deleteRequest("products/{$externalId}");
 
-		// Add authorization header
-		if (!empty($this->api_token)) {
-			$headers['api-token'] = $this->api_token;
+		if (is_wp_error($response)) {
+			return $response;
 		}
 
-		return $headers;
+		return ['success' => true, 'deleted' => $externalId];
 	}
 
-	/******************************************************************
-	 * INTEGRATION ACTIONS
-	 ******************************************************************/
-
-	protected function handleImportFromHelcim()
-	{
-		$this->queueOperation('import_catalog', [
-			'user_id' => $this->userID
-		], [
-			'priority' => 'normal'
-		]);
-
-		return [
-			'success'	=> true,
-			'message'	=> 'Import synced'
-		];
-	}
-
-	protected function handleSyncToHelcim()
-	{
-		$post_types = array_map(function ($type) {
-			return jvbCheckBase($type);
-		}, $this->syncPostTypes);
-
-		// Get all posts to sync
-		$posts = get_posts([
-			'post_type' => $post_types,
-			'posts_per_page' => -1,
-			'post_status' => 'publish'
-		]);
-
-		$post_ids = wp_list_pluck($posts, 'ID');
-
-		// Queue sync operation
-		$this->queueOperation('sync_to_helcim', [
-			'items' => $post_ids,
-			'user_id' => $this->userID
-		], [
-			'priority' => 'normal'
-		]);
-
-		return [
-			'success' => true,
-			'message' => sprintf('Queued %d items for sync to Helcim', count($post_ids))
-		];
-	}
-
-	/******************************************************************
-	 * ADMIN UI
-	 ******************************************************************/
-
 	/**
-	 * Process sync customer operation
+	 * Build Helcim product payload from a WordPress post.
 	 */
-	private function processSyncCustomer(array $data): array
+	protected function buildProductPayload(int $postID): array|\WP_Error
 	{
-		$user_id = $data['user_id'] ?? 0;
+		$meta = \JVBase\meta\Meta::forPost($postID);
+		$post = get_post($postID);
 
-		if (!$user_id) {
-			return [
-				'success' => false,
-				'result' => ['error' => 'No user ID provided']
-			];
-		}
-
-		$user = get_user_by('ID', $user_id);
-		if (!$user) {
-			return [
-				'success' => false,
-				'result' => ['error' => 'User not found']
-			];
-		}
-
-		// Get or create Helcim customer
-		$helcim_customer_id = $this->getOrCreateHelcimCustomer([
-			'email' => $user->user_email,
-			'name' => $user->display_name
-		]);
-
-		if ($helcim_customer_id) {
-			update_user_meta($user_id, BASE . '_helcim_customer_id', $helcim_customer_id);
-
-			return [
-				'success' => true,
-				'result' => ['customer_id' => $helcim_customer_id]
-			];
+		$price = $meta->get('price');
+		if (empty($price) || !is_numeric($price)) {
+			return new \WP_Error('invalid_price', "Post {$postID} has no valid price");
 		}
 
 		return [
-			'success' => false,
-			'result' => ['error' => 'Could not sync customer']
+			'name'        => $post->post_title,
+			'description' => wp_strip_all_tags($post->post_content),
+			'sku'         => $meta->get('sku') ?: "wp-{$postID}",
+			'price'       => (float) $price,
+			'taxExempt'   => (bool) $meta->get('tax_exempt'),
 		];
 	}
 }
diff --git a/inc/integrations/Integrations.php b/inc/integrations/Integrations.php
index e059b5e..a89e56c 100644
--- a/inc/integrations/Integrations.php
+++ b/inc/integrations/Integrations.php
@@ -2998,7 +2998,7 @@
 						$config['value'] = $credentials[$name]??'';
 						$config['autocomplete'] = 'off';
 						$config['base'] = $this->service_name.'_';
-						Form::render($name, null, $config);
+						echo Form::render($name, '', $config);
 					}
 				}
 				if ($this->handleWebhooks) {
diff --git a/inc/integrations/Square.php b/inc/integrations/Square.php
index e0c73c5..8c0f234 100644
--- a/inc/integrations/Square.php
+++ b/inc/integrations/Square.php
@@ -6,6 +6,9 @@
 use Exception;
 use JVBase\registry\PostTypeRegistrar;
 use WP_Error;
+use JVBase\ui\Checkout;
+use JVBase\managers\queue\TypeConfig;
+use JVBase\managers\queue\executors\IntegrationExecutor;
 
 if (!defined('ABSPATH')) {
 	exit;
@@ -843,229 +846,95 @@
 		if (!$this->isSetUp()) {
 			return;
 		}
-		// User login tracking for security
-		add_action('wp_login', [$this, 'trackUserLogin'], 10, 2);
 
-		// Enqueue checkout scripts
+		add_action('wp_login', [$this, 'trackUserLogin'], 10, 2);
 		add_action('wp_enqueue_scripts', [$this, 'enqueueScripts']);
 
-		add_filter('jvbAdditionalActions', [$this, 'outputCheckout']);
-	}
+		// Shared checkout UI (replaces outputCheckout)
+		add_filter('jvbAdditionalActions', [Checkout::class, 'render']);
 
-
-	public function outputCheckout(array $actions):array {
-		if (is_singular(BASE.'dash') || is_post_type_archive(BASE.'dash')) {
-			return $actions;
-		}
-		$form = '<aside id="cart" class="right main">
-			<form id="checkout" data-form-id="checkout" data-save="checkout">';
-
-				$tabs = [
-					'cartItems' => [
-						'title'	=> 'Your Order',
-						'icon'	=> 'cart',
-						'description' => 'Here\'s your order. You can change quantities, remove items, or clear your cart.',
-						'content'	=> $this->cartContent()
-					],
-					'checkout'	=> [
-			'title'	=> 'Checkout',
-			'icon'	=> 'checkout',
-			'description' => 'Securely checkout with your name, email, and payments processed by Square.',
-			'content'	=> '<div class="checkout-section">
-								<h3>Customer Information</h3>
-								'.Form::render('cart_name', null, [
-									'type'		=> 'text',
-									'label'		=> 'Your Name',
-									'required'	=> true,
-									'autocomplete' => 'name'
-								]).
-								Form::render('cart_email', null, [
-									'type'		=> 'email',
-									'label'		=> 'Your Email',
-									'required'	=> true,
-									'autocomplete'=> 'email',
-								]).
-								Form::render('cart_phone', null, [
-									'type'		=> 'tel',
-									'label'		=> 'Your Phone',
-									'required'	=> true,
-									'autocomplete'=> 'phone'
-								]).'
-								<h3>Pickup Details</h3>'.
-								Form::render('pickup_time', null, [
-									'type'		=> 'datetime',
-									'label'		=> 'Pickup Type',
-									'min'		=> '11:00',
-									'max'		=> '20:00',
-									'required'	=> true,
-								]).
-								Form::render('special_instructions', null, [
-									'type'		=> 'textarea',
-									'label'		=> 'Special Instructions',
-									'quill'		=> true,
-								]).'
-								<textarea name="special_instructions" placeholder="Special instructions or dietary notes"></textarea>
-							</div>
-
-							<div class="checkout-section">
-								<h3>Payment Information</h3>
-								<div id="saved-cards"></div>
-								<div id="square-card-container"></div>
-							</div>'
-		],
-					'order'	=> [
-			'title'	=> 'Your Order',
-			'icon' => 'truck',
-			'hidden'	=> true,
-			'description' => '',
-			'content'	=> $this->renderOrderStatus()
-		]
-				];
-		$form .= jvbRenderTabs($tabs, true);
-
-		$form .= '<div class="cart-total row end"><p class="tax">Tax: <span></span></p><p class="total">GRAND TOTAL: <span></span></p></div>
-		</form>
-		</aside>
-		<template class="restoredCart">
-			<div class="restored">
-				<h3>Looks like we left things hanging</h3>
-				<p>We\'ve restored your cart from your last session below.</p>
-				<p>If you\'d rather start over, click the button below.</p>
-				<div class="row btw">
-					<button type="button" onclick="window.squareCheckout.clearCart();this.closest(\'.restored\').remove()">'.jvbIcon('trash').'Clear Cart</button>
-					<button type="button" onclick="this.closest(\'.restored\').remove()">'.jvbIcon('x').'Dismiss</button>
-				</div>
-			</div>
-		</template>
-		<template class="cartItem">
-			<tr class="item">
-				<td class="item">
-					<label for="quantity"></label>
-					<div class="quantity field" data-min="0" data-max="50" data-step="1" data-price="17" data-id="">
-
-						<button type="button" class="decrease"aria-label="Decrease Add to Order">'.jvbIcon('minus-square').'</button>
-
-						<input type="number" id="quantity" name="quantity" value="0" min="0" max="50" step="1" class="quantity-input">
-
-						<button type="button" class="increase" aria-label="Increase Add to Order">'.jvbIcon('plus-square').'</button>
-					</div>
-				</td>
-				<td class="price">
-					<span class="price"></span>
-				</td>
-				<td class="total">
-					<span class="total"></span>
-				</td>
-				<td>
-					<button type="button" data-remove-from-cart>'.jvbIcon('trash').'</button>
-				</td>
-			</tr>
-		</template>
-		<template class="emptyCart">
-			<div class="empty">
-				<p><i><b>No items in cart.</b></i></p>
-				<p>You can <a href="'.get_post_type_archive_link(BASE.'menu_item').'" title="Browse our menu">browse our menu</a> to order.</p>
-			</div>
-		</template>';
-
-
-		$actions[] = [
-			'button' => 	'<button type="button" class="toggle-cart row" title="Your Cart" data-action="toggle-cart" aria-label="Open Cart" aria-controls="checkout" aria-expanded="false">
-					'.jvbIcon('shopping-cart').'<span class="abs"></span><span class="abs count"></span>
-				</button>',
-			'content' =>	$form
-		];
-		return $actions;
-	}
-
-	private function cartContent():string
-	{
-		ob_start();
-		?>
-		<div class="cart-items">
-			<table>
-				<thead>
-					<tr>
-						<th scope="col">Item</th>
-						<th scope="col">Price</th>
-						<th scope="col">Total</th>
-					</tr>
-				</thead>
-				<tbody>
-
-				</tbody>
-			</table>
-		</div>
-
-		<details class="account">
-			<summary>
-				<?php
-				if (is_user_logged_in()) {
-					echo 'Your Favourites and Order History';
-				} else {
-					echo '<a href="'.wp_login_url(get_the_permalink()).'">Log in</a> to save your favourites and view order history.';
-				}
-				?>
-			</summary>
-			<?php
-			if (is_user_logged_in()) {
-				$tabs = [
-					'history' => [
-						'title'	=> 'Order History',
-						'icon' => 'checkout',
-						'description' => 'View your past orders and quickly reorder',
-						'content'	=> $this->renderOrderHistory()
-					],
-					'favourites' => [
-						'title'	=> 'Favourites',
-						'icon'	=> 'heart',
-						'description'	=> 'View your favourites from our menu',
-						'content'		=> $this->renderFavourites()
-					]
-				];
-				jvbRenderTabs($tabs);
+		// Square-specific checkout description
+		add_filter('jvb_checkout_description', function (string $desc, string $provider) {
+			if ($provider === 'square') {
+				return 'Securely checkout with your name, email, and payments processed by Square.';
 			}
+			return $desc;
+		}, 10, 2);
 
-			?>
-		</details>
+		// Square-specific pickup fields (extracted from old outputCheckout)
+		add_filter('jvb_checkout_fields', [$this, 'addPickupFields'], 10, 2);
 
-		<?php
-		return ob_get_clean();
+		// Browse URL for this client (restaurant menu)
+		add_filter('jvb_checkout_browse_url', function () {
+			return get_post_type_archive_link(BASE . 'menu_item');
+		});
+		add_filter('jvb_checkout_browse_text', function () {
+			return 'browse our menu';
+		});
+
+		// Register queue executor types
+		$this->registerQueueTypes();
 	}
 
-	private function renderOrderHistory():string
+	/**
+	 * Pickup/ordering fields for the shared checkout form.
+	 * Specific to this Square client's food ordering use case.
+	 */
+	public function addPickupFields(string $html, string $provider): string
 	{
-		ob_start();
-		//TODO: getRequest, cache for 1 day
-		return ob_get_clean();
-	}
-	private function renderFavourites():string
-	{
-		ob_start();
-		//TODO: get user's favourites and list them
-		return ob_get_clean();
+		if ($provider !== 'square') {
+			return $html;
+		}
+
+		return $html
+			. '<h3>Pickup Details</h3>'
+			. Form::render('pickup_time', null, [
+				'type'     => 'datetime',
+				'label'    => 'Pickup Time',
+				'min'      => '11:00',
+				'max'      => '20:00',
+				'required' => true,
+			])
+			. Form::render('special_instructions', null, [
+				'type'  => 'textarea',
+				'label' => 'Special Instructions',
+				'quill' => true,
+			]);
 	}
 
-	protected function renderOrderStatus():string
+	protected function registerQueueTypes(): void
 	{
-		ob_start();
-		?>
-		<div class="order-confirmation">
-			<h2>Order Confirmed!</h2>
-			<div id="order-status" data-order="">
-				<p>Order #<span class="order-num"></span></p>
-				<div class="status-timeline">
-					<div class="status-item active" data-status="received">Order Received</div>
-					<div class="status-item" data-status="preparing">Preparing</div>
-					<div class="status-item" data-status="ready">Ready for Pickup</div>
-				</div>
-				<div class="pickup-time">
-					Estimated pickup: <span id="eta">Calculating...</span>
-				</div>
-			</div>
-		</div>
-		<?php
-		return ob_get_clean();
+		$queue    = JVB()->queue();
+		$executor = new IntegrationExecutor();
+
+		$queue->registry()->register('square_sync_to', new TypeConfig(
+			executor:   $executor,
+			chunkKey:   'items',
+			chunkSize:  50,
+			maxRetries: 3
+		));
+
+		$queue->registry()->register('square_delete_from', new TypeConfig(
+			executor:   $executor,
+			chunkKey:   'external_ids',
+			chunkSize:  200,
+			maxRetries: 2
+		));
+
+		$queue->registry()->register('square_sync_from', new TypeConfig(
+			executor:   $executor,
+			maxRetries: 3
+		));
+
+		$queue->registry()->register('square_sync_customer', new TypeConfig(
+			executor:   $executor,
+			maxRetries: 2
+		));
+
+		$queue->registry()->register('square_import', new TypeConfig(
+			executor:   $executor,
+			maxRetries: 3
+		));
 	}
 
 	/******************************************************************
@@ -1077,13 +946,12 @@
 	 */
 	protected function handleTheSavePost(int $postID, \WP_Post $post, bool $update, array $settings): void
 	{
-		// Queue the sync operation
-		$this->queueOperation('sync_to_square', [
-			'items' => [$postID],
-			'user_id' => $this->userID
+		$this->queueOperation('sync_to', [
+			'items'   => [$postID],
+			'user_id' => $this->userID,
 		], [
 			'priority' => 'high',
-			'delay' => 30, // Small delay to batch multiple saves
+			'delay'    => 30,
 		]);
 
 		update_post_meta($postID, BASE . '_square_sync_status', 'queued');
@@ -1097,40 +965,35 @@
 		$square_id = get_post_meta($postID, BASE . '_square_catalog_id', true);
 
 		if ($square_id) {
-			$this->queueOperation('delete_from_square', [
-				'square_ids' => [$square_id],
-				'post_id' => $postID
+			$this->queueOperation('delete_from', [
+				'external_ids' => [$square_id],
+				'post_id'      => $postID,
 			], [
-				'priority' => 'high'
+				'priority' => 'high',
 			]);
 		}
 	}
 
 	/**
-	 * Process queued operations
+	 * @deprecated IntegrationExecutor handles new operations via registerQueueTypes().
+	 * Kept for legacy-typed operations ('square_sync_to_square') already queued.
+	 * Safe to remove once all legacy operations have been processed.
 	 */
 	public function processOperation(WP_Error|array $result, object $operation, array $data): WP_Error|array
 	{
-		$base = strtolower($this->service_name).'_';
-		$square = (array_key_exists('user', $data)) ? new self((int)$data['user']) : $this;
-		switch ($operation->type) {
-			case $base.'sync_to_square':
-				return $square->processSyncToSquare($data);
+		$base   = strtolower($this->service_name) . '_';
+		$square = array_key_exists('user', $data) ? new self((int) $data['user']) : $this;
 
-			case $base.'delete_from_square':
-				return $square->processDeleteFromSquare($data);
-
-			case $base.'sync_from_square':
-				return $square->processSyncFromSquare($data);
-
-			case $base.'sync_customer':
-				return $square->processSyncCustomer($data);
-
-			default:
-				return $result;
-		}
+		return match ($operation->type) {
+			$base . 'sync_to_square'     => $square->processSyncToSquare($data),
+			$base . 'delete_from_square' => $square->processDeleteFromSquare($data),
+			$base . 'sync_from_square'   => $square->processSyncFromSquare($data),
+			$base . 'sync_customer'      => $square->processSyncCustomer($data),
+			default                      => $result,
+		};
 	}
 
+
 	/**
 	 * Process sync to Square
 	 */
@@ -2097,7 +1960,7 @@
 	/**
 	 * Enqueue checkout scripts with Square configuration
 	 */
-	public function enqueueScripts():void
+	public function enqueueScripts(): void
 	{
 		$this->loadCredentials();
 		$sdk_url = $this->environment === 'production'
@@ -2109,50 +1972,40 @@
 			$sdk_url,
 			[],
 			null,
-			[
-				'strategy' => 'defer',
-				'in_footer' => true
-			]
+			['strategy' => 'defer', 'in_footer' => true]
 		);
 
-		// Register your custom checkout script
+		// Shared cart checkout base class
+		wp_register_script(
+			'jvb-checkout',
+			JVB_URL . 'assets/js/min/checkout.min.js',
+			['jvb-utility', 'jvb-queue', 'jvb-a11y', 'jvb-cache', 'jvb-tabs', 'jvb-popup'],
+			'1.1.31',
+			['strategy' => 'defer', 'in_footer' => true]
+		);
+
+		// Square checkout extends CartCheckout
 		wp_register_script(
 			'jvb-square-checkout',
 			JVB_URL . 'assets/js/min/square.min.js',
-			[
-//				'square-payments-sdk',
-				'jvb-utility',
-				'jvb-queue',
-				'jvb-a11y',
-				'jvb-cache',
-				'jvb-tabs',
-				'jvb-popup'
-			],
-			'1.0.0',
-			[
-				'strategy' => 'defer',
-				'in_footer' => true
-			]
+			['jvb-checkout', 'square-payments-sdk'],
+			'1.1.31',
+			['strategy' => 'defer', 'in_footer' => true]
 		);
 
 		wp_enqueue_script('jvb-square-checkout');
 
-		// Localize the checkout script with Square config
-		wp_localize_script(
-			'jvb-square-checkout',
-			'squareConfig',
-			[
-				'isOpen' => jvbIsOpen(),
-				'application_id' => $this->credentials['client_id'] ?? '',
-				'location_id' => $this->locationId,
-				'environment' => $this->environment,
-				'api_url' => rest_url('jvb/v1/square/'),
-				'nonce' => wp_create_nonce('wp_rest'),
-				'currency' => get_option(BASE . 'currency', 'CAD'),
-				'is_logged_in' => is_user_logged_in(),
-				'user_email' => is_user_logged_in() ? wp_get_current_user()->user_email : '' // NEW
-			]
-		);
+		wp_localize_script('jvb-square-checkout', 'squareConfig', [
+			'isOpen'         => jvbIsOpen(),
+			'application_id' => $this->credentials['client_id'] ?? '',
+			'location_id'    => $this->locationId,
+			'environment'    => $this->environment,
+			'api_url'        => rest_url('jvb/v1/square/'),
+			'nonce'          => wp_create_nonce('wp_rest'),
+			'currency'       => get_option(BASE . 'currency', 'CAD'),
+			'is_logged_in'   => is_user_logged_in(),
+			'user_email'     => is_user_logged_in() ? wp_get_current_user()->user_email : '',
+		]);
 	}
 
 	/******************************************************************
@@ -3683,4 +3536,74 @@
 
 		return null;
 	}
+
+	/**
+	 * Single-item sync. Called by IntegrationExecutor::processSyncTo().
+	 * Delegates to syncBatchToService since Square uses batch-upsert.
+	 */
+	public function syncPostToService(int $postID): array|WP_Error
+	{
+		return $this->syncBatchToService(['items' => [$postID]]);
+	}
+
+	/**
+	 * Batch sync — preferred by IntegrationExecutor when available.
+	 * Wraps existing processSyncToSquare which already handles batches.
+	 */
+	public function syncBatchToService(array $data): array|WP_Error
+	{
+		$result = $this->processSyncToSquare($data);
+
+		if (empty($result['success'])) {
+			$errors = implode(', ', $result['result']['errors'] ?? ['Sync failed']);
+			return new WP_Error('square_sync_failed', $errors);
+		}
+
+		return $result;
+	}
+
+	/**
+	 * Delete catalog object from Square.
+	 * Called by IntegrationExecutor::processDeleteFrom().
+	 */
+	public function deleteFromService(string $externalId): array|WP_Error
+	{
+		$result = $this->processDeleteFromSquare(['square_ids' => [$externalId]]);
+
+		if (empty($result['success'])) {
+			return new WP_Error('square_delete_failed', $result['result']['error'] ?? 'Delete failed');
+		}
+
+		return $result;
+	}
+
+	/**
+	 * Import from Square catalog → WordPress.
+	 * Called by IntegrationExecutor::processImport().
+	 */
+	public function importFromService(array $data): array|WP_Error
+	{
+		$result = $this->processSyncFromSquare($data);
+
+		if (empty($result['success'])) {
+			return new WP_Error('square_import_failed', $result['result']['error'] ?? 'Import failed');
+		}
+
+		return $result;
+	}
+
+	/**
+	 * Sync customer to Square.
+	 * Called by IntegrationExecutor::processSyncCustomer().
+	 */
+	public function syncCustomer(array $data): array|WP_Error
+	{
+		$result = $this->processSyncCustomer($data);
+
+		if (empty($result['success'])) {
+			return new WP_Error('square_customer_sync_failed', $result['result']['error'] ?? 'Customer sync failed');
+		}
+
+		return $result;
+	}
 }
diff --git a/inc/managers/AdminPages.php b/inc/managers/AdminPages.php
index cf42870..866893c 100644
--- a/inc/managers/AdminPages.php
+++ b/inc/managers/AdminPages.php
@@ -2,6 +2,8 @@
 namespace JVBase\managers;
 
 use JVBase\utility\Features;
+use JVBase\rest\Route;
+use JVBase\rest\PermissionHandler;
 use WP_REST_Response;
 
 if (!defined('ABSPATH')) {
@@ -47,182 +49,11 @@
 
 		add_filter(BASE.'admin_action_filter', [$this, 'handleCacheActions'], 10, 3);
 
-		add_action('rest_api_init', [$this, 'registerRestRoutes']);
 		// Handle form submissions
 		add_action('admin_init', [$this, 'handleAdminPageSubmission']);
 		add_action('admin_notices', [$this, 'displayAdminNotices']);
     }
 
-	/**
-	 * Register REST API routes for admin actions
-	 */
-	public function registerRestRoutes(): void
-	{
-		register_rest_route('jvb/v1', '/admin-cache', [
-			'methods' => 'POST',
-			'callback' => [$this, 'handleCacheAction'],
-			'permission_callback' => [$this, 'checkAdminPermission']
-		]);
-
-		register_rest_route('jvb/v1', '/admin-icons', [
-			'methods' => 'POST',
-			'callback' => [$this, 'handleIconAction'],
-			'permission_callback' => [$this, 'checkAdminPermission']
-		]);
-	}
-
-	/**
-	 * Check if user has admin permissions
-	 */
-	public function checkAdminPermission(\WP_REST_Request $request): bool
-	{
-		if (!current_user_can('manage_options')) {
-			return false;
-		}
-
-		// Verify nonce
-		$nonce = $request->get_header('X-WP-Nonce');
-		if (!wp_verify_nonce($nonce, 'wp_rest')) {
-			return false;
-		}
-
-		return true;
-	}
-
-	/**
-	 * Handle cache-related actions
-	 */
-	public function handleCacheAction(\WP_REST_Request $request): \WP_REST_Response
-	{
-		$action = sanitize_text_field($request->get_param('action'));
-
-		switch ($action) {
-			case 'flush-all':
-				$total = Cache::flushAll();
-				return new \WP_REST_Response([
-					'success' => true,
-					'message' => $total.' caches flushed successfully'
-				]);
-
-			case 'flush-cache':
-				$group = sanitize_text_field($request->get_param('group'));
-				if (empty($group)) {
-					return new \WP_REST_Response([
-						'success' => false,
-						'message' => 'No cache group specified'
-					], 400);
-				}
-
-				Cache::for($group)?->flush();
-
-				return new \WP_REST_Response([
-					'success' => true,
-					'message' => "Cache group '{$group}' flushed successfully"
-				]);
-
-			default:
-				return new \WP_REST_Response([
-					'success' => false,
-					'message' => 'Invalid action'
-				], 400);
-		}
-	}
-
-	/**
-	 * Handle icon-related actions
-	 */
-	public function handleIconAction(\WP_REST_Request $request): \WP_REST_Response
-	{
-		$action = sanitize_text_field($request->get_param('action'));
-		$source = sanitize_text_field($request->get_param('source') ?? 'icons');
-		$icons = IconsManager::for($source);
-
-		switch ($action) {
-			case 'refresh-icons':
-				// Force regenerate CSS immediately
-				$icons->forceRefresh();
-				IconsManager::regenerateAllCSS([$source => true]);
-
-				return new \WP_REST_Response([
-					'success' => true,
-					'message' => "Icon CSS regenerated successfully for '{$source}'"
-				]);
-
-			case 'refresh-all-icons':
-				// Regenerate all icon sources
-				foreach (['icons', 'forms', 'dash'] as $src) {
-					IconsManager::for($src)->forceRefresh();
-				}
-				IconsManager::regenerateAllCSS();
-
-				return new \WP_REST_Response([
-					'success' => true,
-					'message' => 'All icon CSS files regenerated successfully'
-				]);
-
-			case 'restore-icon-version':
-				$timestamp = (int)$request->get_param('timestamp');
-				if (empty($timestamp)) {
-					return new \WP_REST_Response([
-						'success' => false,
-						'message' => 'No timestamp provided'
-					], 400);
-				}
-
-				if ($icons->restoreVersion($timestamp)) {
-					return new \WP_REST_Response([
-						'success' => true,
-						'message' => 'Icon version restored successfully'
-					]);
-				}
-
-				return new \WP_REST_Response([
-					'success' => false,
-					'message' => 'Failed to restore icon version'
-				], 500);
-
-			case 'merge-icon-versions':
-				$timestamps = $request->get_param('timestamps');
-
-				if (empty($timestamps) || !is_array($timestamps)) {
-					return new \WP_REST_Response([
-						'success' => false,
-						'message' => 'No versions selected for merging'
-					], 400);
-				}
-
-				$timestamps = array_map('intval', $timestamps);
-
-				if (count($timestamps) < 2) {
-					return new \WP_REST_Response([
-						'success' => false,
-						'message' => 'Please select at least 2 versions to merge'
-					], 400);
-				}
-
-				if ($icons->mergeVersions($timestamps)) {
-					// Regenerate CSS after merge
-					IconsManager::regenerateAllCSS([$source => true]);
-
-					return new \WP_REST_Response([
-						'success' => true,
-						'message' => 'Icon versions merged successfully'
-					]);
-				}
-
-				return new \WP_REST_Response([
-					'success' => false,
-					'message' => 'Failed to merge icon versions'
-				], 500);
-
-			default:
-				return new \WP_REST_Response([
-					'success' => false,
-					'message' => 'Invalid action'
-				], 400);
-		}
-	}
-
     /**
      * Register a subpage to appear under the main settings page
      *
@@ -615,40 +446,41 @@
      *
      * @param string $hook Current admin page
      */
-    public function enqueueAdminAssets(string $hook):void
-    {
-        // Check if we're on an Edmonton Ink admin page
-        if (strpos($hook, BASE) === false) {
-            return;
-        }
+	public function enqueueAdminAssets(string $hook):void
+	{
+		// More robust check for JVB admin pages
+		if (strpos($hook, BASE) === false) {
+			return;
+		}
 
-        // Enqueue admin styles
-        wp_enqueue_style(
-            'jvb-admin-styles',
-            JVB_URL . 'assets/css/admin.css',
-            [],
-            '1.0.0'
-        );
+		// Enqueue admin styles
+		wp_enqueue_style(
+			'jvb-admin-styles',
+			JVB_URL . 'assets/css/admin.css',
+			[],
+			'1.1'
+		);
 
-        // Enqueue admin scripts
-        wp_enqueue_script(
-            'jvb-admin-scripts',
-            JVB_URL . 'assets/js/admin.js',
-            [],
-            '1.0.0',
-            true
-        );
+		// Enqueue admin scripts - make sure jvb-auth is loaded first
+		wp_enqueue_script(
+			'jvb-admin-scripts',
+			JVB_URL . 'assets/js/admin.js',
+			['jvb-auth'],
+			'1.1',
+			['strategy' => 'defer', 'in_footer' => true]
+		);
 
-        wp_localize_script(
-            'jvb-admin-scripts',
-            'jvbSettings',
-            [
-                'api'    => rest_url('jvb/v1/admin-action'),
-                'nonce'     => wp_create_nonce('wp_rest'),
-                'action' => wp_create_nonce('itsme'),
-            ]
-        );
-    }
+		// Localize to jvb-admin-scripts as well for redundancy
+		wp_localize_script(
+			'jvb-auth',
+			'jvbSettings',
+			[
+				'api'    => rest_url('jvb/v1/'),
+				'nonce'  => wp_create_nonce('wp_rest'),
+				'action' => wp_create_nonce('itsme'),
+			]
+		);
+	}
 
     /**
      * Create a custom SVG icon for the admin menu
@@ -679,11 +511,6 @@
 	{
 		$groups = Cache::getAllGroups();
 
-		// Separate generic vs. specific caches
-		$generic_groups = [];
-		$content_specific = [];
-		$nonce = wp_create_nonce('wp_rest');
-
 		// Separate by type
 		$generic = [];
 		$specific = [];
@@ -701,7 +528,9 @@
 			<h1>Cache Management</h1>
 
 			<div class="jvb-cache-actions">
-				<button type="button" class="button button-primary" data-action="flush-all">
+				<button type="button"
+						class="button button-primary"
+						data-cache-action="flush-all">
 					<?= jvbDashIcon('arrows-clockwise'); ?>
 					Flush All Caches
 				</button>
@@ -718,15 +547,18 @@
 					</tr>
 					</thead>
 					<tbody>
-					<?php if (empty($generic_groups)): ?>
+					<?php if (empty($generic)): ?>
 						<tr><td colspan="3">No generic caches registered</td></tr>
 					<?php else: ?>
-						<?php foreach ($generic_groups as $group => $configs): ?>
+						<?php foreach ($generic as $group => $data): ?>
 							<tr>
 								<td><strong><?= esc_html($group); ?></strong></td>
-								<td><?= $this->formatConnections($configs); ?></td>
+								<td><?= $this->formatConnections($data); ?></td>
 								<td>
-									<button type="button" class="button" data-action="flush-cache" data-group="<?= esc_attr($group); ?>">
+									<button type="button"
+											class="button"
+											data-cache-action="flush-cache"
+											data-group="<?= esc_attr($group); ?>">
 										<?= jvbDashIcon('trash'); ?> Flush
 									</button>
 								</td>
@@ -748,24 +580,14 @@
 					</tr>
 					</thead>
 					<tbody>
-					<?php foreach ($specific as $group => $configs): ?>
-						<tr>
-							<td><strong><?= esc_html($group); ?></strong></td>
-							<td><?= $this->formatConnections($configs); ?></td>
-							<td>
-								<button type="button" class="button" data-action="flush-cache" data-group="<?= esc_attr($group); ?>">
-									<?= jvbDashIcon('trash'); ?> Flush
-								</button>
-							</td>
-						</tr>
-					<?php endforeach; ?>
-					<?php foreach ($generic as $group => $data): ?>
+					<?php foreach ($specific as $group => $data): ?>
 						<tr>
 							<td><strong><?= esc_html($group); ?></strong></td>
 							<td><?= $this->formatConnections($data); ?></td>
 							<td>
-								<button type="button" class="button"
-										data-action="flush-cache"
+								<button type="button"
+										class="button"
+										data-cache-action="flush-cache"
 										data-group="<?= esc_attr($group); ?>">
 									<?= jvbDashIcon('trash'); ?> Flush
 								</button>
@@ -776,57 +598,6 @@
 				</table>
 			</details>
 		</div>
-		<script>
-			(function() {
-				const apiUrl = '<?= esc_js(rest_url('jvb/v1/admin-cache')); ?>';
-				const nonce = '<?= esc_js($nonce); ?>';
-
-				function callCacheAction(action, data = {}) {
-					const body = { action, ...data };
-
-					return fetch(apiUrl, {
-						method: 'POST',
-						headers: {
-							'Content-Type': 'application/json',
-							'X-WP-Nonce': nonce
-						},
-						body: JSON.stringify(body)
-					})
-						.then(response => response.json())
-						.then(data => {
-							if (data.success) {
-								alert(data.message || 'Success!');
-								location.reload();
-							} else {
-								alert('Error: ' + (data.message || 'Unknown error'));
-							}
-						})
-						.catch(error => {
-							alert('Network error: ' + error.message);
-							console.error('Error:', error);
-						});
-				}
-
-				// Flush all caches
-				document.querySelector('[data-action="flush-all"]')?.addEventListener('click', function() {
-					if (confirm('Flush all caches? This may temporarily slow down your site.')) {
-						this.disabled = true;
-						callCacheAction('flush-all');
-					}
-				});
-
-				// Flush individual cache groups
-				document.querySelectorAll('[data-action="flush-cache"]').forEach(btn => {
-					btn.addEventListener('click', function() {
-						const group = this.getAttribute('data-group');
-						if (confirm(`Flush cache group "${group}"?`)) {
-							this.disabled = true;
-							callCacheAction('flush-cache', { group: group });
-						}
-					});
-				});
-			})();
-		</script>
 		<?php
 	}
 
@@ -915,11 +686,10 @@
 		$current_source = sanitize_text_field($current_source);
 
 		// Get all registered icon sources
-		$all_sources = ['icons', 'forms', 'dash']; // You could get this dynamically if needed
+		$all_sources = ['icons', 'forms', 'dash'];
 
 		$icons = IconsManager::for($current_source);
 		$versions = $icons->getVersionHistory();
-		$nonce = wp_create_nonce('wp_rest');
 
 		?>
 		<div class="wrap jvb-admin-wrap">
@@ -928,9 +698,11 @@
 			<!-- Source Selector -->
 			<div class="jvb-icon-source-selector">
 				<label for="icon-source-select">Icon Source:</label>
-				<select id="icon-source-select" onchange="window.location.href='<?= admin_url('admin.php?page=' . BASE . 'icons&icon_source='); ?>' + this.value">
+				<select id="icon-source-select"
+						onchange="window.location.href='<?= admin_url('admin.php?page=' . BASE . 'icons&icon_source='); ?>' + this.value">
 					<?php foreach ($all_sources as $source): ?>
-						<option value="<?= esc_attr($source); ?>" <?= selected($current_source, $source, false); ?>>
+						<option value="<?= esc_attr($source); ?>"
+							<?= selected($current_source, $source, false); ?>>
 							<?= esc_html(ucfirst($source)); ?>
 						</option>
 					<?php endforeach; ?>
@@ -938,11 +710,19 @@
 			</div>
 
 			<div class="jvb-icon-actions">
-				<button type="button" class="button button-primary" data-action="refresh-icons" data-source="<?= esc_attr($current_source); ?>">
+				<button type="button"
+						class="button button-primary"
+						data-icon-action="refresh-icons"
+						data-source="<?= esc_attr($current_source); ?>">
 					<?= jvbDashIcon('arrows-clockwise'); ?>
 					Force Refresh CSS
 				</button>
-				<button type="button" class="button" data-action="merge-icon-versions" data-source="<?= esc_attr($current_source); ?>" id="merge-versions-btn" disabled>
+				<button type="button"
+						class="button"
+						data-icon-action="merge-icon-versions"
+						data-source="<?= esc_attr($current_source); ?>"
+						id="merge-versions-btn"
+						disabled>
 					<?= jvbDashIcon('git-merge'); ?>
 					Merge Selected Versions
 				</button>
@@ -985,15 +765,18 @@
 							</td>
 							<td><?= esc_html($version['size_formatted']); ?></td>
 							<td>
-								<button type="button" class="button restore-version-btn"
-										data-action="restore-icon-version"
+								<button type="button"
+										class="button restore-version-btn"
+										data-icon-action="restore-icon-version"
 										data-source="<?= esc_attr($current_source); ?>"
 										data-timestamp="<?= esc_attr($version['timestamp']); ?>">
 									<?= jvbDashIcon('arrow-counter-clockwise'); ?> Restore
 								</button>
 							</td>
 						</tr>
-						<tr id="icon-list-<?= esc_attr($version['timestamp']); ?>" class="icon-list-row" style="display: none;">
+						<tr id="icon-list-<?= esc_attr($version['timestamp']); ?>"
+							class="icon-list-row"
+							style="display: none;">
 							<td colspan="5">
 								<div class="icon-list-content">
 									<?php foreach ($version['iconList'] as $style => $icons): ?>
@@ -1008,108 +791,6 @@
 				</tbody>
 			</table>
 		</div>
-
-		<script>
-			(function() {
-				const apiUrl = '<?= esc_js(rest_url('jvb/v1/admin-icons')); ?>';
-				const nonce = '<?= esc_js($nonce); ?>';
-				const currentSource = '<?= esc_js($current_source); ?>';
-
-				// Helper function for API calls
-				function callIconAction(action, data = {}) {
-					const body = { action, source: currentSource, ...data };
-
-					return fetch(apiUrl, {
-						method: 'POST',
-						headers: {
-							'Content-Type': 'application/json',
-							'X-WP-Nonce': nonce
-						},
-						body: JSON.stringify(body)
-					})
-						.then(response => response.json())
-						.then(data => {
-							if (data.success) {
-								alert(data.message || 'Success!');
-								location.reload();
-							} else {
-								alert('Error: ' + (data.message || 'Unknown error'));
-							}
-							return data;
-						})
-						.catch(error => {
-							alert('Network error: ' + error.message);
-							console.error('Error:', error);
-						});
-				}
-
-				// Enable/disable merge button based on selection
-				document.querySelectorAll('.version-checkbox').forEach(checkbox => {
-					checkbox.addEventListener('change', function() {
-						const checkedCount = document.querySelectorAll('.version-checkbox:checked').length;
-						document.getElementById('merge-versions-btn').disabled = checkedCount < 2;
-					});
-				});
-
-				// Select all functionality
-				const selectAll = document.getElementById('select-all-versions');
-				if (selectAll) {
-					selectAll.addEventListener('change', function() {
-						document.querySelectorAll('.version-checkbox').forEach(checkbox => {
-							checkbox.checked = this.checked;
-							checkbox.dispatchEvent(new Event('change'));
-						});
-					});
-				}
-
-				// Toggle icon list view
-				document.querySelectorAll('.view-icon-list-btn').forEach(btn => {
-					btn.addEventListener('click', function() {
-						const timestamp = this.getAttribute('data-timestamp');
-						const row = document.getElementById('icon-list-' + timestamp);
-						if (row) {
-							row.style.display = row.style.display === 'none' ? '' : 'none';
-						}
-					});
-				});
-
-				// Force refresh button
-				document.querySelector('[data-action="refresh-icons"]')?.addEventListener('click', function() {
-					if (confirm('Force regenerate icon CSS? This will reload the page.')) {
-						this.disabled = true;
-						callIconAction('refresh-icons');
-					}
-				});
-
-				// Merge versions button
-				document.getElementById('merge-versions-btn')?.addEventListener('click', function() {
-					const checkboxes = document.querySelectorAll('.version-checkbox:checked');
-					const timestamps = Array.from(checkboxes).map(cb => parseInt(cb.value));
-
-					if (timestamps.length < 2) {
-						alert('Please select at least 2 versions to merge');
-						return;
-					}
-
-					if (confirm(`Merge ${timestamps.length} versions? This will create a new CSS file with all unique icons.`)) {
-						this.disabled = true;
-						callIconAction('merge-icon-versions', { timestamps: timestamps });
-					}
-				});
-
-				// Restore version buttons
-				document.querySelectorAll('.restore-version-btn').forEach(btn => {
-					btn.addEventListener('click', function() {
-						const timestamp = parseInt(this.getAttribute('data-timestamp'));
-
-						if (confirm('Restore this icon version? This will reload the page.')) {
-							this.disabled = true;
-							callIconAction('restore-icon-version', { timestamp: timestamp });
-						}
-					});
-				});
-			})();
-		</script>
 		<?php
 	}
 
diff --git a/inc/managers/ScriptLoader.php b/inc/managers/ScriptLoader.php
index d953e55..623eb88 100644
--- a/inc/managers/ScriptLoader.php
+++ b/inc/managers/ScriptLoader.php
@@ -2,7 +2,7 @@
 add_action('init', 'jvbRegisterScripts', 5);
 
 function jvbRegisterScripts() {
-	$version = '1.1.31';
+	$version = '1.1.33';
 	$strategy = [
 		'strategy'	=> 'defer',
 		'in_footer'	=> true
diff --git a/inc/managers/_setup.php b/inc/managers/_setup.php
index 880d9b2..50f6299 100644
--- a/inc/managers/_setup.php
+++ b/inc/managers/_setup.php
@@ -18,7 +18,7 @@
 
 	Cache::registerHooks();
 	// Initialize base sources (this registers hooks and includes defaults)
-	IconsManager::for('icons');
+	IconsManager::for();
 	IconsManager::for('forms');
 
 	// Only initialize dash if feature is enabled
diff --git a/inc/managers/queue/Storage.php b/inc/managers/queue/Storage.php
index e4a5397..add78a5 100644
--- a/inc/managers/queue/Storage.php
+++ b/inc/managers/queue/Storage.php
@@ -138,8 +138,8 @@
 			return false;
 		}
 
-		Cache::invalidateGroup('queue');
-//		$this->invalidateUser($op->userId);
+
+		$this->invalidateUser($op->userId);
 
 		return true;
 	}
@@ -184,9 +184,8 @@
 			error_log('[Storage::saveFinal] DB error: ' . $wpdb->last_error);
 			return false;
 		}
-		Cache::invalidateGroup('queue');
-//		$this->invalidateQueueCache();
-//		$this->invalidateUser($op->userId);
+		$this->invalidateQueueCache();
+		$this->invalidateUser($op->userId);
 
 		return true;
 	}
@@ -219,8 +218,7 @@
 		]);
 
 		if ($result) {
-//			$this->invalidateUser($op->userId);
-			Cache::invalidateGroup('queue');
+			$this->invalidateUser($op->userId);
 		}
 
 		return $result !== false;
@@ -450,8 +448,7 @@
 
 	private function invalidateUser(int $userId): void
 	{
-		$this->cache->forget($userId);
-		Cache::touch($userId);
+		Cache::for($userId.'_queue')->flush();
 	}
 	public function getLastError(): string
 	{
diff --git a/inc/managers/queue/_setup.php b/inc/managers/queue/_setup.php
index f7cbbba..37de16b 100644
--- a/inc/managers/queue/_setup.php
+++ b/inc/managers/queue/_setup.php
@@ -30,6 +30,7 @@
 require_once JVB_DIR . '/inc/managers/queue/executors/ContentExecutor.php';
 require_once JVB_DIR . '/inc/managers/queue/executors/ContentTermExecutor.php';
 require_once JVB_DIR . '/inc/managers/queue/executors/InvitationExecutor.php';
+require_once JVB_DIR . '/inc/managers/queue/executors/IntegrationExecutor.php';
 
 // Facade
 require_once JVB_DIR . '/inc/managers/queue/Queue.php';
diff --git a/inc/managers/queue/executors/IntegrationExecutor.php b/inc/managers/queue/executors/IntegrationExecutor.php
new file mode 100644
index 0000000..ea79960
--- /dev/null
+++ b/inc/managers/queue/executors/IntegrationExecutor.php
@@ -0,0 +1,287 @@
+<?php
+namespace JVBase\managers\queue\executors;
+
+use JVBase\managers\queue\{Executor, Operation, Progress, Result};
+use Exception;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+/**
+ * Executor for integration-related queue operations.
+ *
+ * Routes operations to the correct integration instance based on the
+ * operation type prefix (e.g. 'helcim_sync_product' → Helcim class).
+ *
+ * Handles: {integration}_sync_to, {integration}_delete_from,
+ *          {integration}_sync_from, {integration}_sync_customer, etc.
+ *
+ * Registration (in Integrations::registerAdditionalHooks):
+ *   $queue = JVB()->queue();
+ *   $queue->registry()->register('helcim_sync_to', new TypeConfig(
+ *       executor: new IntegrationExecutor(),
+ *       chunkKey: 'items',
+ *       chunkSize: 10,
+ *       maxRetries: 3
+ *   ));
+ */
+final class IntegrationExecutor implements Executor
+{
+	/**
+	 * Map of integration service names to class identifiers.
+	 * Populated on first use from JVB()->connect().
+	 */
+	private array $integrationCache = [];
+
+	public function execute(Operation $operation, Progress $progress): Result
+	{
+		try {
+			[$serviceName, $action] = $this->parseOperationType($operation->type);
+
+			$integration = $this->resolveIntegration($serviceName, $operation->userId);
+
+			if (!$integration || !$integration->isSetUp()) {
+				return new Result(
+					outcome: 'failed',
+					result: ['error' => "Integration '{$serviceName}' not available or not configured"]
+				);
+			}
+
+			$data = $operation->requestData;
+
+			return match ($action) {
+				'sync_to'        => $this->processSyncTo($integration, $data, $progress),
+				'sync_from'      => $this->processSyncFrom($integration, $data, $progress),
+				'delete_from'    => $this->processDeleteFrom($integration, $data, $progress),
+				'sync_customer'  => $this->processSyncCustomer($integration, $data, $progress),
+				'import'         => $this->processImport($integration, $data, $progress),
+				default          => $this->processDynamic($integration, $action, $data, $progress),
+			};
+
+		} catch (Exception $e) {
+			JVB()->error()->log(
+				'[IntegrationExecutor]:execute',
+				$e->getMessage(),
+				[
+					'operation_id'   => $operation->id,
+					'operation_type' => $operation->type,
+					'user_id'        => $operation->userId,
+				]
+			);
+
+			return new Result(
+				outcome: 'failed',
+				result: ['error' => $e->getMessage()]
+			);
+		}
+	}
+
+	/*****************************************************************
+	 * TYPE PARSING
+	 *****************************************************************/
+
+	/**
+	 * Parse 'helcim_sync_to' → ['helcim', 'sync_to']
+	 */
+	private function parseOperationType(string $type): array
+	{
+		// Remove BASE prefix if present (e.g. 'jvb_helcim_sync_to' → 'helcim_sync_to')
+		$type = str_replace(BASE, '', $type);
+
+		$pos = strpos($type, '_');
+		if ($pos === false) {
+			throw new Exception("Invalid integration operation type: {$type}");
+		}
+
+		$serviceName = substr($type, 0, $pos);
+		$action      = substr($type, $pos + 1);
+
+		return [$serviceName, $action];
+	}
+
+	/**
+	 * Resolve integration instance, optionally for a specific user
+	 */
+	private function resolveIntegration(string $serviceName, int $userId): ?object
+	{
+		if (!isset($this->integrationCache[$serviceName])) {
+			$this->integrationCache[$serviceName] = JVB()->connect($serviceName);
+		}
+
+		$integration = $this->integrationCache[$serviceName];
+
+		// If operation has a user context, re-instantiate for that user
+		if ($integration && $userId) {
+			$class = get_class($integration);
+			return new $class($userId);
+		}
+
+		return $integration;
+	}
+
+	/*****************************************************************
+	 * OPERATION HANDLERS
+	 *****************************************************************/
+
+	/**
+	 * Sync WordPress posts → external service
+	 */
+	private function processSyncTo(object $integration, array $data, Progress $progress): Result
+	{
+		$items   = $data['items'] ?? [];
+		$success = [];
+		$errors  = [];
+
+		if (empty($items)) {
+			return new Result(outcome: 'success', result: ['synced' => [], 'message' => 'No items to sync']);
+		}
+
+		foreach ($items as $postID) {
+			try {
+				$result = $integration->syncPostToService((int)$postID);
+				if (is_wp_error($result)) {
+					$errors[$postID] = $result->get_error_message();
+					$progress->failItem($postID, $result->get_error_message());
+				} else {
+					$success[] = $postID;
+					$progress->advance();
+				}
+			} catch (Exception $e) {
+				$errors[$postID] = $e->getMessage();
+				$progress->failItem($postID, $e->getMessage());
+			}
+		}
+
+		$outcome = empty($errors) ? 'success' : (empty($success) ? 'failed' : 'partial');
+
+		return new Result(
+			outcome: $outcome,
+			result: [
+				'synced'        => $success,
+				'errors'        => $errors,
+				'synced_count'  => count($success),
+				'failed_count'  => count($errors),
+			]
+		);
+	}
+
+	/**
+	 * Sync external service → WordPress posts
+	 */
+	private function processSyncFrom(object $integration, array $data, Progress $progress): Result
+	{
+		$items   = $data['items'] ?? $data['external_ids'] ?? [];
+		$success = [];
+		$errors  = [];
+
+		foreach ($items as $externalId) {
+			try {
+				$result = $integration->syncFromService($externalId);
+				if (is_wp_error($result)) {
+					$errors[$externalId] = $result->get_error_message();
+					$progress->failItem($externalId, $result->get_error_message());
+				} else {
+					$success[] = $externalId;
+					$progress->advance();
+				}
+			} catch (Exception $e) {
+				$errors[$externalId] = $e->getMessage();
+				$progress->failItem($externalId, $e->getMessage());
+			}
+		}
+
+		$outcome = empty($errors) ? 'success' : (empty($success) ? 'failed' : 'partial');
+
+		return new Result(
+			outcome: $outcome,
+			result: ['imported' => $success, 'errors' => $errors]
+		);
+	}
+
+	/**
+	 * Delete items from external service
+	 */
+	private function processDeleteFrom(object $integration, array $data, Progress $progress): Result
+	{
+		$externalIds = $data['external_ids'] ?? [];
+		$success     = [];
+		$errors      = [];
+
+		foreach ($externalIds as $externalId) {
+			try {
+				$result = $integration->deleteFromService($externalId);
+				if (is_wp_error($result)) {
+					$errors[$externalId] = $result->get_error_message();
+				} else {
+					$success[] = $externalId;
+				}
+				$progress->advance();
+			} catch (Exception $e) {
+				$errors[$externalId] = $e->getMessage();
+				$progress->failItem($externalId, $e->getMessage());
+			}
+		}
+
+		$outcome = empty($errors) ? 'success' : (empty($success) ? 'failed' : 'partial');
+		return new Result(outcome: $outcome, result: ['deleted' => $success, 'errors' => $errors]);
+	}
+
+	/**
+	 * Sync a customer record
+	 */
+	private function processSyncCustomer(object $integration, array $data, Progress $progress): Result
+	{
+		try {
+			$result = $integration->syncCustomer($data);
+			$progress->advance();
+
+			if (is_wp_error($result)) {
+				return new Result(outcome: 'failed', result: ['error' => $result->get_error_message()]);
+			}
+			return new Result(outcome: 'success', result: $result);
+		} catch (Exception $e) {
+			return new Result(outcome: 'failed', result: ['error' => $e->getMessage()]);
+		}
+	}
+
+	/**
+	 * Bulk import from external service
+	 */
+	private function processImport(object $integration, array $data, Progress $progress): Result
+	{
+		try {
+			$result = $integration->importFromService($data);
+			$progress->advance($data['count'] ?? 1);
+
+			if (is_wp_error($result)) {
+				return new Result(outcome: 'failed', result: ['error' => $result->get_error_message()]);
+			}
+			return new Result(outcome: 'success', result: $result);
+		} catch (Exception $e) {
+			return new Result(outcome: 'failed', result: ['error' => $e->getMessage()]);
+		}
+	}
+
+	/**
+	 * Fallback: call processAction on the integration for non-standard actions
+	 */
+	private function processDynamic(object $integration, string $action, array $data, Progress $progress): Result
+	{
+		try {
+			$result = $integration->processAction($action, $data);
+			$progress->advance();
+
+			if (is_wp_error($result)) {
+				return new Result(outcome: 'failed', result: ['error' => $result->get_error_message()]);
+			}
+
+			return new Result(
+				outcome: 'success',
+				result: is_array($result) ? $result : ['result' => $result]
+			);
+		} catch (Exception $e) {
+			return new Result(outcome: 'failed', result: ['error' => $e->getMessage()]);
+		}
+	}
+}
diff --git a/inc/meta/Form.php b/inc/meta/Form.php
index 4a515bd..f6de609 100644
--- a/inc/meta/Form.php
+++ b/inc/meta/Form.php
@@ -99,7 +99,7 @@
 			'<div class="%s" data-field="%s" data-field-type="%s"%s>',
 			$classes,
 			$name,
-			$config['type'],
+			str_replace('_', '-', $config['type']),
 			$datasets
 		);
 
@@ -276,7 +276,7 @@
 
 	protected static function renderText(string $name, mixed $value, array $config): string
 	{
-		$value = ($value === '') ? ' value="'.esc_attr($value).'"' : '';
+		$value = ($value !== '') ? ' value="'.esc_attr($value).'"' : '';
 		$input = sprintf(
 			'<input type="%s"%s%s />',
 			$config['subtype']??'text',
@@ -312,7 +312,7 @@
 	{
 		$attrs = static::inputAttrs($name, $config);
 
-		$value = ($value === '') ? ' value="'.esc_attr($value).'"' : '';
+		$value = ($value !== '') ? ' value="'.esc_attr($value).'"' : '';
 		$input = sprintf(
 			'<input type="number"%s%s />',
 			$value,
@@ -331,7 +331,7 @@
 
 		$attrs = static::inputAttrs($name, $config);
 
-		$value = ($value === '') ? ' value="'.esc_attr($value).'"' : '';
+		$value = ($value !== '') ? ' value="'.esc_attr($value).'"' : '';
 		$input = sprintf(
 			'<div class="quantity">
 				<button type="button" class="decrease" title="%s" aria-label="Decrease %s">%s</button>
@@ -354,7 +354,7 @@
 	protected static function renderEmail(string $name, mixed $value, array $config): string
 	{
 		$config['validate'] = 'email';
-		$value = ($value === '') ? ' value="'.esc_attr($value).'"' : '';
+		$value = ($value !== '') ? ' value="'.esc_attr($value).'"' : '';
 		$input = sprintf(
 			'<input type="email"%s%s />',
 			$value,
@@ -367,7 +367,7 @@
 	protected static function renderUrl(string $name, mixed $value, array $config): string
 	{
 		$config['validate'] = 'url';
-		$value = ($value === '') ? ' value="'.esc_attr($value).'"' : '';
+		$value = ($value !== '') ? ' value="'.esc_attr($value).'"' : '';
 		$input = sprintf(
 			'<input type="url"%s%s />',
 			$value,
@@ -380,7 +380,7 @@
 	protected static function renderTel(string $name, mixed $value, array $config): string
 	{
 		$config['validate'] = 'phone';
-		$value = ($value === '') ? ' value="'.esc_attr($value).'"' : '';
+		$value = ($value !== '') ? ' value="'.esc_attr($value).'"' : '';
 		$input = sprintf(
 			'<input type="tel"%s%s />',
 			$value,
@@ -401,7 +401,7 @@
 			}
 		}
 
-		$value = ($value === '') ? ' value="'.esc_attr($value).'"' : '';
+		$value = ($value !== '') ? ' value="'.esc_attr($value).'"' : '';
 		$input = sprintf(
 			'<input type="date"%s%s />',
 			$value,
@@ -413,7 +413,7 @@
 
 	protected static function renderTime(string $name, mixed $value, array $config): string
 	{
-		$value = ($value === '') ? ' value="'.esc_attr($value).'"' : '';
+		$value = ($value !== '') ? ' value="'.esc_attr($value).'"' : '';
 		$input = sprintf(
 			'<input type="time"%s%s />',
 			$value,
@@ -425,7 +425,7 @@
 
 	protected static function renderDatetime(string $name, mixed $value, array $config): string
 	{
-		$value = ($value === '') ? ' value="'.esc_attr($value).'"' : '';
+		$value = ($value !== '') ? ' value="'.esc_attr($value).'"' : '';
 		$input = sprintf(
 			'<input type="datetime-local"%s%s />',
 			$value,
@@ -492,6 +492,8 @@
 		$optionsHtml = '';
 		if (empty($config['required'])) {
 			$optionsHtml .= '<option value="">— Select —</option>';
+		} else {
+			$optionsHtml .= '<option value="" disabled selected hidden>— Select —</option>';
 		}
 
 		foreach ($options as $optValue => $optLabel) {
@@ -538,7 +540,7 @@
 				$optValue,
 				esc_attr($optValue),
 				$checked,
-				$name,
+				esc_attr($name),
 				$optValue,
 				esc_html($optLabel)
 			);
@@ -564,14 +566,16 @@
 		foreach ($options as $optValue => $optLabel) {
 			$radios .= sprintf(
 				'
-                    <input type="radio" name="%s" value="%s"%s />
+                    <input type="radio" name="%s" id="%s-%s" value="%s"%s />
 				<label class="radio-option" for="%s-%s">
                     <span>%s</span>
                 </label>',
 				esc_attr($name),
+				esc_attr($name),
+				$optValue,
 				esc_attr($optValue),
 				checked($value, $optValue),
-				$name,
+				esc_attr($name),
 				$optValue,
 				esc_html($optLabel)
 			);
@@ -1443,7 +1447,7 @@
 		$input .= sprintf(
 			'<button type="button" class="button add-tag">%s<span>%s</span></button></div>',
 			jvbIcon('plus'),
-			$field['add_label']??'Add'
+			$config['add_label']??'Add'
 		);
 
 		//Tag Display
@@ -1463,7 +1467,7 @@
 	}
 		protected static function renderTagItems(array $fields, mixed $value, string $name, string $tagFormat):string
 		{
-			if ($value === '') {
+			if (!$value || $value === '') {
 				return '';
 			}
 			if (is_string($value)) {
@@ -1485,37 +1489,39 @@
 
 			$out = sprintf(
 				'<div class="tag-item"%s><span class="tag-label">%s</span>',
-				($index) ? ' data-index="'.$index.'"' : '',
+				($index !== null) ? ' data-index="'.$index.'"' : '',
 				$tagText
 			);
 
 			foreach ($fields as $fieldName => $fieldConfig) {
 				$value = $values[$fieldName]??'';
-				$fullName = (!$index) ? $fieldName : sprintf('%s:%s:%s', $name, $index, $fieldName);
+				$fullName = ($index === null) ? $fieldName : sprintf('%s:%s:%s', $name, $index, $fieldName);
 
 				$out .= sprintf(
 					'<input type="hidden"
-					name="%s"
-					value="%s"
-					data-field="%s"
-					data-field-type="%s" />',
+						name="%s"
+						value="%s"
+						data-field="%s"
+						data-field-type="%s"
+						id="%s" />',
 					esc_attr($fullName),
 					esc_attr($value),
 					esc_attr($fieldName),
-					esc_attr($fieldConfig['type'])
-				);
-
-				$out .= sprintf(
-					'<button type="button" class="remove-tag" aria-label="Remove">%s</button>',
-					jvbIcon('x')
+					esc_attr($fieldConfig['type']),
+					esc_attr($fullName)
 				);
 			}
+
+			$out .= sprintf(
+				'<button type="button" class="remove-tag" aria-label="Remove">%s</button>',
+				jvbIcon('x')
+			);
 			$out .='</div>';
 			return $out;
 		}
 			protected static function getTagDisplayText(array $fields, mixed $values, string $tagFormat):string
 			{
-				if (empty($data)) {
+				if (empty($values)) {
 					return 'New Item';
 				}
 
@@ -1524,7 +1530,7 @@
 						$firstKey = array_key_first($fields);
 						return $values[$firstKey] ?? 'New Item';
 					case 'all_fields':
-						$values = array_filter(array_values($data));
+						$values = array_filter(array_values($values));
 						return implode(', ', $values) ?: 'New Item';
 					default:
 						if (strpos($tagFormat, '{') !== false) {
diff --git a/inc/registry/FieldRegistry.php b/inc/registry/FieldRegistry.php
index 372a1f9..4de8e10 100644
--- a/inc/registry/FieldRegistry.php
+++ b/inc/registry/FieldRegistry.php
@@ -8,7 +8,7 @@
 use JVBase\registry\providers\CalendarFieldProvider;
 use JVBase\registry\providers\CommonFieldProvider;
 use JVBase\registry\providers\FieldProviderInterface;
-use JVBase\registry\providers\HelcimFieldProvider;
+use JVBase\utility\Features;
 use JVBase\registry\providers\IntegrationFieldProvider;
 
 class FieldRegistry
@@ -42,11 +42,6 @@
 		$this->addFieldProvider('common', new CommonFieldProvider());
 		$this->addFieldProvider('calendar', new CalendarFieldProvider());
 		$this->addFieldProvider('integration', new IntegrationFieldProvider());
-//		if (jvbSiteUsesHelcim()) {
-//			$this->addFieldProvider('helcim', new HelcimFieldProvider());
-//		}
-
-
 
 		// Allow extensions to add providers
 		do_action(BASE . 'register_field_providers', $this);
@@ -165,6 +160,14 @@
 			}
 		}
 
+		if (Features::hasIntegration('helcim') && jvbCheck('use_helcim', $config)) {
+			$helcim = JVB()->connect('helcim');
+			if ($helcim) {
+				$contentType = $config['integrations']['helcim']['content_type'] ?? $helcim->getDefaultContentType();
+				$fields = array_merge($fields, $helcim->getHelcimMeta($contentType));
+			}
+		}
+
 		return $fields;
 	}
 
diff --git a/inc/registry/providers/HelcimFieldProvider.php b/inc/registry/providers/HelcimFieldProvider.php
deleted file mode 100644
index dcc73af..0000000
--- a/inc/registry/providers/HelcimFieldProvider.php
+++ /dev/null
@@ -1,498 +0,0 @@
-<?php
-namespace JVBase\registry\providers;
-
-if (!defined('ABSPATH')) {
-	exit;
-}
-
-class HelcimFieldProvider implements FieldProviderInterface
-{
-	public function getFields(...$args): array
-	{
-		$config = $args[0] ?? [];
-
-		$fields = [
-			// Basic Product Fields
-			'price' => [
-				'type'        => 'number',
-				'bulkEdit'    => true,
-				'label'       => 'Price',
-				'step'        => 0.01,
-				'max'         => 99999,
-				'description' => 'Product price'
-			],
-
-			'product_type' => [
-				'type'  => 'select',
-				'label' => 'Product Type',
-				'options' => [
-					'REGULAR'       => 'Regular Product',
-					'SERVICE'       => 'Service',
-					'DIGITAL'       => 'Digital Product',
-					'FOOD_AND_BEV'  => 'Food & Beverage',
-					'EVENT'         => 'Event/Ticket',
-					'SUBSCRIPTION'  => 'Subscription',
-					'DONATION'      => 'Donation'
-				],
-				'default' => $config['integrations']['helcim']['content_type'] ?? 'REGULAR'
-			],
-
-			'cart_quantity' => [
-				'type'   => 'number',
-				'label'  => 'Quantity',
-				'hidden' => true,
-			],
-
-			// Tax & Shipping
-			'tax_exempt' => [
-				'type'    => 'true_false',
-				'label'   => 'Tax Exempt',
-				'section' => 'helcim-tax'
-			],
-
-			'shipping_required' => [
-				'type'    => 'true_false',
-				'label'   => 'Shipping Required',
-				'section' => 'helcim-shipping'
-			],
-
-			'shipping_weight' => [
-				'type'        => 'number',
-				'label'       => 'Shipping Weight (kg)',
-				'step'        => 0.01,
-				'section'     => 'helcim-shipping',
-				'condition'   => [
-					'field'    => 'shipping_required',
-					'operator' => '==',
-					'value'    => true
-				]
-			],
-
-			// Availability
-			'available_online' => [
-				'type'    => 'true_false',
-				'label'   => 'Available Online',
-				'section' => 'helcim-availability',
-				'default' => true
-			],
-
-			'available_for_pickup' => [
-				'type'    => 'true_false',
-				'label'   => 'Available for Pickup',
-				'section' => 'helcim-availability',
-				'default' => true
-			],
-
-			'available_for_delivery' => [
-				'type'    => 'true_false',
-				'label'   => 'Available for Delivery',
-				'section' => 'helcim-availability',
-				'default' => false
-			],
-
-			'_helcim_sku' => [
-				'type'        => 'text',
-				'label'       => 'SKU',
-				'description' => 'Stock keeping unit',
-				'section'     => 'helcim-config'
-			],
-
-			// Product Variations
-			'product_variations' => [
-				'type'        => 'repeater',
-				'label'       => 'Product Variations',
-				'description' => 'Different versions of this product',
-				'add_label'   => 'Add Variation',
-				'section'     => 'variations',
-				'fields'      => $this->getVariationFields($config)
-			],
-
-			// Product Options
-			'options' => [
-				'type'   => 'group',
-				'label'  => 'Product Options',
-				'section'=> 'helcim-options',
-				'fields' => [
-					'max_order' => [
-						'type'    => 'number',
-						'label'   => 'Maximum per order',
-						'default' => 50
-					],
-					'min_order' => [
-						'type'    => 'number',
-						'label'   => 'Minimum per order',
-						'default' => 0,
-					],
-					'step' => [
-						'type'    => 'number',
-						'label'   => 'Order increment',
-						'default' => 1,
-					],
-					'preparation_time' => [
-						'type'        => 'number',
-						'label'       => 'Preparation time (minutes)',
-						'description' => 'Time needed to prepare this item',
-						'condition'   => [
-							'field'    => 'product_type',
-							'operator' => 'in',
-							'value'    => ['FOOD_AND_BEV', 'SERVICE']
-						]
-					]
-				]
-			],
-
-			// Subscription Fields
-			'subscription_settings' => [
-				'type'      => 'group',
-				'label'     => 'Subscription Settings',
-				'section'   => 'helcim-subscription',
-				'condition' => [
-					'field'    => 'product_type',
-					'operator' => '==',
-					'value'    => 'SUBSCRIPTION'
-				],
-				'fields' => [
-					'billing_cycle' => [
-						'type'    => 'select',
-						'label'   => 'Billing Cycle',
-						'options' => [
-							'daily'     => 'Daily',
-							'weekly'    => 'Weekly',
-							'monthly'   => 'Monthly',
-							'quarterly' => 'Quarterly',
-							'yearly'    => 'Yearly'
-						],
-						'default' => 'monthly'
-					],
-					'trial_period' => [
-						'type'        => 'number',
-						'label'       => 'Trial Period (days)',
-						'description' => 'Free trial period before billing starts',
-						'default'     => 0
-					],
-					'setup_fee' => [
-						'type'        => 'number',
-						'label'       => 'Setup Fee',
-						'step'        => 0.01,
-						'description' => 'One-time setup fee'
-					]
-				]
-			],
-
-			// Food & Beverage Specific
-			'food_settings' => [
-				'type'      => 'group',
-				'label'     => 'Food & Beverage Settings',
-				'section'   => 'helcim-food',
-				'condition' => [
-					'field'    => 'product_type',
-					'operator' => '==',
-					'value'    => 'FOOD_AND_BEV'
-				],
-				'fields' => [
-					'ingredients' => [
-						'type'        => 'textarea',
-						'label'       => 'Ingredients',
-						'description' => 'List ingredients (comma separated)'
-					],
-					'allergens' => [
-						'type'        => 'checkbox_list',
-						'label'       => 'Allergens',
-						'options'     => [
-							'gluten'   => 'Contains Gluten',
-							'dairy'    => 'Contains Dairy',
-							'nuts'     => 'Contains Nuts',
-							'soy'      => 'Contains Soy',
-							'eggs'     => 'Contains Eggs',
-							'seafood'  => 'Contains Seafood'
-						]
-					],
-					'dietary_options' => [
-						'type'    => 'checkbox_list',
-						'label'   => 'Dietary Options',
-						'options' => [
-							'vegetarian'   => 'Vegetarian',
-							'vegan'        => 'Vegan',
-							'gluten_free'  => 'Gluten Free',
-							'dairy_free'   => 'Dairy Free',
-							'keto'         => 'Keto Friendly',
-							'halal'        => 'Halal',
-							'kosher'       => 'Kosher'
-						]
-					],
-					'spice_level' => [
-						'type'    => 'range',
-						'label'   => 'Spice Level',
-						'min'     => 0,
-						'max'     => 5,
-						'default' => 0
-					],
-					'serving_size' => [
-						'type'        => 'text',
-						'label'       => 'Serving Size',
-						'description' => 'e.g., "Serves 2-3"'
-					]
-				]
-			],
-
-			// Service Specific
-			'service_settings' => [
-				'type'      => 'group',
-				'label'     => 'Service Settings',
-				'section'   => 'helcim-service',
-				'condition' => [
-					'field'    => 'product_type',
-					'operator' => '==',
-					'value'    => 'SERVICE'
-				],
-				'fields' => [
-					'service_duration' => [
-						'type'        => 'number',
-						'label'       => 'Duration (minutes)',
-						'description' => 'Service duration in minutes'
-					],
-					'booking_required' => [
-						'type'  => 'true_false',
-						'label' => 'Booking Required'
-					],
-					'capacity' => [
-						'type'        => 'number',
-						'label'       => 'Service Capacity',
-						'description' => 'Maximum number of customers per service'
-					],
-					'staff_required' => [
-						'type'        => 'number',
-						'label'       => 'Staff Required',
-						'description' => 'Number of staff needed',
-						'default'     => 1
-					]
-				]
-			],
-
-			// Event Specific
-			'event_settings' => [
-				'type'      => 'group',
-				'label'     => 'Event Settings',
-				'section'   => 'helcim-event',
-				'condition' => [
-					'field'    => 'product_type',
-					'operator' => '==',
-					'value'    => 'EVENT'
-				],
-				'fields' => [
-					'event_date' => [
-						'type'  => 'datetime',
-						'label' => 'Event Date & Time'
-					],
-					'event_location' => [
-						'type'  => 'text',
-						'label' => 'Event Location'
-					],
-					'max_attendees' => [
-						'type'  => 'number',
-						'label' => 'Maximum Attendees'
-					],
-					'early_bird_price' => [
-						'type'        => 'number',
-						'label'       => 'Early Bird Price',
-						'step'        => 0.01,
-						'description' => 'Discounted price for early registrations'
-					],
-					'early_bird_deadline' => [
-						'type'  => 'date',
-						'label' => 'Early Bird Deadline'
-					]
-				]
-			]
-		];
-
-		// Add inventory fields if configured
-		if ($config['hasInventory'] ?? false) {
-			$fields['_helcim_inventory'] = [
-				'type'     => 'number',
-				'label'    => 'Inventory',
-				'bulkEdit' => true,
-				'section'  => 'inventory'
-			];
-
-			$fields['track_inventory'] = [
-				'type'    => 'true_false',
-				'label'   => 'Track Inventory',
-				'section' => 'inventory',
-				'default' => true
-			];
-
-			$fields['low_stock_threshold'] = [
-				'type'        => 'number',
-				'label'       => 'Low Stock Alert',
-				'description' => 'Alert when stock falls below this level',
-				'section'     => 'inventory',
-				'default'     => 5
-			];
-
-			$fields['product_variations']['fields']['inventory'] = [
-				'type'        => 'number',
-				'label'       => 'Stock Quantity',
-				'description' => 'Current stock for this variation'
-			];
-		}
-
-		return $fields;
-	}
-
-	/**
-	 * Get variation fields
-	 */
-	private function getVariationFields(array $config): array
-	{
-		$fields = [
-			'name' => [
-				'type'        => 'text',
-				'label'       => 'Variation Name',
-				'description' => 'e.g., "Small", "Large", "Red"'
-			],
-			'price' => [
-				'type'        => 'number',
-				'label'       => 'Price',
-				'step'        => 0.01,
-				'max'         => 99999,
-				'description' => 'Price for this variation'
-			],
-			'sku' => [
-				'type'        => 'text',
-				'label'       => 'SKU',
-				'description' => 'Stock keeping unit for this variation'
-			],
-			'track_inventory' => [
-				'type'  => 'true_false',
-				'label' => 'Track Inventory',
-			],
-			'_helcim_variation_id' => [
-				'type'        => 'text',
-				'label'       => 'Helcim Variation ID',
-				'description' => 'Helcim ID for this variation',
-				'hidden'      => true
-			],
-			'_helcim_last_sync' => [
-				'type'   => 'datetime',
-				'label'  => 'Last Sync',
-				'hidden' => true
-			],
-			'options' => [
-				'type'        => 'group',
-				'label'       => 'Variation Options',
-				'collapsible' => true,
-				'fields'      => [
-					'color' => [
-						'type'        => 'color',
-						'label'       => 'Color',
-						'description' => 'Visual color for this variation'
-					],
-					'size' => [
-						'type'    => 'select',
-						'label'   => 'Size',
-						'options' => [
-							''      => 'N/A',
-							'xs'    => 'Extra Small',
-							's'     => 'Small',
-							'm'     => 'Medium',
-							'l'     => 'Large',
-							'xl'    => 'Extra Large',
-							'xxl'   => '2X Large',
-							'custom'=> 'Custom'
-						]
-					],
-					'custom_size' => [
-						'type'        => 'text',
-						'label'       => 'Custom Size',
-						'condition'   => [
-							'field'    => 'size',
-							'operator' => '==',
-							'value'    => 'custom'
-						]
-					],
-					'weight' => [
-						'type'        => 'number',
-						'label'       => 'Weight (kg)',
-						'step'        => 0.01,
-						'description' => 'Weight of this variation'
-					],
-					'dimensions' => [
-						'type'   => 'group',
-						'label'  => 'Dimensions',
-						'fields' => [
-							'length' => [
-								'type'  => 'number',
-								'label' => 'Length (cm)',
-								'step'  => 0.1
-							],
-							'width' => [
-								'type'  => 'number',
-								'label' => 'Width (cm)',
-								'step'  => 0.1
-							],
-							'height' => [
-								'type'  => 'number',
-								'label' => 'Height (cm)',
-								'step'  => 0.1
-							]
-						]
-					]
-				]
-			]
-		];
-
-		// Add service-specific variation fields
-		$product_type = $_POST['product_type'] ?? $config['integrations']['helcim']['content_type'] ?? 'REGULAR';
-
-		if ($product_type === 'SERVICE') {
-			$fields['service_duration'] = [
-				'type'        => 'number',
-				'label'       => 'Duration (minutes)',
-				'description' => 'Duration for this service variation'
-			];
-
-			$fields['available_for_booking'] = [
-				'type'  => 'true_false',
-				'label' => 'Available for Booking'
-			];
-		}
-
-		if ($product_type === 'FOOD_AND_BEV') {
-			$fields['portion_size'] = [
-				'type'    => 'select',
-				'label'   => 'Portion Size',
-				'options' => [
-					'small'    => 'Small',
-					'regular'  => 'Regular',
-					'large'    => 'Large',
-					'family'   => 'Family Size'
-				]
-			];
-
-			$fields['calories'] = [
-				'type'        => 'number',
-				'label'       => 'Calories',
-				'description' => 'Calorie count for this variation'
-			];
-		}
-
-		if ($product_type === 'DIGITAL') {
-			$fields['download_limit'] = [
-				'type'        => 'number',
-				'label'       => 'Download Limit',
-				'description' => 'Maximum number of downloads',
-				'default'     => -1
-			];
-
-			$fields['expiry_days'] = [
-				'type'        => 'number',
-				'label'       => 'Access Duration (days)',
-				'description' => 'Days until download expires',
-				'default'     => 0
-			];
-		}
-
-		return $fields;
-	}
-}
diff --git a/inc/rest/Route.php b/inc/rest/Route.php
index 4deb44a..dd356a9 100644
--- a/inc/rest/Route.php
+++ b/inc/rest/Route.php
@@ -334,9 +334,33 @@
 			'email' => 'sanitize_email',
 			'url' => 'esc_url_raw',
 			'boolean', 'bool' => 'rest_sanitize_boolean',
+			'array' => function($value) {
+				if (is_array($value)) {
+					return $value;
+				}
+				return [];
+			},
+			'object' => function($value) {
+				if (is_array($value) || is_object($value)) {
+					return (array) $value;
+				}
+				return [];
+			},
 			default => null,
 		};
 
+		// Add validate callback for array/object types
+		if (in_array($type, ['array', 'object'])) {
+			$arg['validate_callback'] = function($value, $request, $param) {
+				// Allow empty arrays/objects
+				if (empty($value)) {
+					return true;
+				}
+				// Ensure it's an array or object
+				return is_array($value) || is_object($value);
+			};
+		}
+
 		// Parse modifiers
 		foreach (array_slice($parts, 1) as $part) {
 			$part = trim($part);
diff --git a/inc/rest/_setup.php b/inc/rest/_setup.php
index 26cbb13..7ba414a 100644
--- a/inc/rest/_setup.php
+++ b/inc/rest/_setup.php
@@ -37,7 +37,7 @@
 require(JVB_DIR . '/inc/rest/routes/UploadRoutes.php');
 require(JVB_DIR . '/inc/rest/routes/SettingsRoutes.php');
 if (Features::forSite()->has('dashboard')) {
-//	require(JVB_DIR . '/inc/rest/routes/AdminRoutes.php');
+	require(JVB_DIR . '/inc/rest/routes/AdminRoutes.php');
 	require(JVB_DIR . '/inc/rest/routes/ContentRoutes.php');
 	require(JVB_DIR . '/inc/rest/routes/ContentTermsRoutes.php');
 //	require(JVB_DIR . '/inc/rest/routes/BioRoutes.php');
@@ -68,6 +68,10 @@
 	require(JVB_DIR . '/inc/rest/routes/IntegrationsSquareRoutes.php');
 }
 
+if (Features::hasIntegration('helcim')) {
+	require(JVB_DIR . '/inc/rest/routes/IntegrationsHelcimRoutes.php');
+}
+
 require(JVB_DIR .'/inc/rest/routes/OptionsRoutes.php');
 require(JVB_DIR .'/inc/rest/routes/FormRoutes.php');
 require(JVB_DIR .'/inc/rest/routes/IntegrationsRoutes.php');
diff --git a/inc/rest/routes/AdminRoutes.php b/inc/rest/routes/AdminRoutes.php
new file mode 100644
index 0000000..4f89e76
--- /dev/null
+++ b/inc/rest/routes/AdminRoutes.php
@@ -0,0 +1,225 @@
+<?php
+namespace JVBase\rest\routes;
+
+use JVBase\JVB;
+use JVBase\managers\Cache;
+use JVBase\managers\IconsManager;
+use JVBase\rest\Rest;
+use JVBase\rest\Route;
+use JVBase\rest\PermissionHandler;
+use WP_Query;
+use WP_Error;
+use WP_REST_Request;
+use WP_REST_Response;
+
+if (!defined('ABSPATH')) {
+    exit; // Exit if accessed directly
+}
+
+class AdminRoutes extends Rest
+{
+    protected array $fields;
+    protected $meta;
+    protected string $metaType;
+    protected string $content;
+
+    public function __construct()
+    {
+        $this->cacheName = 'itsme';
+        parent::__construct();
+    }
+    public function registerRoutes():void
+    {
+		Route::for('admin-cache')
+			->post([$this, 'handleCacheAction'])
+			->auth('admin')
+			->rateLimit(30)
+			->register();
+
+		Route::for('admin-icons')
+			->post([$this, 'handleIconAction'])
+			->auth('admin')
+			->rateLimit(30)
+			->register();
+
+		Route::for('admin-action')
+			->post([$this, 'adminAction'])
+			->auth('admin')
+			->rateLimit()
+			->register();
+    }
+
+    /**
+     * Handles admin actions from the custom WP Admin pages.
+     * Extended by other managers that register admin subpages
+     * @param WP_REST_Request $request
+     *
+     * @return WP_REST_Response
+     */
+    public function adminAction(WP_REST_Request $request):WP_REST_Response
+    {
+        error_log('Request Params: '.print_r($request->get_param('action'), true));
+        return apply_filters(
+            BASE.'admin_action_filter',
+            new WP_REST_Response([
+                'success'   => false,
+                'message'   => 'No filters found'
+            ]),
+            $request,
+            sanitize_text_field($request->get_param('action'))
+        );
+    }
+
+
+    /**
+     * @param array $filters
+     *
+     * @return array
+     */
+    protected function checkFilters(array $filters)
+    {
+        global $karma;
+        $out = [];
+        foreach ($filters as $type => $value) {
+            if (!array_key_exists($type, $karma)) {
+                continue;
+            }
+            $out[$type]  = jvbSanitizeIDList($value);
+        }
+        return $out;
+    }
+
+	/**
+	 * Handle cache-related actions
+	 */
+	public function handleCacheAction(\WP_REST_Request $request): \WP_REST_Response
+	{
+		$action = sanitize_text_field($request->get_param('action'));
+
+		switch ($action) {
+			case 'flush-all':
+				$total = Cache::flushAll();
+				return new \WP_REST_Response([
+					'success' => true,
+					'message' => $total.' caches flushed successfully'
+				]);
+
+			case 'flush-cache':
+				$group = sanitize_text_field($request->get_param('group'));
+				if (empty($group)) {
+					return new \WP_REST_Response([
+						'success' => false,
+						'message' => 'No cache group specified'
+					], 400);
+				}
+
+				Cache::for($group)?->flush();
+
+				return new \WP_REST_Response([
+					'success' => true,
+					'message' => "Cache group '{$group}' flushed successfully"
+				]);
+
+			default:
+				return new \WP_REST_Response([
+					'success' => false,
+					'message' => 'Invalid action'
+				], 400);
+		}
+	}
+
+	/**
+	 * Handle icon-related actions
+	 */
+	public function handleIconAction(\WP_REST_Request $request): \WP_REST_Response
+	{
+		$action = sanitize_text_field($request->get_param('action'));
+		$source = sanitize_text_field($request->get_param('source') ?? 'icons');
+		$icons = IconsManager::for($source);
+
+		switch ($action) {
+			case 'refresh-icons':
+				// Force regenerate CSS immediately
+				$icons->forceRefresh();
+				IconsManager::regenerateAllCSS([$source => true]);
+
+				return new \WP_REST_Response([
+					'success' => true,
+					'message' => "Icon CSS regenerated successfully for '{$source}'"
+				]);
+
+			case 'refresh-all-icons':
+				// Regenerate all icon sources
+				foreach (['icons', 'forms', 'dash'] as $src) {
+					IconsManager::for($src)->forceRefresh();
+				}
+				IconsManager::regenerateAllCSS();
+
+				return new \WP_REST_Response([
+					'success' => true,
+					'message' => 'All icon CSS files regenerated successfully'
+				]);
+
+			case 'restore-icon-version':
+				$timestamp = (int)$request->get_param('timestamp');
+				if (empty($timestamp)) {
+					return new \WP_REST_Response([
+						'success' => false,
+						'message' => 'No timestamp provided'
+					], 400);
+				}
+
+				if ($icons->restoreVersion($timestamp)) {
+					return new \WP_REST_Response([
+						'success' => true,
+						'message' => 'Icon version restored successfully'
+					]);
+				}
+
+				return new \WP_REST_Response([
+					'success' => false,
+					'message' => 'Failed to restore icon version'
+				], 500);
+
+			case 'merge-icon-versions':
+				$timestamps = $request->get_param('timestamps');
+
+				if (empty($timestamps) || !is_array($timestamps)) {
+					return new \WP_REST_Response([
+						'success' => false,
+						'message' => 'No versions selected for merging'
+					], 400);
+				}
+
+				$timestamps = array_map('intval', $timestamps);
+
+				if (count($timestamps) < 2) {
+					return new \WP_REST_Response([
+						'success' => false,
+						'message' => 'Please select at least 2 versions to merge'
+					], 400);
+				}
+
+				if ($icons->mergeVersions($timestamps)) {
+					// Regenerate CSS after merge
+					IconsManager::regenerateAllCSS([$source => true]);
+
+					return new \WP_REST_Response([
+						'success' => true,
+						'message' => 'Icon versions merged successfully'
+					]);
+				}
+
+				return new \WP_REST_Response([
+					'success' => false,
+					'message' => 'Failed to merge icon versions'
+				], 500);
+
+			default:
+				return new \WP_REST_Response([
+					'success' => false,
+					'message' => 'Invalid action'
+				], 400);
+		}
+	}
+}
diff --git a/inc/rest/routes/ApprovalRoutes.php b/inc/rest/routes/ApprovalRoutes.php
index e01ffd5..6f9d590 100644
--- a/inc/rest/routes/ApprovalRoutes.php
+++ b/inc/rest/routes/ApprovalRoutes.php
@@ -80,7 +80,8 @@
 				'notes' => 'string',
 			])
 			->auth(PermissionHandler::combine(['user', 'verified']))
-			->rateLimit(3);
+			->rateLimit(3)
+			->register();
     }
 
     /**
diff --git a/inc/rest/routes/ContentRoutes.php b/inc/rest/routes/ContentRoutes.php
index bbeefce..1c90f30 100644
--- a/inc/rest/routes/ContentRoutes.php
+++ b/inc/rest/routes/ContentRoutes.php
@@ -96,7 +96,8 @@
 				'user'	=> 'int|required',
 				'posts' => 'required',
 				'content' => 'string',
-			]);
+			])
+			->register();
 	}
 
 	protected function initTimelineFields(string $content): void
diff --git a/inc/rest/routes/ContentTermsRoutes.php b/inc/rest/routes/ContentTermsRoutes.php
index aa863e0..eee6b6b 100644
--- a/inc/rest/routes/ContentTermsRoutes.php
+++ b/inc/rest/routes/ContentTermsRoutes.php
@@ -117,8 +117,9 @@
 				'user' => 'int|required',
 				'term_id' => 'int|required'
 			])
-			->auth(PermissionHandler::custom([$this, 'checkTermPermission']))
-			->rateLimit(10);
+			->auth(PermissionHandler::combine([[$this, 'checkTermPermission']]))
+			->rateLimit(10)
+			->register();
 
 		// Member management (if track_changes enabled)
 		if (Features::forTaxonomy($this->taxonomy)->has('track_changes')) {
@@ -139,8 +140,9 @@
 					'target_user' => 'int|required',
 					'action' => 'string|enum:add,remove|required'
 				])
-				->auth(PermissionHandler::custom([$this, 'checkTermPermission']))
-				->rateLimit(5);
+				->auth(PermissionHandler::combine([[$this, 'checkTermPermission']]))
+				->rateLimit(5)
+				->register();
 		}
 
 		// Membership requests (if verify_entry enabled)
@@ -153,8 +155,9 @@
 					'status' => 'string|enum:requested,accepted,rejected,all|default:requested',
 					'page' => 'int|default:1|min:1'
 				])
-				->auth(PermissionHandler::custom([$this, 'checkTermPermission']))
-				->rateLimit(20);
+				->auth(PermissionHandler::combine([[$this, 'checkTermPermission']]))
+				->rateLimit(20)
+				->register();
 
 			Route::for("{$base}/request")
 				->post([$this, 'handleRequest'])
@@ -166,7 +169,8 @@
 					'notes' => 'string'
 				])
 				->auth('verified')
-				->rateLimit(5);
+				->rateLimit(5)
+				->register();
 		}
 
 		// Ownership/management (if is_ownable enabled)
@@ -180,8 +184,9 @@
 					'role' => 'string|enum:owner,manager|required',
 					'grant' => 'bool|required'
 				])
-				->auth(PermissionHandler::custom([$this, 'checkOwnerPermission']))
-				->rateLimit(5);
+				->auth(PermissionHandler::combine([[$this, 'checkTermPermission']]))
+				->rateLimit(5)
+				->register();
 		}
 	}
 
diff --git a/inc/rest/routes/ErrorRoutes.php b/inc/rest/routes/ErrorRoutes.php
index 0a4737e..aab3658 100644
--- a/inc/rest/routes/ErrorRoutes.php
+++ b/inc/rest/routes/ErrorRoutes.php
@@ -26,7 +26,8 @@
 				'context' => 'string',
 			])
 			->auth('public')
-			->rateLimit(10);
+			->rateLimit(10)
+			->register();
     }
 
     /**
diff --git a/inc/rest/routes/FavouritesRoutes.php b/inc/rest/routes/FavouritesRoutes.php
index f127aae..9690a53 100644
--- a/inc/rest/routes/FavouritesRoutes.php
+++ b/inc/rest/routes/FavouritesRoutes.php
@@ -80,7 +80,8 @@
 				'notes' => 'string',
 			])
 			->auth(PermissionHandler::combine(['user', ['actionNonce' => 'favourites-']]))
-			->rateLimit(30);
+			->rateLimit(30)
+			->register();
 
 		// Lists endpoints
 		Route::for('favourites/lists')
@@ -98,13 +99,15 @@
 				'items' => 'array',
 			])
 			->auth(PermissionHandler::combine(['user', ['actionNonce' => 'favourites-']]))
-			->rateLimit(20);
+			->rateLimit(20)
+			->register();
 
 		// Favourite counts
 		Route::for('favourites/counts')
 			->get([$this, 'getFavouriteCounts'])
 			->args(['user' => 'integer|required'])
-			->auth(PermissionHandler::combine(['user', ['actionNonce' => 'favourites-']]));
+			->auth(PermissionHandler::combine(['user', ['actionNonce' => 'favourites-']]))
+			->register();
 	}
 
 	/**
diff --git a/inc/rest/routes/FeedRoutes.php b/inc/rest/routes/FeedRoutes.php
index af6c2c1..0368b21 100644
--- a/inc/rest/routes/FeedRoutes.php
+++ b/inc/rest/routes/FeedRoutes.php
@@ -95,13 +95,15 @@
 				'highlight' => 'string',
 			])
 			->auth('public')
-			->rateLimit(30, 60);
+			->rateLimit(30)
+			->register();
 
 		// Feed types endpoint
 		Route::for('feed/types')
 			->get([$this, 'getFeedTypes'])
 			->auth('public')
-			->rateLimit(60, 60);
+			->rateLimit()
+			->register();
 	}
 
 	/**
diff --git a/inc/rest/routes/FormRoutes.php b/inc/rest/routes/FormRoutes.php
index 9b136e2..e6be516 100644
--- a/inc/rest/routes/FormRoutes.php
+++ b/inc/rest/routes/FormRoutes.php
@@ -57,14 +57,16 @@
 			->rateLimit(5) // 5 submissions per minute
 			->get([$this, 'getForms'])
 			->auth(PermissionHandler::combine(['logged_in', ['actionNonce'=>'dash-']]))
-			->rateLimit(30);
+			->rateLimit(30)
+			->register();
 
 		// Get specific form configuration
 		Route::for(Route::pattern('forms/{form_type}'))
 			->get([$this, 'getForm'])
 			->arg('form_type', 'string|required')
 			->auth('logged_in')
-			->rateLimit(30);
+			->rateLimit(30)
+			->register();
 	}
 
 	/**
diff --git a/inc/rest/routes/ImporterRoutes.php b/inc/rest/routes/ImporterRoutes.php
index 2e4bdaf..b5debfa 100644
--- a/inc/rest/routes/ImporterRoutes.php
+++ b/inc/rest/routes/ImporterRoutes.php
@@ -33,7 +33,8 @@
 				'options' => 'string', // JSON string of options
 			])
 			->auth('admin')
-			->rateLimit(3, 300); // 3 imports per 5 minutes
+			->rateLimit(3, 300)
+			->register(); // 3 imports per 5 minutes
 
 		// Sales import endpoint
 		Route::for('jane/import-sales')
@@ -42,14 +43,16 @@
 				'options' => 'string', // JSON string of options
 			])
 			->auth('admin')
-			->rateLimit(3, 300); // 3 imports per 5 minutes
+			->rateLimit(3, 300)
+			->register(); // 3 imports per 5 minutes
 
 		// Get import status
 		Route::for(Route::pattern('jane/import-status/{id}'))
 			->get([$this, 'getImportStatus'])
 			->arg('id', 'string|required')
 			->auth('admin')
-			->rateLimit(30, 60);
+			->rateLimit(30)
+			->register();
 	}
 
 	/**
diff --git a/inc/rest/routes/IntegrationsHelcimRoutes.php b/inc/rest/routes/IntegrationsHelcimRoutes.php
new file mode 100644
index 0000000..6476e16
--- /dev/null
+++ b/inc/rest/routes/IntegrationsHelcimRoutes.php
@@ -0,0 +1,187 @@
+<?php
+namespace JVBase\rest\routes;
+
+use JVBase\rest\Rest;
+use JVBase\rest\Route;
+use WP_REST_Request;
+use WP_REST_Response;
+use Exception;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+class IntegrationsHelcimRoutes extends Rest
+{
+	public function registerRoutes(): void
+	{
+		Route::for('helcim/initialize-checkout')
+			->post([$this, 'handleInitializeCheckout'])
+			->auth('user')
+			->rateLimit(5)
+			->register();
+
+		Route::for('helcim/invoices')
+			->get([$this, 'getInvoices'])
+			->auth('user')
+			->rateLimit(10)
+			->register();
+
+		Route::for(Route::pattern('helcim/invoices/{invoice_id}'))
+			->get([$this, 'getInvoice'])
+			->auth('user')
+			->rateLimit(10)
+			->register();
+
+		Route::for('helcim/saved-cards')
+			->get([$this, 'getSavedCards'])
+			->auth('user')
+			->rateLimit(5)
+			->register();
+
+		Route::for('helcim/validate-transaction')
+			->post([$this, 'validateTransaction'])
+			->auth('user')
+			->rateLimit(10)
+			->register();
+	}
+
+	/**
+	 * Initialize a HelcimPay.js checkout session.
+	 *
+	 * Returns checkoutToken for the frontend to call
+	 * appendHelcimPayIframe(checkoutToken).
+	 */
+	public function handleInitializeCheckout(WP_REST_Request $request): WP_REST_Response
+	{
+		$data    = $request->get_json_params();
+		$user_id = absint($data['user'] ?? get_current_user_id());
+
+		if (empty($data['amount'])) {
+			return $this->validationError(['message' => 'Amount is required']);
+		}
+
+		try {
+			$helcim = JVB()->connect('helcim');
+
+			// Auto-resolve customer ID from logged-in user
+			if (empty($data['customerId']) && $user_id) {
+				$data['customerId'] = $helcim->resolveCustomerId($user_id);
+			}
+
+			$result = $helcim->initializeCheckout($data);
+
+			if (!$result['success']) {
+				return $this->error($result['message'] ?? 'Checkout initialization failed');
+			}
+
+			return $this->success($result);
+
+		} catch (Exception $e) {
+			$this->logError('Helcim checkout init failed', ['error' => $e->getMessage()]);
+			return $this->error($e->getMessage());
+		}
+	}
+
+	/**
+	 * Get invoices for the current user.
+	 */
+	public function getInvoices(WP_REST_Request $request): WP_REST_Response
+	{
+		$user_id = absint($request->get_param('user') ?? get_current_user_id());
+
+		if (!$user_id) {
+			return $this->validationError(['message' => 'Not logged in']);
+		}
+
+		try {
+			$helcim    = JVB()->connect('helcim');
+			$user      = get_userdata($user_id);
+			$result    = $helcim->handleGetInvoices([
+				'email' => $user->user_email,
+			]);
+
+			return $this->success($result);
+
+		} catch (Exception $e) {
+			return $this->error($e->getMessage());
+		}
+	}
+
+	/**
+	 * Get a single invoice by Helcim invoice ID.
+	 */
+	public function getInvoice(WP_REST_Request $request): WP_REST_Response
+	{
+		$invoiceId = $request->get_param('invoice_id');
+
+		if (!$invoiceId) {
+			return $this->validationError(['message' => 'Invoice ID required']);
+		}
+
+		try {
+			$helcim = JVB()->connect('helcim');
+			$result = $helcim->handleGetInvoice(['invoiceId' => $invoiceId]);
+
+			return $this->success($result);
+
+		} catch (Exception $e) {
+			return $this->error($e->getMessage());
+		}
+	}
+
+	/**
+	 * Get saved cards for the current user.
+	 */
+	public function getSavedCards(WP_REST_Request $request): WP_REST_Response
+	{
+		$user_id = absint($request->get_param('user') ?? get_current_user_id());
+
+		if (!$user_id) {
+			return $this->validationError(['message' => 'Not logged in']);
+		}
+
+		try {
+			$helcim = JVB()->connect('helcim');
+			$result = $helcim->handleGetCustomerCards([
+				'email' => get_userdata($user_id)->user_email,
+			]);
+
+			return $this->success($result);
+
+		} catch (Exception $e) {
+			return $this->error($e->getMessage());
+		}
+	}
+
+	/**
+	 * Validate a HelcimPay.js transaction server-side.
+	 *
+	 * Called after the frontend receives a SUCCESS message event.
+	 * Verifies the transaction hash using the secretToken stored
+	 * in the user's session/transient.
+	 */
+	public function validateTransaction(WP_REST_Request $request): WP_REST_Response
+	{
+		$data = $request->get_json_params();
+
+		if (empty($data['secretToken']) || empty($data['transactionData'])) {
+			return $this->validationError(['message' => 'Missing secretToken or transactionData']);
+		}
+
+		try {
+			$helcim = JVB()->connect('helcim');
+			$valid  = $helcim->validateTransaction(
+				$data['secretToken'],
+				$data['transactionData']
+			);
+
+			return $this->success([
+				'valid' => $valid,
+			]);
+
+		} catch (Exception $e) {
+			return $this->error($e->getMessage());
+		}
+	}
+}
diff --git a/inc/rest/routes/IntegrationsRoutes.php b/inc/rest/routes/IntegrationsRoutes.php
index 73112de..ce909bf 100644
--- a/inc/rest/routes/IntegrationsRoutes.php
+++ b/inc/rest/routes/IntegrationsRoutes.php
@@ -29,7 +29,8 @@
 				'data'		=> 'array'
 			])
 			->auth('user')
-			->rateLimit(20);
+			->rateLimit(20)
+			->register();
 		Route::for('oath/connect')
 			->post([$this, 'initiateOAuth'])
 			->auth('user')
@@ -38,7 +39,8 @@
 				'service'	=> 'string|required',
 				'user_id'	=> 'int',
 				'return_url'=> 'url'
-			]);
+			])
+			->register();
 	}
 
 	/**
diff --git a/inc/rest/routes/IntegrationsSquareRoutes.php b/inc/rest/routes/IntegrationsSquareRoutes.php
index 596be1e..4734a65 100644
--- a/inc/rest/routes/IntegrationsSquareRoutes.php
+++ b/inc/rest/routes/IntegrationsSquareRoutes.php
@@ -20,22 +20,26 @@
 		Route::for('square/process-payment')
 			->post([$this, 'handlePaymentProcessing'])
 			->auth('public')
-			->rateLimit(2);
+			->rateLimit(2)
+			->register();
 
 		Route::for('square/saved-cards')
 			->post([$this, 'getSavedCards'])
 			->auth('user')
-			->rateLimit(5);
+			->rateLimit(5)
+			->register();
 
 		Route::for('square/order-history')
 			->get([$this, 'getOrderHistory'])
 			->auth('user')
-			->rateLimit(5);
+			->rateLimit(5)
+			->register();
 
 		Route::for(Route::pattern('square/order-status/{order_id}'))
 			->get([$this, 'getOrderStatus'])
 			->auth('public')
-			->rateLimit(20);
+			->rateLimit(20)
+			->register();
 	}
 
 	//TODO: Are we processing this through our server at all? Or is it in the javascript going straight to square?
diff --git a/inc/rest/routes/Invitations.php b/inc/rest/routes/Invitations.php
index 7cccb3e..1839f44 100644
--- a/inc/rest/routes/Invitations.php
+++ b/inc/rest/routes/Invitations.php
@@ -60,7 +60,8 @@
 				'invitation_id' => 'int'
 			])
 			->auth('verified')
-			->rateLimit(10, 300);
+			->rateLimit(10, 300)
+			->register();
 	}
 
 	/**
diff --git a/inc/rest/routes/LoginRoutes.php b/inc/rest/routes/LoginRoutes.php
index 2898b23..dc8d528 100644
--- a/inc/rest/routes/LoginRoutes.php
+++ b/inc/rest/routes/LoginRoutes.php
@@ -39,7 +39,8 @@
 		Route::for('auth/status')
 			->get([$this, 'getAuthStatus'])
 			->auth('public')
-			->rateLimit();
+			->rateLimit()
+			->register();
 
 		// Standard login
 		Route::for('auth/login')
@@ -51,7 +52,8 @@
 				'redirect_to' => 'string',
 			])
 			->auth('public')
-			->rateLimit(5, 300);
+			->rateLimit(5, 300)
+			->register();
 
 		// User registration
 		Route::for('auth/register')
@@ -64,7 +66,8 @@
 				'redirect_to' => 'string',
 			])
 			->auth('public')
-			->rateLimit(3, 3600);
+			->rateLimit(3, 3600)
+			->register();
 
 		// Request password reset
 		Route::for('auth/lostpassword')
@@ -73,7 +76,8 @@
 				'user_email' => 'email|required',
 			])
 			->auth('public')
-			->rateLimit(3, 3600);
+			->rateLimit(3, 3600)
+			->register();
 
 		// Reset password with token
 		Route::for('auth/resetpass')
@@ -85,7 +89,8 @@
 				'pass2' => 'string|required',
 			])
 			->auth('public')
-			->rateLimit(5, 300);
+			->rateLimit(5, 300)
+			->register();
 
 		// Magic link endpoint
 		if ($this->hasMagicLink) {
@@ -97,16 +102,16 @@
 					'redirect_to' => 'string',
 				])
 				->auth('public')
-				->rateLimit(5, 3600);
+				->rateLimit(5, 3600)
+				->register();
 		}
 
 		// Logout endpoint
 		Route::for('auth/logout')
 			->post([$this, 'handleLogout'])
 			->auth('logged_in')
-			->rateLimit(10, 60);
-
-		error_log('=================== LOGIN ROUTES REGISTERED ===================');
+			->rateLimit(10)
+			->register();
 	}
 
 	/**
diff --git a/inc/rest/routes/NewsRoutes.php b/inc/rest/routes/NewsRoutes.php
index bb9caae..b0aedfc 100644
--- a/inc/rest/routes/NewsRoutes.php
+++ b/inc/rest/routes/NewsRoutes.php
@@ -60,13 +60,15 @@
 				'type' => 'integer',
 			])
 			->auth(PermissionHandler::combine(['user','nonce',['actionNonce'=>'dash-']]))
-			->rateLimit(30);
+			->rateLimit(30)
+			->register();
 
 		Route::for(Route::pattern('news/{id}'))
 			->get([$this, 'getNewsItem'])
 			->arg('id', 'integer|required')
 			->auth(PermissionHandler::combine(['user','nonce', ['actionNonce'=>'dash-']]))
-			->rateLimit(30);
+			->rateLimit(30)
+			->register();
     }
 
     /**
diff --git a/inc/rest/routes/NotificationsRoutes.php b/inc/rest/routes/NotificationsRoutes.php
index f523b62..ee85aeb 100644
--- a/inc/rest/routes/NotificationsRoutes.php
+++ b/inc/rest/routes/NotificationsRoutes.php
@@ -152,7 +152,8 @@
 				'offset' => 'integer|default:0',
 			])
 			->auth('user')
-			->rateLimit(30);
+			->rateLimit(30)
+			->register();
 
 		// Mark as read
 		Route::for('notifications/read')
@@ -162,7 +163,8 @@
 				'notification_id' => 'integer|required',
 			])
 			->auth('user')
-			->rateLimit(30);
+			->rateLimit(30)
+			->register();
 
 		// Mark all as read
 		Route::for('notifications/read-all')
@@ -172,7 +174,8 @@
 				'type' => 'string',
 			])
 			->auth('user')
-			->rateLimit(10);
+			->rateLimit(10)
+			->register();
 
 		// Mark as actioned
 		Route::for('notifications/action')
@@ -182,7 +185,8 @@
 				'notification_id' => 'integer|required',
 			])
 			->auth('user')
-			->rateLimit(30);
+			->rateLimit(30)
+			->register();
 
 		// Dismiss notification
 		Route::for('notifications/dismiss')
@@ -192,7 +196,8 @@
 				'notification_id' => 'integer|required',
 			])
 			->auth('user')
-			->rateLimit(30);
+			->rateLimit(30)
+			->register();
 
 		// Get unread count
 		Route::for('notifications/count')
@@ -202,7 +207,8 @@
 				'type' => 'string',
 			])
 			->auth('user')
-			->rateLimit(60);
+			->rateLimit()
+			->register();
 	}
 
 	// =========================================================================
diff --git a/inc/rest/routes/OptionsRoutes.php b/inc/rest/routes/OptionsRoutes.php
index 2bb2c2c..355db8c 100644
--- a/inc/rest/routes/OptionsRoutes.php
+++ b/inc/rest/routes/OptionsRoutes.php
@@ -32,7 +32,8 @@
 			->args([
 				'user'	=> 'int|required',
 				'id'	=> 'string|required',
-			]);
+			])
+			->register();
 	}
 
 	public function saveOptions(WP_REST_Request $request):WP_REST_Response
diff --git a/inc/rest/routes/QueueRoutes.php b/inc/rest/routes/QueueRoutes.php
index 3c27a3a..ad4507f 100644
--- a/inc/rest/routes/QueueRoutes.php
+++ b/inc/rest/routes/QueueRoutes.php
@@ -48,7 +48,8 @@
 				'action' => 'string|required|enum:dismiss,retry,cancel',
 			])
 			->auth('user')
-			->rateLimit(30);
+			->rateLimit(30)
+			->register();
 
 		// Poll endpoint
 		Route::for('queue/poll')
@@ -58,20 +59,23 @@
 				'ids' => 'string',
 			])
 			->auth('user')
-			->rateLimit(15);
+			->rateLimit(15)
+			->register();
 
 		// Errors endpoint
 		Route::for('queue/errors')
 			->get([$this, 'getOperationErrors'])
 			->auth('user')
-			->rateLimit(15);
+			->rateLimit(15)
+			->register();
 
 		// Single operation with dynamic ID
 		Route::for(Route::pattern('queue/{id}'))
 			->get([$this, 'getOperation'])
 			->arg('id', 'string|required')
 			->auth('user')
-			->rateLimit(15);
+			->rateLimit(15)
+			->register();
 	}
 
 	/**
@@ -84,6 +88,7 @@
 	{
 		$params = $request->get_params();
 		$user_id = absint($params['user']);
+		$this->cache = Cache::for($user_id.'_queue');
 		$status = sanitize_text_field($params['status']);
 		$ids = !empty($params['ids'])
 			? array_map('trim', array_map('sanitize_text_field', explode(',', $params['ids'])))
@@ -145,6 +150,8 @@
 		$action = sanitize_text_field($data['action'] ?? '');
 		$user_id = absint($data['user']);
 
+		$this->cache = Cache::for($user_id.'_queue');
+
 		// Validate input
 		if (empty($ids)) {
 			return Response::validationError(['ids' => 'Missing or invalid operation IDs']);
@@ -176,6 +183,7 @@
 	public function pollQueue(WP_REST_Request $request): WP_REST_Response
 	{
 		$userId = $request->get_param('user');
+		$this->cache = Cache::for($userId.'_queue');
 		$since = $request->get_param('since');
 		$ids = $request->get_param('ids');
 
@@ -214,6 +222,7 @@
 	public function getOperationErrors(WP_REST_Request $request): WP_REST_Response
 	{
 		$user_id = absint($request->get_param('user'));
+		$this->cache = Cache::for($user_id.'_queue');
 		$operations = JVB()->queue()->getUserOperations($user_id, [
 			'state' => 'completed',
 			'outcome' => ['failed', 'failed_permanent', 'partial'],
@@ -239,6 +248,7 @@
 	{
 		$id = $request->get_param('id');
 		$userId = $request->get_param('user');
+		$this->cache = Cache::for($userId.'_queue');
 
 		$op = JVB()->queue()->get($id);
 
diff --git a/inc/rest/routes/ReferralRoutes.php b/inc/rest/routes/ReferralRoutes.php
index 787f22f..bd8ab1d 100644
--- a/inc/rest/routes/ReferralRoutes.php
+++ b/inc/rest/routes/ReferralRoutes.php
@@ -56,7 +56,8 @@
 				'action' => 'string|required|enum:invite,consulted,treated,remove,resend'
 			])
 			->auth('user')
-			->rateLimit(10);
+			->rateLimit(10)
+			->register();
 
 		// Referral code endpoint
 		Route::for('referrals/code')
@@ -67,32 +68,37 @@
 			->post([$this, 'validateCode'])
 			->args(['code' => 'string|required'])
 			->auth('public')
-			->rateLimit(10);
+			->rateLimit(10)
+			->register();
 
 		// Stats endpoint
 		Route::for('referrals/stats')
 			->get([$this, 'getStats'])
 			->args(['user' => 'integer'])
 			->auth('user')
-			->rateLimit(30);
+			->rateLimit(30)
+			->register();
 
 		// Settings endpoint (admin only)
 		Route::for('referrals/settings')
 			->get([$this, 'getSettings'])
 			->post([$this, 'updateSettings'])
 			->auth('admin')
-			->rateLimit(10);
+			->rateLimit(10)
+			->register();
 
 		// CSV Upload endpoints (admin only)
 		Route::for('referrals/upload-clients')
 			->post([$this, 'handleClientUpload'])
 			->auth('admin')
-			->rateLimit(3);
+			->rateLimit(3)
+			->register();
 
 		Route::for('referrals/upload-sales')
 			->post([$this, 'handleSalesUpload'])
 			->auth('admin')
-			->rateLimit(3);
+			->rateLimit(3)
+			->register();
 	}
 
 	/**
diff --git a/inc/rest/routes/SEORoutes.php b/inc/rest/routes/SEORoutes.php
index e5154ed..52602a6 100644
--- a/inc/rest/routes/SEORoutes.php
+++ b/inc/rest/routes/SEORoutes.php
@@ -43,14 +43,16 @@
 				'action' => 'string|required|enum:save,reset,preview',
 				'context'=> 'string|required'
 			])
-			->rateLimit(30);
+			->rateLimit(30)
+			->register();
 
 		Route::for('seo/fields')
 			->get([$this, 'getFields'])
 			->auth('admin')
 			->args([
 				'type'=>'string|required'
-			]);
+			])
+			->register();
 	}
 
 	/**
diff --git a/inc/rest/routes/SettingsRoutes.php b/inc/rest/routes/SettingsRoutes.php
index f49c2bd..26b77ea 100644
--- a/inc/rest/routes/SettingsRoutes.php
+++ b/inc/rest/routes/SettingsRoutes.php
@@ -38,7 +38,8 @@
 		Route::for('settings')
 			->post([$this, 'saveSettings'])
 			->auth(PermissionHandler::combine(['admin', ['actionNonce' => 'dash-']]))
-			->rateLimit(20);
+			->rateLimit(20)
+			->register();
     }
 
     /**
diff --git a/inc/rest/routes/TermRoutes.php b/inc/rest/routes/TermRoutes.php
index 7f03d49..431c7b5 100644
--- a/inc/rest/routes/TermRoutes.php
+++ b/inc/rest/routes/TermRoutes.php
@@ -56,12 +56,14 @@
 				'taxonomy'	=> 'string|required',
 				'name'		=> 'string|required',
 				'parent'	=> 'int|default:0',
-			]);
+			])
+			->register();
 
 		Route::for('terms/check')
 			->get([$this,'getTermDetails'])
 			->auth('public')
-			->rateLimit();
+			->rateLimit()
+			->register();
     }
 
     /**
diff --git a/inc/rest/routes/UploadRoutes.php b/inc/rest/routes/UploadRoutes.php
index 04e5488..39518ae 100644
--- a/inc/rest/routes/UploadRoutes.php
+++ b/inc/rest/routes/UploadRoutes.php
@@ -99,7 +99,8 @@
 				'id' 		=> 'string|required',
 				'content' 	=> 'string|required',
 				'user'		=> 'int|required'
-			]);
+			])
+			->register();
 
 		Route::for('uploads/meta')
 			->post([$this, 'handleMetadataUpdate'])
@@ -108,7 +109,8 @@
 			->args([
 				'user'	=> 'int|required',
 				'items'	=> 'array|required'
-			]);
+			])
+			->register();
     }
 
 	/**
diff --git a/inc/rest/routes/VoteRoutes.php b/inc/rest/routes/VoteRoutes.php
index 1ddf49f..b79ec08 100644
--- a/inc/rest/routes/VoteRoutes.php
+++ b/inc/rest/routes/VoteRoutes.php
@@ -46,7 +46,8 @@
 				'user' => 'integer',
 			])
 			->auth('user')
-			->rateLimit(30);
+			->rateLimit(30)
+			->register();
     }
 
     /**
diff --git a/inc/ui/CRUDSkeleton.php b/inc/ui/CRUDSkeleton.php
index 3fa2dc6..481f0e1 100644
--- a/inc/ui/CRUDSkeleton.php
+++ b/inc/ui/CRUDSkeleton.php
@@ -1576,7 +1576,6 @@
 					$tabs = false;
 				}
 
-
 				$fields = $this->fields;
 				if (!$this->isTimeline) {
 					$first = ['post_thumbnail', 'post_title', 'price'];
@@ -1586,9 +1585,8 @@
 							if ($tabs) {
 								$tabs['basic']['content'] .= Form::render($f, '', $fields[$f]);
 							} else {
-								Form::render($f, '', $fields[$f]);
+								echo Form::render($f, '', $fields[$f]);
 							}
-
 							unset($fields[$f]);
 						}
 					}
@@ -1629,7 +1627,7 @@
 						if (in_array($config['type'], ['taxonomy', 'selector'])) {
 							$config = array_merge($config, $this->taxConfig($config['taxonomy'], $config['label']));
 						}
-						Form::render($n, '', $config);
+						echo Form::render($n, '', $config);
 					}
 				}
 
diff --git a/inc/ui/Checkout.php b/inc/ui/Checkout.php
new file mode 100644
index 0000000..974d09f
--- /dev/null
+++ b/inc/ui/Checkout.php
@@ -0,0 +1,285 @@
+<?php
+namespace JVBase\ui;
+
+use JVBase\meta\Form;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+/**
+ * Shared Checkout UI
+ *
+ * Provider-agnostic checkout markup used by any payment integration
+ * (Square, Helcim, etc). Integrations hook into this via:
+ *   - add_filter('jvbAdditionalActions', [Checkout::class, 'render'])
+ *
+ * The active provider is determined by `jvbGetPaymentProvider()`,
+ * which returns the integration instance (Square or Helcim).
+ *
+ * Provider-specific differences are handled by:
+ *   - data-provider attribute on the form (for JS to detect)
+ *   - #payment-container (provider JS attaches its own UI here)
+ *   - Filterable sections for provider-specific content
+ *
+ * @since 1.0.0
+ */
+class Checkout
+{
+	/**
+	 * Render the checkout aside and append to actions.
+	 *
+	 * Hooked via: add_filter('jvbAdditionalActions', [Checkout::class, 'render'])
+	 */
+	public static function render(array $actions): array
+	{
+		if (is_singular(BASE . 'dash') || is_post_type_archive(BASE . 'dash')) {
+			return $actions;
+		}
+
+		$provider = jvbGetPaymentProvider();
+		if (!$provider || !$provider->isSetUp()) {
+			return $actions;
+		}
+
+		$providerName = strtolower($provider->getServiceName());
+
+		$form = '<aside id="cart" class="right main">
+			<form id="checkout" data-form-id="checkout" data-save="checkout" data-provider="' . esc_attr($providerName) . '">';
+
+		$tabs = [
+			'cartItems' => [
+				'title'       => 'Your Order',
+				'icon'        => 'cart',
+				'description' => 'Here\'s your order. You can change quantities, remove items, or clear your cart.',
+				'content'     => self::cartContent(),
+			],
+			'checkout' => [
+				'title'       => 'Checkout',
+				'icon'        => 'checkout',
+				'description' => apply_filters('jvb_checkout_description',
+					'Securely checkout with your name, email, and payment.',
+					$providerName
+				),
+				'content' => self::checkoutContent($providerName),
+			],
+			'order' => [
+				'title'       => 'Your Order',
+				'icon'        => 'truck',
+				'hidden'      => true,
+				'description' => '',
+				'content'     => self::orderStatus(),
+			],
+		];
+
+		$form .= jvbRenderTabs($tabs, true);
+
+		$form .= '<div class="cart-total row end">
+				<p class="tax">Tax: <span></span></p>
+				<p class="total">GRAND TOTAL: <span></span></p>
+			</div>
+		</form>
+		</aside>';
+
+		$form .= self::templates($providerName);
+
+		$actions[] = [
+			'button'  => self::toggleButton(),
+			'content' => $form,
+		];
+
+		return $actions;
+	}
+
+	/*****************************************************************
+	 * SECTIONS
+	 *****************************************************************/
+
+	/**
+	 * Checkout tab content: customer info + payment container
+	 */
+	private static function checkoutContent(string $provider): string
+	{
+		$fields = '<div class="checkout-section">
+			<h3>Customer Information</h3>'
+			. Form::render('cart_name', null, [
+				'type'         => 'text',
+				'label'        => 'Your Name',
+				'required'     => true,
+				'autocomplete' => 'name',
+			])
+			. Form::render('cart_email', null, [
+				'type'         => 'email',
+				'label'        => 'Your Email',
+				'required'     => true,
+				'autocomplete' => 'email',
+			])
+			. Form::render('cart_phone', null, [
+				'type'         => 'tel',
+				'label'        => 'Your Phone',
+				'required'     => true,
+				'autocomplete' => 'phone',
+			]);
+
+		// Optional sections — integrations can add pickup, scheduling, etc.
+		$fields .= apply_filters('jvb_checkout_fields', '', $provider);
+
+		$fields .= '</div>';
+
+		// Payment section — provider JS mounts its own UI inside #payment-container
+		$fields .= '<div class="checkout-section">
+			<h3>Payment Information</h3>
+			<div id="saved-cards"></div>
+			<div id="payment-container" data-provider="' . esc_attr($provider) . '"></div>
+		</div>';
+
+		return $fields;
+	}
+
+	/**
+	 * Cart items tab: table + account details
+	 */
+	private static function cartContent(): string
+	{
+		ob_start();
+		?>
+		<div class="cart-items">
+			<table>
+				<thead>
+				<tr>
+					<th scope="col">Item</th>
+					<th scope="col">Price</th>
+					<th scope="col">Total</th>
+				</tr>
+				</thead>
+				<tbody></tbody>
+			</table>
+		</div>
+
+		<details class="account">
+			<summary>
+				<?php
+				if (is_user_logged_in()) {
+					echo 'Your Favourites and Order History';
+				} else {
+					echo '<a href="' . wp_login_url(get_the_permalink()) . '">Log in</a> to save your favourites and view order history.';
+				}
+				?>
+			</summary>
+			<?php
+			if (is_user_logged_in()) {
+				$tabs = [
+					'history' => [
+						'title'       => 'Order History',
+						'icon'        => 'checkout',
+						'description' => 'View your past orders and quickly reorder',
+						'content'     => apply_filters('jvb_checkout_order_history', ''),
+					],
+					'favourites' => [
+						'title'       => 'Favourites',
+						'icon'        => 'heart',
+						'description' => 'View your favourites',
+						'content'     => apply_filters('jvb_checkout_favourites', ''),
+					],
+				];
+				jvbRenderTabs($tabs);
+			}
+			?>
+		</details>
+		<?php
+		return ob_get_clean();
+	}
+
+	/**
+	 * Order confirmation / status tracking
+	 */
+	private static function orderStatus(): string
+	{
+		$statuses = apply_filters('jvb_checkout_order_statuses', [
+			'received'  => 'Order Received',
+			'preparing' => 'Preparing',
+			'ready'     => 'Ready for Pickup',
+		]);
+
+		ob_start();
+		?>
+		<div class="order-confirmation">
+			<h2>Order Confirmed!</h2>
+			<div id="order-status" data-order="">
+				<p>Order #<span class="order-num"></span></p>
+				<div class="status-timeline">
+					<?php foreach ($statuses as $key => $label): ?>
+						<div class="status-item<?php echo $key === array_key_first($statuses) ? ' active' : ''; ?>"
+							 data-status="<?php echo esc_attr($key); ?>">
+							<?php echo esc_html($label); ?>
+						</div>
+					<?php endforeach; ?>
+				</div>
+				<div class="pickup-time">
+					Estimated pickup: <span id="eta">Calculating...</span>
+				</div>
+			</div>
+		</div>
+		<?php
+		return ob_get_clean();
+	}
+
+	/*****************************************************************
+	 * TEMPLATES — cloned by JS at runtime
+	 *****************************************************************/
+
+	private static function templates(string $provider): string
+	{
+		// Browse link is filterable per-site
+		$browseUrl  = apply_filters('jvb_checkout_browse_url', '#');
+		$browseText = apply_filters('jvb_checkout_browse_text', 'browse our products');
+
+		return '<template class="restoredCart">
+			<div class="restored">
+				<h3>Looks like we left things hanging</h3>
+				<p>We\'ve restored your cart from your last session below.</p>
+				<p>If you\'d rather start over, click the button below.</p>
+				<div class="row btw">
+					<button type="button" data-clear-cart>' . jvbIcon('trash') . 'Clear Cart</button>
+					<button type="button" data-dismiss>' . jvbIcon('x') . 'Dismiss</button>
+				</div>
+			</div>
+		</template>
+		<template class="cartItem">
+			<tr class="item">
+				<td class="item">
+					<label for="quantity"></label>
+					<div class="quantity field" data-min="0" data-max="50" data-step="1" data-price="" data-id="" data-catalog-id="">
+						<button type="button" class="decrease" aria-label="Decrease quantity">' . jvbIcon('minus-square') . '</button>
+						<input type="number" id="quantity" name="quantity" value="0" min="0" max="50" step="1" class="quantity-input">
+						<button type="button" class="increase" aria-label="Increase quantity">' . jvbIcon('plus-square') . '</button>
+					</div>
+				</td>
+				<td class="price"><span class="price"></span></td>
+				<td class="total"><span class="total"></span></td>
+				<td>
+					<button type="button" data-remove-from-cart>' . jvbIcon('trash') . '</button>
+				</td>
+			</tr>
+		</template>
+		<template class="emptyCart">
+			<div class="empty">
+				<p><i><b>No items in cart.</b></i></p>
+				<p>You can <a href="' . esc_url($browseUrl) . '" title="' . esc_attr($browseText) . '">' . $browseText . '</a> to order.</p>
+			</div>
+		</template>';
+	}
+
+	/**
+	 * Cart toggle button
+	 */
+	private static function toggleButton(): string
+	{
+		return '<button type="button" class="toggle-cart row" title="Your Cart"
+				data-action="toggle-cart" aria-label="Open Cart"
+				aria-controls="checkout" aria-expanded="false">'
+			. jvbIcon('shopping-cart')
+			. '<span class="abs"></span><span class="abs count"></span>
+		</button>';
+	}
+}
diff --git a/inc/ui/_setup.php b/inc/ui/_setup.php
index 29cfcc0..5e9a8bf 100644
--- a/inc/ui/_setup.php
+++ b/inc/ui/_setup.php
@@ -3,3 +3,4 @@
 require(JVB_DIR.'/inc/ui/Navigation.php');
 require(JVB_DIR.'/inc/ui/Tabs.php');
 require(JVB_DIR.'/inc/ui/CRUDSkeleton.php');
+require(JVB_DIR.'/inc/ui/Checkout.php');
diff --git a/webpack.jvb.js b/webpack.jvb.js
index 7983537..7995231 100644
--- a/webpack.jvb.js
+++ b/webpack.jvb.js
@@ -7,7 +7,7 @@
 		'a11y':                './assets/js/concise/A11yHelper.js',
 		'auth':                './assets/js/concise/AuthManager.js',
 		// 'admin':               './assets/js/dash/Admin.js',
-		'bioManager':          './assets/js/concise/BioManager.js',
+		// 'bioManager':          './assets/js/concise/BioManager.js',
 		'ContentManager':      './assets/js/concise/ContentManager.js',
 		'hours':               './assets/js/concise/CopyHours.js',
 		'crud':                './assets/js/concise/CRUD.js',
@@ -40,7 +40,9 @@
 		'shopManager':         './assets/js/concise/ShopManager.js',
 		'cache':               './assets/js/concise/SimpleCache.js',
 		'schema':              './assets/js/concise/SchemaManager.js',
-		'square':              './assets/js/concise/SquareCheckout.js',
+		'square':              './assets/js/concise/CheckoutSquare.js',
+		'helcim':              './assets/js/concise/CheckoutHelcim.js',
+		'checkout':              './assets/js/concise/Checkout.js',
 		'tabs':                './assets/js/concise/Tabs.js',
 		'creator':             './assets/js/concise/TaxonomyCreator.js',
 		'selector':            './assets/js/concise/TaxonomySelector.js',

--
Gitblit v1.10.0