From c204185ae86a98994f80010abf35a190c9406739 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sun, 12 Jul 2026 18:08:19 +0000
Subject: [PATCH] =Refactor of Integrations.php. Separated different functionality into traits that classes can use to add that functionality. Hopefully will make maintaining it a little easier. Still have to finish up, as well as refactoring the individual classes to utilize the new system.

---
 inc/integrations/RateLimits.php                |  103 +
 inc/integrations/Requests.php                  |  114 +
 build/menu/style-index-rtl.css                 |    2 
 inc/integrations/SyncFrom.php                  |   41 
 inc/integrations/services/GoogleMyBusiness.php |    8 
 inc/integrations/services/Cloudflare.php       |    4 
 inc/integrations/services/PostMark.php         |    7 
 inc/integrations/_Base.php                     |  110 +
 inc/managers/Dashboard/DashboardManager.php    |   34 
 src/menu/style.scss                            |    4 
 inc/rest/routes/FeedRoutes.php                 |    8 
 inc/integrations/SyncTo.php                    |  310 ++++
 inc/ui/Checkout.php                            |   12 
 build/menu/style-index.css                     |    2 
 inc/integrations/_setup.php                    |   29 
 inc/integrations/services/Helcim.php           |   16 
 inc/integrations/Admin.php                     |  233 +++
 inc/integrations/OAuth.php                     |  351 +++++
 inc/integrations/SyncDefaults.php              |   13 
 JVBase.php                                     |   72 
 inc/rest/routes/IntegrationsSquareRoutes.php   |    4 
 inc/integrations/Field.php                     |   68 
 inc/integrations/services/BlueSky.php          |    8 
 inc/integrations/services/Square.php           |   55 
 inc/integrations/Integrations.php              | 1902 +--------------------------
 inc/integrations/AdminActionResult.php         |   11 
 inc/integrations/services/Facebook.php         |   11 
 inc/integrations/AdminActions.php              |  105 +
 inc/integrations/Webhooks.php                  |  125 +
 inc/integrations/Ajax.php                      |   10 
 inc/integrations/SyncHelpers.php               |  144 ++
 inc/integrations/services/GoogleMaps.php       |    4 
 inc/integrations/OAuthCredentials.php          |   47 
 inc/integrations/UserConnection.php            |   62 
 inc/integrations/services/Umami.php            |   12 
 inc/integrations/services/Instagram.php        |   16 
 inc/integrations/Auth.php                      |   60 
 37 files changed, 2,114 insertions(+), 2,003 deletions(-)

diff --git a/JVBase.php b/JVBase.php
index e771147..36f94ca 100644
--- a/JVBase.php
+++ b/JVBase.php
@@ -1,52 +1,52 @@
 <?php
 namespace JVBase;
 
-use JVBase\blocks\CustomBlocks;
+use JVBase\admin\SEOAdmin;
+use JVBase\base\SchemaHelper;
 use JVBase\base\Site;
+use JVBase\blocks\CustomBlocks;
+use JVBase\managers\AdminPages;
 use JVBase\managers\ApprovalManager;
+use JVBase\managers\Dashboard\DashboardManager;
+use JVBase\managers\DirectoryManager;
 use JVBase\managers\EmailManager;
 use JVBase\managers\ErrorHandler;
 use JVBase\managers\FavouritesManager;
 use JVBase\managers\InvitationsManager;
 use JVBase\managers\LoginManager;
 use JVBase\managers\MagicLinkManager;
+use JVBase\managers\NotificationManager;
 use JVBase\managers\queue\Queue;
-use JVBase\managers\Dashboard\DashboardManager;
-use JVBase\managers\DirectoryManager;
 use JVBase\managers\ReferralManager;
 use JVBase\managers\RoleManager;
 use JVBase\managers\SEO\render\SchemaOutput;
-use JVBase\admin\SEOAdmin;
-use JVBase\managers\AdminPages;
-use JVBase\managers\NotificationManager;
 use JVBase\managers\TaxonomyRelationships;
 use JVBase\managers\UserTermsManager;
 use JVBase\registrar\Registrar;
-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\AdminRoutes;
+use JVBase\rest\routes\ApprovalRoutes;
 use JVBase\rest\routes\ContentRoutes;
-use JVBase\rest\routes\TermRoutes;
-use JVBase\rest\routes\UploadRoutes;
-use JVBase\rest\routes\SettingsRoutes;
 use JVBase\rest\routes\ContentTermsRoutes;
-use JVBase\rest\routes\SEORoutes;
-use JVBase\rest\routes\QueueRoutes;
 use JVBase\rest\routes\ErrorRoutes;
+use JVBase\rest\routes\FavouritesRoutes;
+use JVBase\rest\routes\FeedRoutes;
 use JVBase\rest\routes\FormRoutes;
+use JVBase\rest\routes\IntegrationsHelcimRoutes;
+use JVBase\rest\routes\IntegrationsRoutes;
+use JVBase\rest\routes\IntegrationsSquareRoutes;
+use JVBase\rest\routes\Invitations;
 use JVBase\rest\routes\LoginRoutes;
 use JVBase\rest\routes\NewsRoutes;
+use JVBase\rest\routes\NotificationsRoutes;
+use JVBase\rest\routes\OptionsRoutes;
+use JVBase\rest\routes\QueueRoutes;
 use JVBase\rest\routes\ReferralRoutes;
 use JVBase\rest\routes\ResponseRoutes;
-use JVBase\rest\routes\OptionsRoutes;
+use JVBase\rest\routes\SEORoutes;
+use JVBase\rest\routes\SettingsRoutes;
+use JVBase\rest\routes\TermRoutes;
+use JVBase\rest\routes\UploadRoutes;
 use JVBase\rest\routes\VoteRoutes;
-use JVBase\rest\routes\Invitations;
-use JVBase\rest\routes\ApprovalRoutes;
-use JVBase\rest\routes\AdminRoutes;
-use JVBase\rest\routes\IntegrationsRoutes;
-use JVBase\base\SchemaHelper;
 use UserRoutes;
 
 if (!defined('ABSPATH')) {
@@ -65,16 +65,16 @@
 	protected CustomBlocks $customBlocks;
 
 	protected array $serviceMap = [
-		'maps' => 'JVBase\integrations\GoogleMaps',
-		'square' => 'JVBase\integrations\Square',
-		'facebook' => 'JVBase\integrations\Facebook',
-		'helcim' => 'JVBase\integrations\Helcim',
-		'instagram' => 'JVBase\integrations\Instagram',
-		'bluesky' => 'JVBase\integrations\BlueSky',
-		'gmb' => 'JVBase\integrations\GoogleMyBusiness',
-		'cloudflare' => 'JVBase\integrations\Cloudflare',
-		'umami' => 'JVBase\integrations\Umami',
-		'postmark' => 'JVBase\integrations\PostMark',
+		'maps' => 'JVBase\integrations\services\GoogleMaps',
+		'square' => 'JVBase\integrations\services\Square',
+		'facebook' => 'JVBase\integrations\services\Facebook',
+		'helcim' => 'JVBase\integrations\services\Helcim',
+		'instagram' => 'JVBase\integrations\services\Instagram',
+		'bluesky' => 'JVBase\integrations\services\BlueSky',
+		'gmb' => 'JVBase\integrations\services\GoogleMyBusiness',
+		'cloudflare' => 'JVBase\integrations\services\Cloudflare',
+		'umami' => 'JVBase\integrations\services\Umami',
+		'postmark' => 'JVBase\integrations\services\PostMark',
 	];
 
 	public static function getInstance(): JVB
@@ -307,9 +307,11 @@
 //
 //			return $this->serviceMap[$service]::getInstance($userID);
 //		}
-		return is_null($userID) ?
-			$this->integrations[$service]??null :
-			(array_key_exists($service, $this->integrations) ? $this->serviceMap[$service]::getInstance($userID) : null);
+		$instance = $this->integrations[$service]??null;
+		if ($instance) {
+			$instance->switchUser($userID);
+		}
+		return $instance;
 	}
 
 	public function userCanConnect(string $service, int $userID): bool
diff --git a/build/menu/style-index-rtl.css b/build/menu/style-index-rtl.css
index 899b4f8..8007299 100644
--- a/build/menu/style-index-rtl.css
+++ b/build/menu/style-index-rtl.css
@@ -1 +1 @@
-.info h4{font-size:var(--txt-medium);font-weight:var(--fw-h);margin:0}.field.quantity>label{right:var(--offScreen);position:absolute}nav.menu{max-width:100%}nav.menu ul{--justify:flex-start}nav.menu ul li{padding:0 .5rem}main>header{grid-column:full}main>header h1,main>section *{max-width:var(--wide)}.menu-item.menu-item{margin:var(--sp2);max-width:none}.menu-item.menu-item+.menu-item{border-top:1px solid rgb(var(--base-200));padding-top:var(--sp4)}.menu-item.menu-item .header{flex-wrap:nowrap}.menu-item.menu-item .header h3,.menu-item.menu-item .header p{margin:0;width:-moz-max-content;width:max-content}
+.info h4{font-size:var(--txt-medium);font-weight:var(--fw-h);margin:0}.field.quantity.quantity>label{right:var(--offScreen);position:absolute}nav.menu{max-width:100%}nav.menu ul{--justify:flex-start}nav.menu ul li{padding:0 .5rem}main>header{grid-column:full}main>header h1{max-width:var(--wide)}main>section *{max-width:var(--wide)!important}.menu-item.menu-item{margin:var(--sp2);max-width:none}.menu-item.menu-item+.menu-item{border-top:1px solid rgb(var(--base-200));padding-top:var(--sp4)}.menu-item.menu-item .header{flex-wrap:nowrap}.menu-item.menu-item .header h3,.menu-item.menu-item .header p{margin:0;width:-moz-max-content;width:max-content}
diff --git a/build/menu/style-index.css b/build/menu/style-index.css
index 59fbfe6..531238d 100644
--- a/build/menu/style-index.css
+++ b/build/menu/style-index.css
@@ -1 +1 @@
-.info h4{font-size:var(--txt-medium);font-weight:var(--fw-h);margin:0}.field.quantity>label{left:var(--offScreen);position:absolute}nav.menu{max-width:100%}nav.menu ul{--justify:flex-start}nav.menu ul li{padding:0 .5rem}main>header{grid-column:full}main>header h1,main>section *{max-width:var(--wide)}.menu-item.menu-item{margin:var(--sp2);max-width:none}.menu-item.menu-item+.menu-item{border-top:1px solid rgb(var(--base-200));padding-top:var(--sp4)}.menu-item.menu-item .header{flex-wrap:nowrap}.menu-item.menu-item .header h3,.menu-item.menu-item .header p{margin:0;width:-moz-max-content;width:max-content}
+.info h4{font-size:var(--txt-medium);font-weight:var(--fw-h);margin:0}.field.quantity.quantity>label{left:var(--offScreen);position:absolute}nav.menu{max-width:100%}nav.menu ul{--justify:flex-start}nav.menu ul li{padding:0 .5rem}main>header{grid-column:full}main>header h1{max-width:var(--wide)}main>section *{max-width:var(--wide)!important}.menu-item.menu-item{margin:var(--sp2);max-width:none}.menu-item.menu-item+.menu-item{border-top:1px solid rgb(var(--base-200));padding-top:var(--sp4)}.menu-item.menu-item .header{flex-wrap:nowrap}.menu-item.menu-item .header h3,.menu-item.menu-item .header p{margin:0;width:-moz-max-content;width:max-content}
diff --git a/inc/integrations/Admin.php b/inc/integrations/Admin.php
new file mode 100644
index 0000000..122a459
--- /dev/null
+++ b/inc/integrations/Admin.php
@@ -0,0 +1,233 @@
+<?php
+namespace JVBase\integrations;
+
+use JVBase\meta\Form;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+trait Admin {
+	use _Base;
+	//An array of AdminAction objects
+	protected array $actions = [];
+	protected array $instructions = [];
+
+	protected array $fields = [];
+	protected function initializeActions():void
+	{
+		$actions = [
+			[
+				'name'		=> 'Save Credentials',
+				'label'		=> 'Save Changes',
+				'icon'		=> 'floppy-disk',
+				'action'	=> [$this, 'saveCredentials']
+			],
+			[
+				'name'		=> 'Clear Credentials',
+				'icon'		=> 'broom',
+				'action'	=> [$this, 'clearCredentials']
+			],
+		];
+		foreach ($actions as $a) {
+			$this->addAction($a['name'], $a['action'], $a['slug']??null, $a['icon']??null, $a['label']??null);
+		}
+	}
+
+	public function addAction(string $name, callable $callback, ?string $slug = null, ?string $icon = null, ?string $label = null):void
+	{
+		$action = new AdminActions($name, $callback);
+		if (!is_null($slug)) {
+			$action->setSlug($slug);
+		}
+		if (!is_null($icon)) {
+			$action->setIcon($icon);
+		}
+		if (!is_null($label)) {
+			$action->setLabel($label);
+		}
+		$this->actions[$action->getSlug()] = $action;
+	}
+	/**
+	 * @param string $action
+	 * @param mixed|null $data
+	 * @return array [
+	 * 		'success'	=> {bool},
+	 * 		'message'	=> {string} optional message,
+	 *		'data'		=> {mixed} optional data to return
+	 * ]
+	 */
+	public function processAction(string $action, mixed $data = null):array
+	{
+		if (!array_key_exists($action, $this->actions)) {
+			$this->logError('processAction', 'Attempted action not setup: '.print_r(['action' => $action, 'service' => $this->service_name], true));
+			return $this->response(false, 'Attempted action not set up');
+		}
+		$theAction = $this->actions[$action];
+		if (!$theAction->checkPermission()) {
+			$this->logError('processAction', 'User has no capabilities to perform this action: '.print_r(['action' => $action, 'service' => $this->service_name], true));
+			return  $this->response(false, 'Insufficient permissions');
+		}
+
+		$result = $theAction->handleCallback($data);
+		if (is_wp_error($result)) {
+			$this->logError('processAction', $result->get_error_message());
+			return $this->response(false, $result->get_error_message());
+		}
+
+		return $result;
+	}
+
+	public function response(bool $success, string $message):array
+	{
+		return [
+			'success'	=> $success,
+			'message'	=> $message
+		];
+	}
+
+	public function renderConnection():string
+	{
+		if ($this->userID && !JVB()->userCanConnect($this->service_name, $this->userID)) {
+			return '';
+		}
+
+		$credentials = $this->loadCredentials();
+		$isConnected = !empty($credentials);
+
+		$meta = '';
+		if ($isConnected && array_key_exists('updated_at', $credentials) && $credentials['updated_at'] > 0) {
+			$meta = sprintf(
+			'<div class="meta">
+					<small>Last updated: %s ago</small>
+				</div>',
+				human_time_diff($credentials['updated_at'])
+			);
+		}
+
+		$form = sprintf(
+		'<form id="%s" class="integration %s" data-service="%s" data-save="integrations">
+			<div class="header row x-btw">
+				<h3>%s</h3>
+				<div class="setup">
+					<span class="indicator">%s</span>
+					<span class="text">%sSet Up</span>
+				</div>
+			</div>
+			%s',
+			$this->service_name,
+			$isConnected ? 'connected' : 'disconnected',
+			esc_attr($this->service_name),
+			esc_html($this->title),
+			$isConnected ? '●' : '○',
+			$isConnected ? '' : 'Not ',
+			$meta,
+		);
+
+		if (!empty($this->instructions)) {
+			$form .= sprintf(
+				'<details%s>
+					<summary>Instructions</summary>
+					<ol>%s</ol>
+				</details>',
+				$isConnected ? '' : ' open',
+				implode('',array_map(function($el) {
+					return sprintf('<li>%s</li>', $el);
+				}, $this->instructions))
+			);
+		}
+
+
+		if (current_user_can('manage_options')) {
+			$form .= sprintf(
+				'<details class="initial-setup"%s>
+				<summary>Setup</summary>
+				%s
+				%s
+			</details>',
+				$isConnected ? '' : ' open',
+				$this->outputFields($this->credentialFields),
+				method_exists($this, 'renderWebhookUrl') ? $this->renderWebhookUrl() : ''
+			);
+		}
+
+		if (method_exists($this, 'renderOAuthConnectionForm')) {
+			$form .= $this->renderOAuthConnectionForm();
+		}
+
+		if (!empty($this->optionalFields) || $this->hasExtraOptions) {
+			$form .= '<div class="integration-content">';
+			if (!empty($this->optionalFields)) {
+				$form .= sprintf(
+				'<details class="advanced">
+						<summary>Advanced Settings</summary>%s</details>',
+					$this->outputFields($this->optionalFields)
+				);
+			}
+			if ($this->hasExtraOptions) {
+				$form .= sprintf(
+					'<a href="%s" class="btn">More Settings</a>',
+					$this->determineOptionPage()
+				);
+			}
+			$form .= '</div>';
+		}
+
+		$form .= $this->outputActionButtons();
+		$form .= '</form>';
+
+		return $form;
+	}
+		protected function outputActionButtons():string
+		{
+			return sprintf('<div class="actions row x-btw wrap">%s</div>',implode('', array_map(function($action) {
+				$label = '';
+				if (!empty ($action->getIcon())) {
+					$label .= $this->outputIcon($action->getIcon());
+				}
+				$label .= $action->getLabel();
+				return sprintf(
+					'<button type="button" data-action="%s"%s%s>%s</button>',
+					$action->getSlug,
+					$action->getName(),
+					empty($action->getConfirm()) ? '' : ' data-confirm="'.$action->getConfirm().'"',
+					$label
+				);
+			}, array_filter($this->actions, function ($action) {
+				return $action->checkPermission();
+			}))));
+		}
+
+	protected function outputFields(array $fields):string
+	{
+		$credentials = $this->loadCredentials();
+		$fields = array_filter($fields, function ($field) {
+			return $field->checkPermission();
+		});
+		return implode('', array_map(function($f) use ($credentials) {
+			$value = array_key_exists($f->name, $credentials) ? $credentials[$f->name] : '';
+			$config = $f->getConfig();
+			if (!empty($value)) {
+				$config['readonly'] = true;
+			}
+			$config['base'] = $this->service_name.'_';
+			return Form::render($f->name, $value, $f->getConfig());
+		}, $fields));
+	}
+
+	public function addField(string $name, string $label, string $type, callable|string|bool $permission):Field
+	{
+		$field = new Field($name, $label, $type, $permission);
+		$this->fields[$name] = $field;
+		return $field;
+	}
+		public function setFieldRequired(string $name):bool
+		{
+			if (!array_key_exists($name, $this->fields)) {
+				return false;
+			}
+			$this->fields[$name]->setRequired();
+			return true;
+		}
+
+}
diff --git a/inc/integrations/AdminActionResult.php b/inc/integrations/AdminActionResult.php
new file mode 100644
index 0000000..87d2d93
--- /dev/null
+++ b/inc/integrations/AdminActionResult.php
@@ -0,0 +1,11 @@
+<?php
+namespace JVBase\integrations;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+trait AdminActionResult {
+	public bool $success;
+	public array $result;
+}
diff --git a/inc/integrations/AdminActions.php b/inc/integrations/AdminActions.php
new file mode 100644
index 0000000..93e4e7f
--- /dev/null
+++ b/inc/integrations/AdminActions.php
@@ -0,0 +1,105 @@
+<?php
+namespace JVBase\integrations;
+
+use Closure;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+class AdminActions
+{
+	public string $slug;
+	public string $name;
+	public string $label;
+	public ?string $icon = null;
+	public string|Closure $permission = 'manage_options';
+	public Closure $callback;
+	public string $confirm = '';
+
+	public function __construct(string $name, callable $callback)
+	{
+		$this->name = $name;
+		$this->setSlug();
+	}
+
+	public function setSlug(?string $slug = null):void
+	{
+		if (is_null($slug)) {
+			$slug = $this->name;
+		}
+		$this->slug = $this->sanitizeSlug($slug);
+	}
+		protected function sanitizeSlug(string $slug):string
+		{
+			return str_replace('-','_',sanitize_title($slug));
+		}
+	public function getSlug():string
+	{
+		return $this->slug;
+	}
+
+	public function setIcon(string $icon):void
+	{
+		$this->icon = $icon;
+	}
+	public function getIcon():string|null
+	{
+		return $this->icon;
+	}
+
+	public function setLabel(string $label):void
+	{
+		$this->label = $label;
+	}
+	public function getLabel():string
+	{
+		if (isset($this->label)) {
+			return $this->label;
+		}
+		return $this->name;
+	}
+
+	public function setCallback(callable $callback):void
+	{
+		$this->callback = Closure::fromCallable($callback);
+	}
+	public function handleCallback(?array $data = null):mixed
+	{
+		if (is_callable($this->callback)) {
+			return ($this->callback)($data);
+		}
+		error_log('[AdminActions]::handleCallback No callback set for '.$this->name. ' action');
+		return false;
+	}
+
+	public function setPermission(string $permission):void
+	{
+		$this->permission = $permission;
+	}
+	public function checkPermission():bool
+	{
+		if (is_string($this->permission)) {
+			return current_user_can($this->permission);
+		} else if (is_callable($this->permission)) {
+			return ($this->permission)();
+		}
+		error_log('Check Permission failed as no permission set. '.print_r([
+			'permission'	=> $this->permission,
+			], true));
+		return false;
+	}
+
+	public function setConfirm(string $confirm):void
+	{
+		$this->confirm = $confirm;
+	}
+	public function getConfirm():string
+	{
+		return $this->confirm;
+	}
+	public function getName():string
+	{
+		return $this->name;
+	}
+}
diff --git a/inc/integrations/Ajax.php b/inc/integrations/Ajax.php
new file mode 100644
index 0000000..2cd8565
--- /dev/null
+++ b/inc/integrations/Ajax.php
@@ -0,0 +1,10 @@
+<?php
+namespace JVBase\integrations;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+trait Ajax {
+
+}
diff --git a/inc/integrations/CredentialsManager.php b/inc/integrations/Auth.php
similarity index 68%
rename from inc/integrations/CredentialsManager.php
rename to inc/integrations/Auth.php
index a3967a2..85dc232 100644
--- a/inc/integrations/CredentialsManager.php
+++ b/inc/integrations/Auth.php
@@ -1,15 +1,14 @@
 <?php
 namespace JVBase\integrations;
-
 use JVBase\managers\CustomTable;
 
 if (!defined('ABSPATH')) {
 	exit;
 }
 
-class CredentialsManager
+class Auth
 {
-	private static ?CredentialsManager $instance = null;
+	private static ?Auth $instance = null;
 	private string $encryption_key;
 	private CustomTable $table;
 
@@ -24,11 +23,16 @@
 		$table = CustomTable::for('integrations');
 		$table->setColumns([
 			'id'			=> 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
-			'user_id'		=> $table->getUserIDType().' NOT NULL',
+			'user_id'		=> $table->getUserIDType().' DEFAULT NULL',
 			'integration'	=> "ENUM('bluesky','cloudflare','facebook','google-maps','gmb','helcim','instagram','postmark','square','umami') NOT NULL",
-			'credentials'	=> 'varchar(255) DEFAULT NULL',
+			'credentials'	=> 'varchar(1000) DEFAULT NULL',
 			'created_at'	=> 'datetime DEFAULT CURRENT_TIMESTAMP',
-			'updated_at'	=> 'datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'
+			'updated_at'	=> 'datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP',
+			'last_tested'	=> 'datetime DEFAULT NULL',
+			'is_healthy'	=> 'tinyint(1) DEFAULT 1',
+			'webhook_healthy'	=> 'tinyint(1) DEFAULT 1',
+			'request_healthy'	=> 'tinyint(1) DEFAULT 1',
+			'oauth_healthy'		=> 'tinyint(1) DEFAULT 1',
 		]);
 
 		$table->setKeys([
@@ -46,7 +50,7 @@
 		$this->table = $table;
 	}
 
-	public static function getInstance(): CredentialsManager
+	public static function getInstance(): Auth
 	{
 		if (self::$instance === null) {
 			self::$instance = new self();
@@ -60,10 +64,19 @@
 	public function storeCredentials(string $service, array $credentials, ?int $userID = null): bool
 	{
 		$find = $this->findBy($service, $userID);
+		error_log('Storing Credentials: '.print_r($credentials, true));
+
 		$update = [
 			'credentials'	=> $this->encrypt(json_encode($credentials))
 		];
-		return (bool)$this->table->findOrCreate($find, $update);
+
+		error_log('Find: '.print_r($find, true));
+		error_log('Update: '.print_r($update, true));
+		$updated = $this->table->findOrCreate($find, $update);
+		error_log('Last error: '.print_r($this->table->getLastError(), true));
+		error_log('Updated: '.print_r($updated, true));
+
+		return (bool)$updated;
 	}
 
 	/**
@@ -92,7 +105,7 @@
 	protected function findBy(string $service, ?int $userID = null):array
 	{
 		return [
-			'user_id'	=> is_null($userID) ? 0 : $userID,
+			'user_id'	=> $userID,
 			'integration'=> $service
 		];
 	}
@@ -142,4 +155,33 @@
 	{
 		return $this->table->pluck('user_id', ['integration' => $service]);
 	}
+
+	public function updateTested(string $service, ?int $userID = null):void
+	{
+		$find = $this->findBy($service, $userID);
+		$this->table->update([
+			'last_tested'	=> time(),
+		], $find);
+	}
+
+	public function markUnhealthy(string $service, string $type = '', ?int $userID = null):void
+	{
+		$update = [];
+		switch ($type) {
+			case 'oauth':
+				$update['oauth_healthy'] = 0;
+				break;
+			case 'request':
+				$update['request_healthy'] = 0;
+				break;
+			case 'webhook':
+				$update['webhook_healthy'] = 0;
+				break;
+			default:
+				$update['is_healthy'] = 0;
+				break;
+		}
+		$find = $this->findBy($service, $userID);
+		$this->table->update($update, $find);
+	}
 }
diff --git a/inc/integrations/Field.php b/inc/integrations/Field.php
new file mode 100644
index 0000000..9217387
--- /dev/null
+++ b/inc/integrations/Field.php
@@ -0,0 +1,68 @@
+<?php
+namespace JVBase\integrations;
+
+use Closure;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+class Field {
+	public string $name;
+	public string $label;
+	public string $type;
+	public bool $required;
+	public string|Closure|bool $permission;
+	protected array $options;
+
+	public function __construct(string $name, string $label, string $type, callable|string|bool $permission)
+	{
+		$this->name = $name;
+		$this->label = $label;
+		$this->type = $type;
+		$this->permission = $permission;
+	}
+
+	public function checkPermission():bool
+	{
+		if (is_bool($this->permission)) {
+			return $this->permission;
+		}
+		if (is_string($this->permission)) {
+			return current_user_can($this->permission);
+		}
+		if (is_callable($this->permission)) {
+			return ($this->permission)();
+		}
+		error_log('Check Permission failed as no permission set. '.print_r([
+			'permission' => $this->permission,
+				'field'	=> $this->name,
+			], true));
+		return false;
+	}
+
+	public function setRequired():void
+	{
+		$this->required = true;
+	}
+	public function setOptions(array $options):void
+	{
+		$this->options = $options;
+	}
+
+	public function getConfig():array
+	{
+		$conf = [
+			'name'	=> $this->name,
+			'label'	=> $this->label,
+			'type'	=> $this->type,
+		];
+		if ($this->required) {
+			$conf['required'] = true;
+		}
+		if ($this->options) {
+			$conf['options'] = $this->options;
+		}
+		return $conf;
+	}
+}
diff --git a/inc/integrations/Integrations.php b/inc/integrations/Integrations.php
index 6f6232c..dd8ede2 100644
--- a/inc/integrations/Integrations.php
+++ b/inc/integrations/Integrations.php
@@ -3,21 +3,17 @@
 
 use Exception;
 use JVBase\managers\Cache;
+use JVBase\managers\ErrorHandler;
 use JVBase\managers\queue\executors\IntegrationExecutor;
 use JVBase\managers\queue\mergers\DefaultMerger;
 use JVBase\managers\queue\TypeConfig;
+use JVBase\managers\UploadManager;
 use JVBase\meta\Form;
 use JVBase\meta\Meta;
-use JVBase\managers\ErrorHandler;
 use JVBase\registrar\helpers\AddIntegrationFields;
 use JVBase\registrar\Registrar;
 use WP_Error;
 use WP_Post;
-use WP_Query;
-use WP_REST_Request;
-use WP_REST_Response;
-use WP_Term;
-use WP_User;
 
 if (!defined('ABSPATH')) {
 	exit;
@@ -34,85 +30,30 @@
  */
 abstract class Integrations
 {
+	use _Base;
+
 	//Flag to allow for custom settings (defaults, etc) for an integration in the dashboard
-	public static bool $hasExtraOptions = false;
+	public bool $hasExtraOptions = false;
 	public bool $hasBatchCreate = false;
 	public bool $hasBatchUpdate = false;
 	public bool $hasBatchDelete = false;
 	public bool $canCreateOnUpdate = false;
-	/**
-	 * Queue types
-	 * These types match with IntegrationExecutor
-	 */
-	protected static string $syncTo;
-	protected static string $deleteFrom;
-	protected static string $syncFrom;
-	protected static string $syncCustomer;
-	protected static string $import;
+
 	/**
 	 * API Configuration
 	 * These properties define how the integration connects to external services
 	 */
-	protected string $service_name;      // Unique identifier for this service (e.g., 'square', 'facebook')
 	protected string|array $apiBase = ''; // Base URL(s) for API endpoints. Array format: ['base' => '', 'auth' => '']
 	protected array $apiEndpoints = [];   // Valid endpoint paths for this service
-	protected string $apiVersion = '';     // API version string (e.g., 'v2', '2024-01-01')
 
 	protected int $refresh_interval = 0; //seconds before expiry to refresh tokens. 0 to disable
 
-	/**
-	 * OAuth Configuration
-	 * Set these properties if your service uses OAuth authentication
-	 */
-	protected bool $isOAuthService = false;
-	protected array $oauth = [
-		'authorize' => '',    // OAuth authorization endpoint
-		'token' => '',        // Token exchange endpoint
-		'revoke' => '',       // Token revocation endpoint (optional)
-		'scopes' => [],       // Required OAuth scopes
-		'redirect_uri' => '', // OAuth callback URL
-	];
-
-	/**
-	 * Display Properties
-	 * Used for UI rendering in admin interfaces
-	 */
-	public string $title;  // Human-readable service name (e.g., 'Google My Business')
-	public string $icon = '';   // Phosphoricons icon slug
 
 	/**
 	 * Credentials & State
 	 */
-	protected array $credentials = [];      // Service credentials (API keys, tokens, etc.)
-	protected ?int $userID = null;          // User context for user-specific integrations
-	private bool $token_refresh_attempted = false;  // Circuit breaker for token refresh
 
-	protected array $fields = []; 		// The fields to generate that become credentials
-	protected array $advanced = []; 	// The fields that are optional settings
-	protected array $defaults = [];		// Default overrides
 	protected string $defaultContent = 'post'; //Default integration content type, is none is set. MUST EXIST as array key in $this->>getContentTypes
-	protected array $instructions = []; // Each item in array becomes an item in the list of instructions
-
-	protected bool $supportsWebp = true;
-	/**
-	 * Client Management
-	 * For services that benefit from persistent connections
-	 */
-	protected ?object $client = null;               // Persistent client instance
-
-	protected array $actions = [
-		'save_credentials'	=> 'saveCredentials',
-		'clear_credentials'	=> 'deleteCredentials',
-		'clear_cache'		=> 'clearCache',
-		'test_connection'	=> 'testConnection',
-	];
-
-	protected array $buttons = [
-		'save_credentials' => 'Save Changes',
-		'clear_credentials' => 'Clear Credentials',
-		'clear_cache'=> 'Clear Cache',
-		'test_connection' => 'Test Connection',
-	];
 
 	protected array $allowedContent = [];
 
@@ -127,24 +68,6 @@
 		'minimal' => 60,       // 1 minute for frequently changing data
 		'none' => 0           // No caching for real-time data
 	];
-	protected int $ttl = 3600; // Default cache TTL in seconds
-
-	/**
-	 * Rate Limiting Configuration
-	 * Override these values based on service-specific limits
-	 */
-	protected array $rate_limits = [
-		'per_second' => 2,
-		'per_minute' => 30,
-		'per_hour' => 1000
-	];
-	protected array $oauth_rate_limits = [
-			'per_second' => 1,    // Max 1 OAuth request per second
-			'per_minute' => 10,   // Max 10 OAuth requests per minute
-			'per_hour' => 100,    // Max 100 OAuth requests per hour
-		];
-	protected array $request_history = [];
-	protected int $max_history_size = 100;
 
 	/**
 	 * Post Syncing Capabilities
@@ -163,74 +86,51 @@
 	 * Error Handling Configuration
 	 */
 	protected array $lastError = [];
-	protected int $maxRetries = 3;
 	protected array $retryDelays = [1, 2, 5]; // Exponential backoff in seconds
 
-	/**
-	 * Validation Rules
-	 * Define validation rules for credentials and data
-	 */
-	protected array $validationRules = [];
 
-	/**
-	 * Error tracking properties
-	 */
-	protected array $error_stats = [
-		'total_errors' => 0,
-		'consecutive_errors' => 0,
-		'last_success' => null,
-		'error_types' => []
-	];
-
-	protected ?ErrorHandler $errorHandler = null;
-	protected int $error_threshold = 5; // Consecutive errors before marking unhealthy
-	protected bool $is_healthy = true;
-	protected bool $handleWebhooks = false;
 
 	protected function __construct(?int $userID = null)
 	{
 		$this->cacheName = $this->cacheName ?: $this->service_name;
 		$this->userID = $userID;
-		$this->cache = Cache::for('integrations_' . $this->cacheName, $this->ttl);
-
-		// Load error stats from cache
-		$this->loadErrorStats();
+		$this->cache = Cache::for('integrations_' . $this->cacheName);
 
 		$this->getPostTypes();
 		$this->getTaxonomies();
 		$this->setContentTypes();
 		$this->registerHooks();
-		if ($this->isOAuthService) {
-			$this->actions['get_oauth_url'] = 'getOAuthUrlAction';
-			$this->actions['connect_oauth'] = 'handleOAuthConnect';
-            $this->actions['disconnect_oauth'] = 'handleOAuthDisconnect';
-			$this->actions['refresh_token'] = 'refreshOAuthToken';
-			$this->buttons['refresh_token'] = 'Refresh OAuth Connection';
-			$this->buttons['disconnect_oauth'] = 'Disconnect OAuth';
-		}
-		if ($this->handleWebhooks) {
-			$this->advanced['webhook_signature_key'] = [
-				'label' => 'Webhook Signature Key',
-				'type' => 'text',
-				'subtype' => 'password',
-				'placeholder' => 'Enter webhook signature key',
-				'hint' => 'Used to verify webhook authenticity (recommended)'
-			];
+
+		if (method_exists($this, 'setQueueTypes')) {
+			$this->setQueueTypes();
 		}
 
-		self::$syncTo = $this->service_name.'_sync_to';
-		self::$deleteFrom = $this->service_name.'_delete_from';
-		self::$syncFrom = $this->service_name.'_sync_from';
-		//TODO: What is the difference between sync_from and import?
-		self::$import = $this->service_name.'import_from';
+		$this->initializeRateLimiters();
+		if (method_exists($this, 'initializeActions')) {
+			$this->initializeActions();
+		}
+		if (method_exists($this, 'addOAuthActions')) {
+			$this->addOAuthActions();
+		}
 
 		add_filter('jvbShouldRenderMeta', [$this, 'checkRenderField'], 10, 4);
 	}
+		protected function initializeRateLimiters():void
+		{
+			$key = $this->service_name;
+			if (!is_null($this->userID)) {
+				$key .= '_'.$this->userID;
+			}
 
-	protected function addFilters():bool
-	{
-		return is_null($this->userID);
-	}
+			if ($this->hasRequests && !isset($this->requestLimiter) && !is_null($this->oauthLimiter)) {
+				$this->requestLimiter = new RateLimits($key);
+			}
+			if ($this->hasOAuth && !isset($this->oauthLimiter) && !is_null($this->oauthLimiter)) {
+				$key .= '_oauth';
+				$this->oauthLimiter = new RateLimits($key, ['s' => 1,'m' => 10, 'h' => 100]);
+			}
+		}
+
 
 	protected function setContentTypes():void
 	{
@@ -238,7 +138,6 @@
 	}
 	public function getContentTypes(string $type):array
 	{
-
 		return (array_key_exists($type, $this->contentTypes)) ? $this->contentTypes[$type] : $this->contentTypes;
 	}
 
@@ -256,7 +155,7 @@
 
 	public function handleOAuthConnect(): array
 	{
-		if (!$this->isOAuthService) {
+		if (!$this->hasOAuth) {
 			return ['success' => false, 'message' => 'Not an OAuth service'];
 		}
 
@@ -355,7 +254,7 @@
 //		if (!empty($this->oauth['redirect_uri'])) {
 //			return $this->oauth['redirect_uri'];
 //		}
-		if ($this->isOAuthService) {
+		if ($this->hasOAuth) {
 			return admin_url('admin-ajax.php?action=' . BASE . $this->service_name . '_oauth_callback');
 		}
 		// Changed from admin-ajax.php to REST endpoint
@@ -393,71 +292,6 @@
 		}
 	}
 
-	/**
-	 * @param string $action
-	 * @param mixed|null $data
-	 * @return array [
-	 * 		'success'	=> {bool},
-	 * 		'message'	=> {string} optional message,
-	 *		'data'		=> {mixed} optional data to return
-	 * ]
-	 */
-	public function processAction(string $action, mixed $data = null):array
-	{
-		if (!$this->checkCapabilities($action, $this->userID)) {
-			$this->logError('User cannot perform this action');
-			return [
-				'success'	=> false,
-				'message'	=> 'Insufficient permissions'
-			];
-		}
-
-		if (!array_key_exists($action, $this->actions)) {
-			$this->logError('Attempted action not set up', [
-				'action'	=> $action,
-				'stored'	=> $this->actions,
-				'service'	=> $this->service_name,
-				'method'	=> 'processAction'
-			]);
-			return [
-				'success'	=> false,
-				'message'	=> 'Invalid action'
-			];
-		}
-		if (!method_exists($this, $this->actions[$action])) {
-			$this->logError('Attempted method not set up', [
-				'attempted'	=> $this->actions['action'],
-				'service'	=> $this->service_name,
-				'method'	=> 'processAction'
-			]);
-			return [
-				'success'	=> false,
-				'message'	=> 'Action not set up'
-			];
-		}
-
-		$method = $this->actions[$action];
-		// Log the action for debugging
-		$this->logDebug("Processing action", [
-			'action' => $action,
-			'method' => $method,
-			'data' => $data
-		]);
-		if ($data !== null) {
-			$result = $this->$method($data);
-		} else {
-			$result =  $this->$method();
-		}
-		if (is_wp_error($result)) {
-			return [
-				'success' => false,
-				'message' => $result->get_error_message()
-			];
-		}
-
-		return is_array($result) ? $result : ['success' => true, 'data' => $result];
-
-	}
 
 	/*********************************************************************
 	 * ABSTRACT METHODS - MUST BE IMPLEMENTED BY CHILD CLASSES
@@ -475,22 +309,6 @@
 	 */
 	abstract protected function initialize(): void;
 
-	/**
-	 * Get request headers for API calls
-	 *
-	 * Return an associative array of headers required for API requests.
-	 * Common headers include Authorization, Content-Type, Accept, etc.
-	 *
-	 * Example:
-	 * return [
-	 *     'Authorization' => 'Bearer ' . $this->credentials['access_token'],
-	 *     'Content-Type' => 'application/json',
-	 *     'Accept' => 'application/json',
-	 * ];
-	 *
-	 * @return array Associative array of header names and values
-	 */
-	abstract protected function getRequestHeaders(): array;
 
 	/**
 	 * Render the connection settings form
@@ -513,7 +331,7 @@
 	 * OPTIONAL OVERRIDE METHODS - IMPLEMENT AS NEEDED
 	 *********************************************************************/
 	/**
-	 * Save credentials using CredentialsManager
+	 * Save credentials using Auth
 	 */
 	public function saveCredentials(array $credentials, bool $test = true):array
 	{
@@ -552,7 +370,7 @@
 
 
 			// Connection successful, save credentials
-			$stored = CredentialsManager::getInstance()->storeCredentials(
+			$stored = Auth::getInstance()->storeCredentials(
 				$this->service_name,
 				$credentials,
 				$this->userID
@@ -576,7 +394,7 @@
 
 		} catch (\Exception $e) {
 			// Revert to old credentials on any error
-			$old = CredentialsManager::getInstance()->getCredentials(
+			$old = Auth::getInstance()->getCredentials(
 				$this->service_name,
 				$this->userID
 			);
@@ -595,7 +413,7 @@
 	 */
 	public function deleteCredentials():array
 	{
-		$success = CredentialsManager::getInstance()->deleteCredentials($this->service_name, $this->userID);
+		$success = Auth::getInstance()->deleteCredentials($this->service_name, $this->userID);
 		return [
 			'success'	=> $success
 		];
@@ -759,285 +577,18 @@
 	 * API REQUEST METHODS
 	 *********************************************************************/
 
-	/**
-	 * Make an API request with automatic retry and error handling
-	 * @param string $method
-	 * @param string $endpoint
-	 * @param array $data
-	 * @param string|null $baseKey array key for the $this->apiEndpoints
-	 * @param array $options
-	 * @return WP_Error|array
-	 */
-	protected function makeRequest(
-		string $method,
-		string $endpoint,
-		array  $data = [],
-		?string $baseKey = null,
-		array  $options = []
-	): array|WP_Error
-	{
-		if (!$this->is_healthy) {
-			return new WP_Error('unhealthy', 'Connection marked unhealthy. Skipping fetch');
-		}
-		$this->ensureInitialized();
-		if (!$this->isSetUp()){
-			$this->logError('Connection not setup for '.$this->service_name, [
-				'method' => 'makeRequest',
-			]);
-			return new WP_Error('Error', 'Connection is not set up to make request');
-		}
-
-		// Rate limiting check
-		if (!$this->checkRateLimit()) {
-			return new WP_Error('rate_limit', 'Rate limit exceeded. Please try again later.');
-		}
-
-
-		$attempt = 0;
-		$lastError = null;
-
-		while ($attempt < $this->maxRetries) {
-			try {
-//				$this->logDebug('[Integrations] Making request to '.$this->service_name);
-				$result = $this->executeRequest($method, $endpoint, $data, $baseKey, $options);
-				if (!$result) {
-					return new WP_Error('Request Error');
-				}
-				$this->recordRequest($method, $endpoint);
-//				$this->logDebug('[Integrations]', [
-//					'response' => $result
-//				]);
-				// Reset error stats on success
-				$this->resetErrorStats();
-				return $result;
-
-			} catch (Exception $e) {
-				$lastError = $e;
-				$this->recordRequest($method, $endpoint);
-				$attempt++;
-
-				// Retry with backoff for server errors
-				if ($attempt < $this->maxRetries && !$this->isClientError($e)) {
-					$delay = pow(2, $attempt) * 1000000; // 2^attempt seconds in microseconds
-					$jitter = rand(0, $delay * 0.3); // Add 0-30% jitter
-					usleep($delay + $jitter);
-				} else {
-					break;
-				}
-
-			}
-		}
-
-		$this->logError('API request failed after retries', [
-			'method' => $method,
-			'endpoint' => $endpoint,
-			'attempts' => $attempt,
-			'error' => $lastError ? $lastError->getMessage() : 'Unknown error'
-		]);
-
-		return new WP_Error('api_error', $lastError ? $lastError->getMessage() : 'API request failed');
-	}
-
-	/**
-	 * Execute the actual request
-	 */
-	protected function executeRequest(
-		string $method,
-		string $endpoint,
-		array  $data = [],
-		?string $baseKey = null,
-		array  $options = []
-	): ?array
-	{
-		$args = $this->buildRequestArgs($method, $data, $options);
-		$url = $this->getApiUrl($endpoint, $baseKey);
-		if (!$url) {
-			return null;
-		}
-
-//		$this->logDebug("$method request to: $url: ".print_r($args, true));
-
-		// Make the request
-		$response = match($method) {
-			'GET' => wp_remote_get($url, $args),
-			'POST' => wp_remote_post($url, $args),
-			'PUT', 'PATCH', 'DELETE' => wp_remote_request($url, array_merge($args, ['method' => $method])),
-			default => null
-		};
-
-		if (!$response) {
-			$this->logError("Unsupported HTTP method $method for $this->service_name");
-			return null;
-		}
-
-		if (is_wp_error($response)) {
-			$this->logError("Error for: $this->service_name in executeRequest", ['message' => $response->get_error_message()]);
-			return null;
-		}
-
-		$response_code = wp_remote_retrieve_response_code($response);
-		$body = wp_remote_retrieve_body($response);
-
-		// Handle 401 - try to refresh token and retry once
-		if ($response_code === 401 && $this->isOAuthService && !empty($this->credentials['refresh_token'])) {
-			// Avoid infinite retry loop - only retry once
-			static $retry_count = 0;
-
-			if ($retry_count === 0) {
-				$retry_count++;
-
-//				$this->logDebug('Got 401, attempting token refresh...');
-				if ($this->refreshOAuthToken()) {
-//					$this->logDebug('Token refreshed successfully, retrying request...');
-
-					// Rebuild request args with new token
-					$args = $this->buildRequestArgs($method, $data, $options);
-
-					// Retry the request
-					$response = match($method) {
-						'GET' => wp_remote_get($url, $args),
-						'POST' => wp_remote_post($url, $args),
-						'PUT', 'PATCH', 'DELETE' => wp_remote_request($url, array_merge($args, ['method' => $method])),
-						default => null
-					};
-
-					if ($response && !is_wp_error($response)) {
-						$response_code = wp_remote_retrieve_response_code($response);
-						$body = wp_remote_retrieve_body($response);
-					}
-				}
-				$retry_count = 0; // Reset for next request
-			}
-		}
-
-		if ($response_code >= 400) {
-			$this->handleApiError($response_code, $body, $endpoint);
-		}
-
-		$decoded = json_decode($body, true);
-		if (json_last_error() !== JSON_ERROR_NONE) {
-			return ['raw_response' => $body];
-		}
-
-		$this->updateLastTestedTime();
-
-		return $decoded;
-	}
-
-	/**
-	 * GET request with caching
-	 */
-	protected function getRequest(
-		string $endpoint,
-		array  $params = [],
-		?string $baseKey = null,
-		string $cacheStrategy = 'moderate',
-		bool   $force = false
-	): ?array
-	{
-		$cacheKey = $this->buildCacheKey('GET', $endpoint, $params, $baseKey);
-
-		$ttl = is_int($cacheStrategy)
-			? max(0, $cacheStrategy)
-			: ($this->cacheStrategy[$cacheStrategy] ?? $this->ttl);
-
-		if (!$force && $ttl > 0) {
-			$cached = $this->cache->get($cacheKey);
-			if ($cached !== false) {
-
-//				$this->logDebug("Cache hit for: $cacheKey");
-				return $cached;
-			}
-		}
-
-		$result = $this->makeRequest('GET', $endpoint, $params, $baseKey);
-
-		// Only cache successful responses (not WP_Error and not error objects)
-		if ($result && !is_wp_error($result) && !$this->isErrorResponse($result) && $ttl > 0) {
-			$this->cache->set($cacheKey, $result, $ttl);
-		}
-
-		return $result;
-	}
-	/**
-	 * Check if response contains an error
-	 * Override in child classes for service-specific error detection
-	 */
-	protected function isErrorResponse(array $response): bool
-	{
-		// Common error patterns across APIs
-		return isset($response['error'])
-			|| isset($response['errors'])
-			|| isset($response['error_description']);
-	}
-
-	/**
-	 * POST request
-	 */
-	protected function postRequest(string $endpoint, array $data = [], ?string $baseKey = null): ?array
-	{
-		return $this->makeRequest('POST', $endpoint, $data, $baseKey);
-	}
-
-	/**
-	 * PUT request
-	 */
-	protected function putRequest(string $endpoint, array $data = [], ?string $baseKey = null): ?array
-	{
-		return $this->makeRequest('PUT', $endpoint, $data, $baseKey);
-	}
-
-	/**
-	 * DELETE request
-	 */
-	protected function deleteRequest(string $endpoint, array $data = [], ?string $baseKey = null): ?array
-	{
-		return $this->makeRequest('DELETE', $endpoint, $data, $baseKey);
-	}
-
-	/**
-	 * PATCH request
-	 */
-	protected function patchRequest(string $endpoint, array $data = [], ?string $baseKey = null): WP_Error|array
-	{
-		return $this->makeRequest('PATCH', $endpoint, $data, $baseKey);
-	}
 
 
 
-	/**
-	 * Build request arguments
-	 */
-	protected function buildRequestArgs(string $method, array $data, array $options): array
-	{
-		$args = [
-			'method' => $method,
-			'headers' => $this->getRequestHeaders(),
-			'timeout' => $options['timeout'] ?? 30,
-			'sslverify' => $options['sslverify'] ?? true,
-			'redirection' => $options['redirection'] ?? 5,
-			'blocking' => $options['blocking'] ?? true,
-			'compress' => $options['compress'] ?? true,
-			'decompress' => $options['decompress'] ?? true,
-		];
 
-		if ($method === 'GET' && !empty($data)) {
-			// Add query parameters for GET requests
-			$args['body'] = $data;
-		} elseif (!empty($data)) {
-			$content_type = $args['headers']['Content-Type'] ?? 'application/json';
 
-			if (str_contains($content_type, 'application/json')) {
-				$args['body'] = json_encode($data);
-			} elseif (str_contains($content_type, 'application/x-www-form-urlencoded')) {
-				$args['body'] = http_build_query($data);
-			} else {
-				$args['body'] = $data;
-			}
-		}
 
-		return $args;
-	}
+
+
+
+
+
+
 
 	/*********************************************************************
 	 * SYNC METHODS
@@ -1101,114 +652,17 @@
 	 *********************************************************************/
 
 	/**
-	 * Get cache duration based on endpoint
-	 *
-	 * @param string $endpoint The API endpoint
-	 * @return int Cache duration in seconds
-	 */
-	protected function getCacheDuration(string $endpoint): int
-	{
-		// Override in child classes for endpoint-specific caching
-		// Example: user profiles = aggressive, posts = moderate, analytics = minimal
-		return $this->ttl;
-	}
-
-	/**
-	 * Check if we're within rate limits
-	 *
-	 * @return bool True if within limits
-	 */
-	protected function checkRateLimit(): bool
-	{
-		$now = time();
-
-		// Clean old history
-		$this->request_history = array_filter($this->request_history, function($timestamp) use ($now) {
-			return ($now - $timestamp) < 3600; // Keep last hour
-		});
-
-		// Trim to max size
-		if (count($this->request_history) > $this->max_history_size) {
-			$this->request_history = array_slice($this->request_history, -$this->max_history_size);
-		}
-
-		// Check limits
-		$recent_counts = [
-			'second' => 0,
-			'minute' => 0,
-			'hour' => count($this->request_history)
-		];
-
-		foreach ($this->request_history as $timestamp) {
-			if ($now - $timestamp <= 1) $recent_counts['second']++;
-			if ($now - $timestamp <= 60) $recent_counts['minute']++;
-		}
-
-		if ($recent_counts['second'] >= $this->rate_limits['per_second']) return false;
-		if ($recent_counts['minute'] >= $this->rate_limits['per_minute']) return false;
-		if ($recent_counts['hour'] >= $this->rate_limits['per_hour']) return false;
-
-		return true;
-	}
-
-	protected function checkOAuthRateLimit(): bool
-	{
-		$cache_key = 'oauth_rate_limit_' . $this->service_name . '_' . $this->userID;
-		$attempts = get_transient($cache_key) ?: [];
-
-		$now = time();
-
-		// Clean old attempts
-		$attempts = array_filter($attempts, function($timestamp) use ($now) {
-			return ($now - $timestamp) < 3600; // Keep last hour
-		});
-
-		// Check per-minute limit
-		$recent = array_filter($attempts, function($timestamp) use ($now) {
-			return ($now - $timestamp) < 60;
-		});
-
-		if (count($recent) >= 5) { // Max 5 OAuth attempts per minute
-			return false;
-		}
-
-		// Add current attempt
-		$attempts[] = $now;
-
-		// Save updated attempts
-		set_transient($cache_key, $attempts, 3600);
-
-		return true;
-	}
-
-
-	/**
-	 * Record a request for rate limiting
-	 *
-	 * @param string $method HTTP method
-	 * @param string $endpoint API endpoint
-	 * @return void
-	 */
-	protected function recordRequest(string $method, string $endpoint): void
-	{
-		$this->request_history[] = time();
-	}
-
-
-
-	/**
 	 * Register WordPress hooks based on capabilities
 	 */
 	protected function registerHooks(): void
 	{
 		//Handled by IntegrationExecutor now
 //		add_filter(BASE . 'handle_bulk_operation', [$this, 'processOperation'], 10, 3);
-		if (is_null($this->userID)) {
-			add_action( 'wp_ajax_'.BASE.$this->service_name.'_oauth_callback', [$this, 'handleAjaxResponse'] );
-			add_action( 'wp_ajax_nopriv_'.BASE.$this->service_name.'_oauth_callback', [$this, 'handleAjaxResponse'] );
-		}
 
-		if ($this->handleWebhooks && is_null($this->userID)){
+		if (method_exists($this, 'registerOAuthCallbacks')) {
+			$this->registerOAuthCallbacks();
+		}
+		if (method_exists($this, 'registerWebhookEndpoint')) {
 			$this->registerWebhookEndpoint();
 		}
 		// Let child classes register additional hooks if needed
@@ -1280,62 +734,7 @@
 			{
 				//Empty. Integration extensions can register additional operation types from here.
 			}
-	public function handleAjaxResponse()
-	{
 
-		$code = $_GET['code'];
-		$state =  $_GET['state'];
-
-
-		$state_parts = explode('|', $state);
-		$state_key = $state_parts[0] ?? '';
-		$user_id = intval($state_parts[1] ?? 0);
-		$user_id = ($user_id === 0) ? null : $user_id;
-		$return_url = isset($state_parts[2]) ? base64_decode($state_parts[2]) : admin_url('admin.php?page=jvb-integrations');
-
-		$state_data = get_transient('oauth_state_' . $state_key);
-		if (!$state_data || $state_data['service'] !== $this->service_name) {
-			wp_die('Invalid state parameter', 'OAuth Error');
-		}
-
-		// Delete the transient to prevent reuse
-		delete_transient('oauth_state_' . $state_key);
-		// Handle error from OAuth provider
-		if (array_key_exists('error', $_GET)) {
-			$error_description = $_GET['error_description'] ?? 'Authorization denied';
-
-			wp_redirect(add_query_arg([
-				'error' => 'OAuth authorization denied: ' . $error_description
-			], $return_url));
-			exit;
-		}
-
-		// Get integration instance
-		$integration = JVB()->connect($this->service_name, $user_id);
-
-		if (!$integration) {
-			wp_die('Invalid service: ' . esc_html($this->service_name), 'OAuth Error');
-		}
-
-
-		// Exchange code for tokens
-		$result = $integration->handleOAuthCode($code, $state);
-
-		// Redirect back with result
-		if ($result['success']) {
-			wp_redirect(add_query_arg([
-				'success' => 'Successfully connected to ' . $integration->title
-			], $return_url));
-		} else {
-			// Handle failure
-			$error_message = $result['message'] ?? 'Failed to complete OAuth authorization';
-
-			wp_redirect(add_query_arg([
-				'error' => $error_message
-			], $return_url));
-		}
-		exit;
-	}
 	/**
 	 * Handle connection setup and credential storage
 	 *
@@ -1393,45 +792,19 @@
 		}
 	}
 
-	/**
-	 * Load credentials from secure storage
-	 */
-	protected function loadCredentials(): void
-	{
-		$this->credentials = CredentialsManager::getInstance()->getCredentials($this->service_name, $this->userID);
-	}
-
 	public function getCredentials(): array
 	{
-		$this->ensureInitialized();
-		return $this->credentials;
+		return $this->loadCredentials();
 	}
 
-	/**
-	 * Check if integration is properly configured
-	 */
-	public function isSetUp(): bool
+
+
+	public function hasOAuthCredentials(?int $userID = null): bool
 	{
-		if (empty($this->credentials)) {
-			$this->loadCredentials();
+		if ($userID !== $this->userID) {
+			$this->switchUser($userID);
 		}
-		return !empty($this->credentials);
-	}
-
-	public function hasOAuthCredentials(): bool
-	{
-		if (!$this->isOAuthService){
-			return false;
-		}
-
-		$required = ['client_id', 'client_secret', 'access_token'];
-
-		foreach ($required as $field) {
-			if (empty($this->credentials[$field]) || !is_string($this->credentials[$field]) || $this->credentials[$field] === '') {
-				return false;
-			}
-		}
-		return true;
+		return $this->hasOAuth && $this->hasValidOAuth();
 	}
 
 
@@ -1443,42 +816,7 @@
 		if (!$this->isSetUp()){
 			return;
 		}
-		if (empty($this->credentials)) {
-			$this->loadCredentials();
-		}
-
-		if (!empty($this->credentials)) {
-			if ($this->isOAuthService && $this->hasOAuthCredentials()) {
-				// Check if token is expired first
-				if (!$this->isOAuthValid()) {
-					// Only attempt refresh once per request
-					if (!$this->token_refresh_attempted) {
-						$this->token_refresh_attempted = true;
-//						$this->logDebug('OAuth token expired, attempting refresh');
-
-						if (!$this->refreshOAuthToken()) {
-							$this->logError('Failed to refresh expired OAuth token - stopping execution');
-							// Token refresh failed - DO NOT continue making API requests
-							return;
-						}
-					} else {
-						// Already attempted refresh in this request
-//						$this->logDebug('Token refresh already attempted, skipping');
-						return;
-					}
-				}
-				// Check if we should proactively refresh (before expiry)
-				elseif ($this->shouldRefreshToken() && !$this->token_refresh_attempted) {
-					$this->token_refresh_attempted = true;
-//					$this->logDebug('OAuth token should be refreshed proactively');
-					if (!$this->refreshOAuthToken()) {
-						$this->logError('Failed to proactively refresh OAuth token');
-						// Not critical - token is still valid, so continue
-					}
-				}
-			}
-			$this->initialize();
-		}
+		$this->initialize();
 	}
 
 
@@ -1526,9 +864,6 @@
 		// Update error statistics
 		$this->updateErrorStats($code, $endpoint);
 
-		// Check if integration should be marked unhealthy
-		$this->checkIntegrationHealth();
-
 		// Store last error for debugging
 		$this->lastError = [
 			'code' => $code,
@@ -1539,57 +874,10 @@
 		];
 	}
 
-	/**
-	 * Check if error is a client error (4xx)
-	 */
-	protected function isClientError(Exception $e): bool
-	{
-		$code = $e->getCode();
-		return $code >= 400 && $code < 500;
-	}
-
-	/**
-	 * Get last error details
-	 */
-	public function getLastError(): array
-	{
-		return $this->lastError;
-	}
-
 
 	/*****************************************************************
 		OAUTH
 	 *****************************************************************/
-	/**
-	 * Check user capabilities for specific actions
-	 * Can be overridden by child classes for custom logic
-	 */
-	protected function checkCapabilities(string $action, ?int $userID = null): bool
-	{
-		// Default: require manage_options for admin actions
-		$admin_actions = ['disconnect', 'update_credentials', 'clear_cache'];
-
-		if (in_array($action, $admin_actions)) {
-			return current_user_can('manage_options');
-		}
-
-		// User-specific actions
-		return is_user_logged_in();
-	}
-
-	/*****************************************************************
-		OAUTH
-	 *****************************************************************/
-	protected function initializeOAuth():void
-	{
-		$this->oauth = array_merge([
-			'authorize'	=> '',
-			'token'		=> '',
-			'revoke'	=> '',
-			'scopes'	=> [],
-			'redirect_uri' => admin_url('admin-ajax.php?action=' . $this->service_name . '_oauth_callback')
-		], $this->oauth);
-	}
 
 	/**
 	 * Get OAuth authorization URL
@@ -1597,7 +885,7 @@
 	public function getOAuthUrl(?string $return_url = null): string
 	{
 
-		if (!$this->isOAuthService) {
+		if (!$this->hasOAuth) {
 			return '';
 		}
 
@@ -1663,119 +951,7 @@
 		return $params;
 	}
 
-	protected function makeOAuthRequest(string $method, string $url, array $data = []): array|WP_Error
-	{
-		// Check rate limits before making request
-		if (!$this->checkOAuthRateLimit()) {
-			// For OAuth, we might want to be more lenient with rate limits
-			// since these are critical authentication flows
-			$this->logError('OAuth request rate limited, waiting before retry');
-			sleep(2); // Brief pause before critical OAuth request
 
-			// Check again after wait
-			if (!$this->checkRateLimit()) {
-				return new WP_Error(
-					'rate_limit',
-					'OAuth rate limit exceeded. Please try again in a few moments.'
-				);
-			}
-		}
-
-		// Record the request for rate limiting
-		$this->recordRequest($method, $url);
-
-		// Build OAuth-specific headers
-		$headers = [
-			'Content-Type' => 'application/json',
-			'Accept' => 'application/json',
-		];
-
-		// Add API version for services that require it (like Square)
-		if (!empty($this->apiVersion)) {
-			$headers['Square-Version'] = $this->apiVersion;
-		}
-
-		// Build request arguments
-		$args = [
-			'method' => $method,
-			'headers' => $headers,
-			'timeout' => 30,
-			'sslverify' => true,
-			'body' => json_encode($data)
-		];
-
-		// Make the request with retry logic
-		$attempt = 0;
-		$max_attempts = 2; // Fewer retries for OAuth to avoid code expiry
-
-		while ($attempt < $max_attempts) {
-			$attempt++;
-
-			// Make the actual request
-			$response = wp_remote_request($url, $args);
-
-			// Check for WordPress errors
-			if (is_wp_error($response)) {
-				if ($attempt >= $max_attempts) {
-					return $response;
-				}
-
-				// Wait before retry
-				sleep($this->retryDelays[$attempt - 1] ?? 1);
-				continue;
-			}
-
-			// Get response code and body
-			$code = wp_remote_retrieve_response_code($response);
-			$body = wp_remote_retrieve_body($response);
-
-			// Parse JSON response
-			$data = json_decode($body, true);
-
-			// Check for successful response
-			if ($code >= 200 && $code < 300) {
-				// Reset error stats on success
-				$this->resetErrorStats();
-
-				// Return parsed data
-				return $data ?: [];
-			}
-
-			// Handle specific OAuth error codes
-			if ($code === 401) {
-				// Invalid credentials - don't retry
-				$this->handleApiError($code, $body, $url);
-				return new WP_Error('invalid_credentials', 'Invalid OAuth credentials');
-			}
-
-			if ($code === 429) {
-				// Rate limited - wait longer before retry
-				if ($attempt < $max_attempts) {
-					$retry_after = $this->extractRetryAfter($response);
-					sleep($retry_after ?: 5);
-					continue;
-				}
-			}
-
-			// Check if we should retry
-			if ($attempt >= $max_attempts || $code >= 400 && $code < 500) {
-				// Client errors (4xx) typically shouldn't be retried
-				$this->handleApiError($code, $body, $url);
-
-				$error_message = $data['error_description'] ??
-					$data['error'] ??
-					"OAuth request failed with status {$code}";
-
-				return new WP_Error('oauth_error', $error_message, ['code' => $code]);
-			}
-
-			// Server error - retry after delay
-			sleep($this->retryDelays[$attempt - 1] ?? 1);
-		}
-
-		// Shouldn't reach here, but handle it
-		return new WP_Error('oauth_error', 'OAuth request failed after retries');
-	}
 
 	/**
 	 * Extract retry-after header from response
@@ -1870,7 +1046,7 @@
 	 */
 	protected function shouldRefreshToken(): bool
 	{
-		if (!$this->isOAuthService || $this->refresh_interval === 0) {
+		if (!$this->hasOAuth || $this->refresh_interval === 0) {
 			return false;
 		}
 
@@ -1908,7 +1084,7 @@
 	 */
 	public function getTokenStatus(): string
 	{
-		if (!$this->isOAuthService) {
+		if (!$this->hasOAuth) {
 			return 'not_oauth';
 		}
 
@@ -1938,7 +1114,7 @@
 	 */
 	protected function refreshOAuthToken(): bool
 	{
-		if (!$this->isOAuthService || empty($this->credentials['refresh_token'])) {
+		if (!$this->hasOAuth || empty($this->credentials['refresh_token'])) {
 			return false;
 		}
 
@@ -1992,7 +1168,7 @@
 	 */
 	public function revokeOAuthAccess(): bool
 	{
-		if (!$this->isOAuthService || empty($this->oauth['revoke'])) {
+		if (!$this->hasOAuth || empty($this->oauth['revoke'])) {
 			return false;
 		}
 
@@ -2002,7 +1178,7 @@
 			]);
 		}
 
-		return CredentialsManager::getInstance()->deleteCredentials($this->service_name, $this->userID);
+		return Auth::getInstance()->deleteCredentials($this->service_name, $this->userID);
 
 	}
 
@@ -2322,7 +1498,7 @@
 	 */
 	protected function getApiUrl(string $endpoint, ?string $baseKey = null): string|false
 	{
-		if ($this->isOAuthService && in_array($endpoint, $this->oauth)) {
+		if ($this->hasOAuth && in_array($endpoint, $this->oauth)) {
 			return $endpoint;
 		}
 		if (is_array($this->apiBase)) {
@@ -2390,65 +1566,7 @@
 		return false;
 	}
 
-	/**
-	 * Build cache key
-	 */
-	protected function buildCacheKey(string $method, string $endpoint, array $params = []): string
-	{
-		$key_parts = [
-			$this->service_name,
-			$method,
-			$endpoint
-		];
 
-		if ($this->userID) {
-			$key_parts[] = "user_{$this->userID}";
-		}
-
-		if (!empty($params)) {
-			$key_parts[] = md5(serialize($params));
-		}
-
-		return implode('_', $key_parts);
-	}
-
-	/**
-	 * Log error message
-	 */
-	protected function logError(string $message, array $context = [], ?string $severity = null): void
-	{
-		if (!$this->errorHandler) {
-			$this->errorHandler = JVB()->error();
-		}
-		// Determine severity if not provided
-		if ($severity === null) {
-			$severity = 'error';
-		}
-
-		// Add integration-specific context
-		$context = array_merge($context, [
-			'component' => 'Integration:' . $this->service_name,
-			'user_id' => $this->userID,
-			'service' => $this->service_name,
-			'timestamp' => current_time('mysql'),
-			'memory_usage' => memory_get_usage(true),
-			'peak_memory' => memory_get_peak_usage(true),
-			'integration_health' => [
-				'is_healthy' => $this->is_healthy,
-				'consecutive_errors' => $this->error_stats['consecutive_errors'],
-				'total_errors' => $this->error_stats['total_errors'],
-				'last_success' => $this->error_stats['last_success']
-			]
-		]);
-
-		// Log through ErrorHandler
-		$this->errorHandler->log(
-			'Integration:' . $this->service_name,
-			$message,
-			$context,
-			$severity
-		);
-	}
 
 	/**
 	 * Categorize API errors for better tracking
@@ -2549,100 +1667,8 @@
 		$this->saveErrorStats();
 	}
 
-	/**
-	 * Reset error statistics on successful request
-	 */
-	protected function resetErrorStats(): void
-	{
-		$this->error_stats['consecutive_errors'] = 0;
-		$this->error_stats['last_success'] = time();
-		$this->is_healthy = true;
 
-		$this->saveErrorStats();
-	}
 
-	/**
-	 * Get integration health status
-	 */
-	public function getHealthStatus(): array
-	{
-		return [
-			'is_healthy' => $this->is_healthy,
-			'consecutive_errors' => $this->error_stats['consecutive_errors'],
-			'total_errors' => $this->error_stats['total_errors'],
-			'last_success' => $this->error_stats['last_success'],
-			'error_types' => $this->error_stats['error_types'],
-			'threshold' => $this->error_threshold
-		];
-	}
-
-	/**
-	 * Manually reset integration health
-	 */
-	public function resetHealth(): void
-	{
-		$this->error_stats = [
-			'total_errors' => 0,
-			'consecutive_errors' => 0,
-			'last_success' => time(),
-			'error_types' => []
-		];
-		$this->is_healthy = true;
-		$this->saveErrorStats();
-	}
-
-	/**
-	 * Check integration health based on error patterns
-	 */
-	protected function checkIntegrationHealth(): void
-	{
-		// Mark unhealthy if too many consecutive errors
-		if ($this->error_stats['consecutive_errors'] >= $this->error_threshold) {
-			$this->is_healthy = false;
-
-			// Log critical health issue
-			$this->logError(
-				"Integration marked unhealthy after {$this->error_stats['consecutive_errors']} consecutive errors",
-				[
-					'error_stats' => $this->error_stats,
-					'threshold' => $this->error_threshold
-				],
-				'critical'
-			);
-		}
-	}
-
-	/**
-	 * Load error statistics from cache
-	 */
-	protected function loadErrorStats(): void
-	{
-		$cache_key = "error_stats_{$this->service_name}_{$this->userID}";
-		$cached_stats = $this->cache->get($cache_key);
-
-		if ($cached_stats) {
-			$this->error_stats = array_merge($this->error_stats, $cached_stats);
-
-			// Check if integration was marked unhealthy
-			if ($this->error_stats['consecutive_errors'] >= $this->error_threshold) {
-				$this->is_healthy = false;
-			}
-		}
-	}
-
-	/**
-	 * Save error statistics to cache
-	 */
-	protected function saveErrorStats(): void
-	{
-		$cache_key = "error_stats_{$this->service_name}_{$this->userID}";
-		$this->cache->set($cache_key, $this->error_stats, 86400); // 24 hours
-	}
-
-	protected function logDebug(string $message, array $context = []): void
-	{
-		error_log('[Integration: '.$this->service_name.']'.$message.': '.print_r($context, true));
-	}
 
 	/**
 	 * Get service name
@@ -2698,69 +1724,6 @@
 		return "Manage your {$this->getServiceName()} integration settings.";
 	}
 
-	/**
-	 * Get service key for consistency
-	 */
-	public function getServiceKey(): string
-	{
-		return strtolower($this->service_name);
-	}
-
-
-	/**
-	 * Enhanced webhook handling with common patterns
-	 */
-	public function handleWebhook(array $payload): bool
-	{
-		// Log incoming webhook for debugging
-		$this->logDebug('Webhook received', [
-			'payload_keys' => array_keys($payload),
-			'timestamp' => time()
-		]);
-
-		// Validate webhook signature/authenticity
-		if (!$this->validateWebhook($payload)) {
-			$this->logError('Webhook validation failed', [
-				'webhook_type' => 'validation_error',
-				'payload_sample' => array_slice($payload, 0, 5) // Log partial payload for debugging
-			], 'warning');
-			return false;
-		}
-
-		// Check for duplicate processing (idempotency)
-		if ($this->isWebhookProcessed($payload)) {
-			return true; // Return true to prevent retries
-		}
-
-		try {
-			// Process the webhook
-			$result = $this->processWebhook($payload);
-
-			// Mark as processed if successful
-			if ($result) {
-				$this->markWebhookProcessed($payload);
-				$this->resetErrorStats();
-			} else {
-				$this->logError('Webhook processing returned false', [
-					'webhook_id' => $this->extractWebhookId($payload)
-				], 'warning');
-			}
-
-			return $result;
-
-		} catch (Exception $e) {
-			$this->logError('Webhook processing failed', [
-				'error' => $e->getMessage(),
-				'webhook_id' => $this->extractWebhookId($payload),
-				'trace' => $e->getTraceAsString()
-			], 'critical');
-
-			// Update error stats
-			$this->updateErrorStats($e->getCode() ?: 500, 'webhook');
-			$this->checkIntegrationHealth();
-			return false;
-		}
-	}
 
 	/**
 	 * Validate webhook signature
@@ -2782,110 +1745,14 @@
 		return hash_equals($expected, $signature);
 	}
 
-	protected function validateWebhook(array $payload):bool
-	{
-		//Handled by child classes
-		return false;
-	}
-	protected function processWebhook(array $payload):bool
-	{
-		//Handled by child classes
-		return false;
-	}
 
-	/**
-	 * Check if webhook was already processed (prevent duplicates)
-	 */
-	protected function isWebhookProcessed(array $payload): bool
-	{
-		$webhook_id = $this->extractWebhookId($payload);
-		if (!$webhook_id) {
-			return false;
-		}
 
-		$cache_key = "webhook_processed_{$this->service_name}_{$webhook_id}";
-		return $this->cache->get($cache_key) !== null;
-	}
 
-	/**
-	 * Mark webhook as processed
-	 */
-	protected function markWebhookProcessed(array $payload): void
-	{
-		$webhook_id = $this->extractWebhookId($payload);
-		if ($webhook_id) {
-			$cache_key = "webhook_processed_{$this->service_name}_{$webhook_id}";
-			$this->cache->set($cache_key, time(), 86400); // Store for 24 hours
-		}
-	}
 
-	/**
-	 * Extract unique webhook ID (override in child classes)
-	 */
-	protected function extractWebhookId(array $payload): ?string
-	{
-		// Common patterns - child classes can override
-		return $payload['id'] ??
-			$payload['event_id'] ??
-			$payload['webhook_id'] ??
-			md5(serialize($payload));
-	}
-
-	/**
-	 * Register webhook endpoint
-	 */
-	protected function registerWebhookEndpoint(): void
-	{
-		// Register REST API endpoint for webhooks
-		add_action('rest_api_init', function() {
-			register_rest_route('jvb/v1', '/webhooks/' . $this->service_name, [
-				'methods' => 'POST',
-				'callback' => [$this, 'handleWebhookRequest'],
-				'permission_callback' => '__return_true', // Webhooks come from external services
-			]);
-		});
-	}
-
-	/**
-	 * Handle webhook REST API request
-	 */
-	public function handleWebhookRequest(WP_REST_Request $request): WP_REST_Response
-	{
-		$payload = $request->get_params();
-		$headers = $request->get_headers();
-
-		// Include headers in payload for signature validation
-		$payload['_headers'] = $headers;
-
-		$success = $this->handleWebhook($payload);
-
-		return new WP_REST_Response([
-			'success' => $success
-		], $success ? 200 : 400);
-	}
-
-	protected function renderWebhookUrl(): string
-	{
-		if (!$this->handleWebhooks || !$this->isSetUp()) {
-			return '';
-		}
-
-		$webhook_url = rest_url('jvb/v1/webhooks/' . $this->service_name);
-
-		return sprintf(
-			'<div class="webhook-info">
-            <h4>Webhook URL</h4>
-            <code id="webhook-url-%s">%s</code>
-            <p class="hint">Add this URL to your %s webhook settings</p>
-        </div>',
-			esc_attr($this->service_name),
-			esc_html($webhook_url),
-			esc_html($this->title)
-		);
-	}
 
 	public function renderConnection(bool $return = false):string
 	{
+
 		if ($this->userID && !JVB()->userCanConnect($this->service_name, $this->userID)) {
 			return '';
 		}
@@ -2970,7 +1837,7 @@
 						echo Form::render($name, '', $config);
 					}
 				}
-				if ($this->handleWebhooks) {
+				if ($this->hasWebhooks) {
 					echo $this->renderWebhookUrl();
 				}
 				?>
@@ -3108,26 +1975,14 @@
 
 	}
 
-	/**
-	 * Get last tested timestamp - extends your existing pattern
-	 */
-	protected function getLastTestedTime(): ?int
-	{
-		$key = $this->userID
-			? "jvb_integration_{$this->service_name}_last_test_user_{$this->userID}"
-			: "jvb_integration_{$this->service_name}_last_test";
-		return get_option($key, null);
-	}
 
 	/**
 	 * Update last tested time
 	 */
 	public function updateLastTestedTime(): void
 	{
-		$key = $this->userID
-			? "jvb_integration_{$this->service_name}_last_test_user_{$this->userID}"
-			: "jvb_integration_{$this->service_name}_last_test";
-		update_option($key, time());
+		$cred = Auth::getInstance();
+		$cred->updateTested($this->service_name, $this->userID);
 	}
 
 	public function handleAdminPost(): void
@@ -3321,155 +2176,16 @@
 		if ($jpegVersion !== '' && is_int($jpegVersion)) {
 			return $jpegVersion;
 		}
-
-		$converted = $this->convertWebpToJpeg($imgID);
-		if ($converted) {
-			update_post_meta($imgID, BASE . 'jpeg_version', $converted);
-			return $converted;
+		$uploader = new UploadManager();
+		$converted = $uploader->convertImageTo($imgID, 'jpeg', 80, false);
+		if ($converted && !is_wp_error($converted)) {
+			update_post_meta($imgID, BASE . 'jpeg_version', $converted['attachment_id']);
+			return $converted['attachment_id'];
 		}
 
 		return $imgID;
 	}
 
-	/**
-	 * Convert WebP image to JPEG
-	 *
-	 * @param int $webpAttachmentId
-	 * @return int|false Image ID on success, false on failure.
-	 */
-	protected function convertWebpToJpeg(int $webpAttachmentId):int|false
-	{
-		try {
-			$webpPath = get_attached_file($webpAttachmentId);
-
-			if (!file_exists($webpPath)) {
-				throw new Exception('WebP file not found');
-			}
-
-			// Generate JPEG filename
-			$pathInfo = pathinfo($webpPath);
-			$jpegPath = $pathInfo['dirname'] . '/' . $pathInfo['filename'] . '_jpeg.jpg';
-
-			// Check if already exists (shouldn't happen due to meta check, but safety first)
-			if (file_exists($jpegPath)) {
-				// Try to find existing attachment
-				global $wpdb;
-				$existingId = $wpdb->get_var($wpdb->prepare(
-					"SELECT ID FROM {$wpdb->posts} WHERE guid = %s",
-					str_replace(ABSPATH, site_url('/'), $jpegPath)
-				));
-
-				if ($existingId) {
-					return (int)$existingId;
-				}
-			}
-
-			// Perform conversion based on available libraries
-			if (extension_loaded('imagick')) {
-				$this->convertWebpToJpegImagick($webpPath, $jpegPath);
-			} else {
-				$this->convertWebpToJpegGd($webpPath, $jpegPath);
-			}
-
-			// Create attachment for the JPEG
-			$attachmentData = [
-				'post_mime_type' => 'image/jpeg',
-				'post_title' => get_the_title($webpAttachmentId) . ' (JPEG)',
-				'post_content' => '',
-				'post_status' => 'inherit'
-			];
-
-			$jpegAttachmentId = wp_insert_attachment($attachmentData, $jpegPath);
-
-			if (is_wp_error($jpegAttachmentId)) {
-				throw new Exception('Failed to create JPEG attachment: ' . $jpegAttachmentId->get_error_message());
-			}
-
-			// Generate metadata
-			require_once(ABSPATH . 'wp-admin/includes/image.php');
-			$attachData = wp_generate_attachment_metadata($jpegAttachmentId, $jpegPath);
-			wp_update_attachment_metadata($jpegAttachmentId, $attachData);
-
-			// Copy alt text and other meta
-			$altText = get_post_meta($webpAttachmentId, '_wp_attachment_image_alt', true);
-			if ($altText) {
-				update_post_meta($jpegAttachmentId, '_wp_attachment_image_alt', $altText);
-			}
-
-			return $jpegAttachmentId;
-
-		} catch (Exception $e) {
-			$this->logError('Image conversion failed...', [
-				'method'	=> 'convertWebpToJpeg'
-			]);
-			return false;
-		}
-	}
-
-	/**
-	* Convert WebP to JPEG using Imagick
-	*/
-	protected function convertWebpToJpegImagick(string $source, string $destination): void
-	{
-		$image = new \Imagick($source);
-		$image->setImageFormat('jpeg');
-
-		// Set quality (using 85 for good balance of quality/size)
-		$image->setImageCompressionQuality(85);
-
-		// Remove alpha channel and set white background
-		$image->setImageBackgroundColor('white');
-		$image->setImageAlphaChannel(\Imagick::ALPHACHANNEL_REMOVE);
-		$image->mergeImageLayers(\Imagick::LAYERMETHOD_FLATTEN);
-
-		$image->writeImage($destination);
-		$image->clear();
-	}
-
-	/**
-	 * Convert WebP to JPEG using GD
-	 */
-	protected function convertWebpToJpegGd(string $source, string $destination): void
-	{
-		$image = imagecreatefromwebp($source);
-
-		if ($image === false) {
-			throw new Exception('Failed to create image from WebP source');
-		}
-
-		// Get dimensions
-		$width = imagesx($image);
-		$height = imagesy($image);
-
-		// Create new true color image
-		$jpegImage = imagecreatetruecolor($width, $height);
-
-		// Set white background (for transparency)
-		$white = imagecolorallocate($jpegImage, 255, 255, 255);
-		imagefill($jpegImage, 0, 0, $white);
-
-		// Copy WebP image to JPEG
-		imagecopy($jpegImage, $image, 0, 0, 0, 0, $width, $height);
-
-		// Save as JPEG (quality 85)
-		$result = imagejpeg($jpegImage, $destination, 85);
-
-		// Clean up
-		imagedestroy($image);
-		imagedestroy($jpegImage);
-
-		if (!$result) {
-			throw new Exception('Failed to save JPEG image');
-		}
-	}
-	/**
-	 * Reset token refresh attempt flag
-	 * Called automatically when switching users
-	 */
-	protected function resetTokenRefreshFlag(): void
-	{
-		$this->token_refresh_attempted = false;
-	}
 
 	public function getAllowedContent():array
 	{
@@ -3490,15 +2206,7 @@
 		return $this->icon;
 	}
 
-	public function getServiceItemID(int $itemID, string $type = 'post'):string
-	{
-		return match($type) {
-			'post'	=> Meta::forPost($itemID)->get("_{$this->service_name}_item_id"),
-			'term','taxonomy'	=> Meta::forTerm($itemID)->get("_{$this->service_name}_item_id"),
-			'user'	=> Meta::forUser($itemID)->get("_{$this->service_name}_item_id"),
-			default => ''
-		};
-	}
+
 
 	public function canBatchUpdate():bool
 	{
@@ -3512,468 +2220,4 @@
 	{
 		return $this->hasBatchDelete;
 	}
-
-	protected function updateItemStatus(int|array $items, string $status = 'pending', string $type = 'post'):void
-	{
-		if (!is_array($items)) {
-			$items = [$items];
-		}
-		foreach ($items as $ID) {
-			match ($type) {
-				'post'	=> Meta::forPost($ID)->set('_'.$this->service_name.'_sync_status', $status),
-				'term'	=> Meta::forTerm($ID)->set('_'.$this->service_name.'_sync_status', $status),
-				'user'	=> Meta::forUser($ID)->set('_'.$this->service_name.'_sync_status', $status),
-				default => false
-			};
-		}
-	}
-
-	public function updateBatchToService(array $data):array
-	{
-		$type = $data['type']??'post';
-
-		$newlyCreated = [];
-		if (!$this->canCreateOnUpdate) {
-			$created = array_filter($data['items'], function($ID) use ($type) {
-				return !empty($this->getServiceItemID($ID, $type));
-			});
-
-			//Test to see if we have any that haven't been created yet.
-			//For some services, items may have to be created before they can be updated
-			if (count($created) !== count($data['items'])) {
-				$notCreated = array_filter($data['items'], function($ID) use ($type) {
-					return empty($this->getServiceItemID($ID, $type));
-				});
-				$newData = $data;
-				$newData['items'] = $notCreated;
-				$newlyCreated = $this->createBatchToService($newData);
-			}
-
-			// If we don't have any that are created, just send the noUpdatedItems response, with any newly created items added
-			if (empty($created)) {
-				return $this->noUpdatedItems($newlyCreated);
-			}
-			$data['items'] = $created;
-		}
-
-
-		$items = $this->formatItems($data['items'], $type);
-
-		if (empty($items)) {
-			return $this->noUpdatedItems($newlyCreated);
-		}
-
-		if ($this->hasBatchUpdate) {
-			$response = $this->sendBatchUpdate($items);
-			if (!is_wp_error($response)) {
-				$result = $this->processBatchUpdateResponse($data, $response);
-			} else {
-				$this->logError('Batch update failed',[
-					'method'	=> 'updateBatchToService',
-					'item_ids'	=> $data['items'],
-					'error'		=> $response
-				]);
-				$this->updateItemStatus($data['items'], 'error');
-
-				$result = [
-					'outcome'	=> 'failed_permanent',
-					'result'	=> 'Could not update items'
-				];
-			}
-		} else {
-			$success = $errors = [];
-			//Does not have batch update, manually update each one
-			foreach ($items as $item) {
-				$itemResult = $this->updateOne($item);
-				if (!is_wp_error($itemResult)) {
-					$success[] = $itemResult;
-				} else {
-					$errors[] = $itemResult;
-				}
-			}
-
-			$result = [
-				'outcome'	=> empty($errors) ? 'success' : (empty($success) ? 'failed' : 'partial'),
-				'result'	=> [
-					'success'	=> $success,
-					'errors'	=> $errors
-				]
-			];
-		}
-
-		return array_merge($result, $newlyCreated);
-	}
-
-	/**
-	 * To be implemented by integration extension. Updates a single item
-	 * @param array $item
-	 * @return array|WP_Error
-	 */
-		protected function updateOne(array $item):array|WP_Error
-		{
-			return new WP_Error('invalid', 'Integration '.$this->service_name.' must implement its own updateOne method');
-		}
-
-	/**
-	 * Overridden by child classes
-	 * @param array $items
-	 * @return array|WP_Error
-	 */
-		protected function sendBatchUpdate(array $items):array|WP_Error
-		{
-			return new WP_Error('invalid', 'Integration '.$this->service_name.' must implement its own sendBatchUpdate');
-		}
-	/**
-	 * Overridden by child classes
-	 * @param array $items
-	 * @return array|WP_Error
-	 */
-		protected function sendBatchCreate(array $items):array|WP_Error
-		{
-			return new WP_Error('invalid', 'Integration '.$this->service_name.' must implement its own sendBatchCreate');
-		}
-
-	/**
-	 * To be implemented by extensions
-	 * @param array $data
-	 * @param array $response
-	 * @return array
-	 */
-		protected function processBatchUpdateResponse(array $data, array $response):array
-		{
-			return [
-				'outcome'	=> 'failed',
-				'result'	=> [
-					'message'	=> $this->service_name.' should implement processBatchUpdateResponse.'
-				]
-			];
-		}
-	/**
-	 * To be implemented by extensions
-	 * @param array $data
-	 * @param array $response
-	 * @return array
-	 */
-		protected function processBatchCreateResponse(array $data, array $response):array
-		{
-			return [
-				'outcome'	=> 'failed',
-				'result'	=> [
-					'message'	=> $this->service_name.' should implement processBatchCreateResponse.'
-				]
-			];
-		}
-
-	/**
-	 * Defaults to an array of formatted items. Can be overridden in child classes
-	 * @param array $itemIDs
-	 * @param string $type
-	 * @return array
-	 */
-		protected function formatItems(array $itemIDs, string $type):array
-		{
-			$items = [];
-			foreach ($itemIDs as $ID) {
-				try {
-					$items[] = $this->formatForService($ID, $type);
-				} catch (Exception $e){
-					$this->logError('Could not format Item for service',[
-						'itemID' => $ID,
-						'type'	=> $type,
-						'message'	=> $e->getMessage(),
-					]);
-				}
-			}
-			return $items;
-		}
-
-	/**
-	 * Must be defined according to how each service needs it to be
-	 * @param int $itemID
-	 * @param string $type
-	 * @return array
-	 * @throws Exception
-	 */
-		protected function formatForService(int $itemID, string $type = 'post'):array
-		{
-			throw new Exception('formatForService must be implemented by child class');
-		}
-		protected function noUpdatedItems(array $created):array
-		{
-			$result =  [
-				'outcome'	=> 'success',
-				'result'	=> [
-					'message'	=> 'No items to update',
-					'updated'	=> [],
-					'errors'	=> [],
-				]
-			];
-
-			if (!empty($created)) {
-				error_log('Result before: '.print_r($result, true));
-				$result = array_merge($result, $created);
-				error_log('Result after merge: '.print_r($result, true));
-			}
-			return $result;
-		}
-
-		protected function noCreatedItems(array $updated):array
-		{
-			$result = [
-				'outcome'	=> 'success',
-				'result'	=> [
-					'message'	=> 'No items to create',
-					'created'	=> [],
-					'errors'	=> [],
-				]
-			];
-			if (!empty($updated)) {
-				$result = array_merge($result, $updated);
-			}
-			return $result;
-		}
-
-		public function createBatchToService($data):array
-		{
-			$type = $data['type']??'post';
-
-			//Check if any of the submitted ids are already created
-			$created = array_filter($data['items'],
-				function($ID) use ($type) {
-					return !empty($this->getServiceItemID($ID, $type));
-				});
-
-			$updated = [];
-			if (!empty($created)) {
-				$updateData = $data;
-				$updateData['items'] = $created;
-				$updated = $this->updateBatchToService($data);
-
-				//remove any updated items from the original items to process
-				$data['items'] = array_filter($data['items'], function ($ID) use ($created) {
-					return !in_array($ID, $created);
-				});
-			}
-
-			$items = $this->formatItems($data['items'], $type);
-			if (empty($items)) {
-				return $this->noCreatedItems($updated);
-			}
-
-			$response = [
-				'outcome'	=> 'failed',
-				'result'	=> [
-					'message'	=> 'No result'
-				]
-			];
-			if ($this->hasBatchCreate) {
-				$response = $this->sendBatchCreate($items);
-				if (!is_wp_error($response)) {
-					$result = $this->processBatchCreateResponse($data, $response);
-				} else {
-					$this->logError('Batch create failed',[
-						'method'	=> 'createBatchToService',
-						'item_ids'	=> $data['items'],
-						'error'		=> $response
-					]);
-					$this->updateItemStatus($data['items'], 'error');
-
-					$result = [
-						'outcome'	=> 'failed_permanent',
-						'result'	=> 'Could not update items'
-					];
-				}
-			} else {
-				$success = $errors = [];
-				foreach ($items as $item) {
-					$itemResult = $this->createOne($item);
-					if (!is_wp_error($itemResult)) {
-						$success[] = $itemResult;
-					} else {
-						$errors[] = $itemResult;
-					}
-				}
-				$result = [
-					'outcome'	=> empty($errors) ? 'success' : (empty($success) ? 'failed' : 'partial'),
-					'result'	=> [
-						'success'	=> $success,
-						'errors'	=> $errors
-					]
-				];
-			}
-
-			return array_merge($result, $updated);
-		}
-
-		/**
-		 * To be implemented by integration extension. Updates a single item
-		 * @param array $item
-		 * @return array|WP_Error
-		 */
-		protected function createOne(array $item):array|WP_Error
-		{
-			return new WP_Error('invalid', 'Integration '.$this->service_name.' must implement its own createOne');
-		}
-
-	public function findWPItem(string $integrationID, ?string $type = null):array|false
-	{
-		if (empty($integrationID)) {
-			error_log('Integrations::findWPItem No Integration ID received.');
-			return false;
-		}
-		$types = ['post', 'term', 'user'];
-		if (!is_null($type) && !in_array($type, $types)) {
-			error_log('Integrations::findWPItem invalid type sent: '.$type.'; defaulting to checking all');
-			$type = null;
-		}
-		if (!is_null($type)) {
-			return $this->queryWPType($integrationID, $type);
-		} else {
-			foreach ($types as $type) {
-				$check = $this->queryWPType($integrationID, $type);
-				if ($check) {
-					return $check;
-				}
-			}
-		}
-		return false;
-	}
-
-	/**
-	 * @param string $integrationID
-	 * @param string|null $type
-	 * @return array|false if success, an array of $ID and $type
-	 */
-		public function queryWPType(string $integrationID, ?string $type = null):array|false
-		{
-			if (!in_array($type, ['post', 'term', 'user'])) {
-				return $this->findWPItem($integrationID);
-			}
-			return match($type) {
-				'post'	=> $this->findWPPost($integrationID),
-				'term'	=> $this->findWPTerm($integrationID),
-				'user'	=> $this->findWPUser($integrationID),
-				default => false
-			};
-		}
-		public function findWPPost(string $integrationID):array|false
-		{
-			if (empty($integrationID)) {
-				return false;
-			}
-			$get = new WP_Query([
-				'meta_query'	=> [
-					'key'	=> BASE."_{$this->service_name}_item_id",
-					'value'	=> $integrationID,
-				],
-				'fields'	=> 'ids',
-				'posts_per_page' => 1,
-			]);
-			return $get->have_posts() && !empty($get->posts) ? [$get->posts[0], 'post'] : false;
-		}
-		public function findWPTerm(string $integrationID):array|false
-		{
-			if (empty($integrationID)) {
-				return false;
-			}
-			$get = get_terms([
-				'meta_query'	=> [
-					'key'	=> BASE."_{$this->service_name}_item_id",
-					'value'	=> $integrationID,
-				],
-				'fields'	=> 'ids',
-				'hide_empty'=> true,
-				'number'	=> 1
-			]);
-			return is_wp_error($get) || empty($get) ? false : [$get[0], 'term'];
-		}
-		public function findWPUser(string $integrationID):array|false
-		{
-			if (empty($integrationID)) {
-				return false;
-			}
-			$get = get_users([
-				'meta_query'	=> [
-					'key'	=> BASE."_{$this->service_name}_item_id",
-					'value'	=> $integrationID,
-				],
-				'fields'	=> 'ids',
-				'number'	=> 1,
-			]);
-			return is_wp_error($get) || empty($get) ? false : [$get[0], 'user'];
-		}
-
-	/*********************************************************************************
-	 * Syncing WP items from Service
-	*********************************************************************************/
-	/**
-	 * @param array $itemData
-	 * @return bool
-	 * @throws Exception
-	 */
-	protected function createItemFromService(array $itemData):bool
-	{
-		throw new Exception('createItemFromService must be implemented by child class '.$this->service_name);
-	}
-
-	/**
-	 * @param array $itemData
-	 * @return bool
-	 * @throws Exception
-	 */
-	protected function updateItemFromService(array $itemData):bool
-	{
-		throw new Exception('updateItemFromService must be implemented by child class '.$this->service_name);
-	}
-
-	/**
-	 * @param array $itemData
-	 * @return bool
-	 * @throws Exception
-	 */
-	protected function deleteItemFromService(array $itemData):bool
-	{
-		throw new Exception('deleteItemFromService must be implemented by child class '.$this->service_name);
-	}
-
-	/****************************************************************
-	 * CONNECT to a user's integration
-	****************************************************************/
-	protected array $connections = [];
-	public function connectAs(?int $userID = null):bool
-	{
-		if (!$this->userCanConnect($userID)) {
-			return false;
-		}
-		$this->cleanup();
-		$this->userID = $userID;
-		$this->ensureInitialized();
-		return true;
-	}
-
-	public function userCanConnect(?int $userID):bool
-	{
-		$user = get_userdata($userID);
-		if (!$user || is_wp_error($user)) {
-			return false;
-		}
-		if (current_user_can('manage_options')) {
-			return true;
-		}
-		$role = jvbUserRole($userID);
-		$registrar = Registrar::getInstance($role);
-		if (!$registrar || !$registrar->hasIntegration($this->service_name)) {
-			return false;
-		}
-
-		return (bool) Meta::forUser($userID)->get('integration_'.$this->service_name.'_connected');
-	}
-
-	protected function cleanup():void
-	{
-		$this->userID = false;
-		$this->credentials = [];
-		$this->request_history = [];
-		$this->resetTokenRefreshFlag();
-	}
 }
diff --git a/inc/integrations/OAuth.php b/inc/integrations/OAuth.php
new file mode 100644
index 0000000..5f8085f
--- /dev/null
+++ b/inc/integrations/OAuth.php
@@ -0,0 +1,351 @@
+<?php
+namespace JVBase\integrations;
+
+use JetBrains\PhpStorm\NoReturn;
+use WP_Error;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+trait OAuth
+{
+	use _Base, Admin;
+	protected bool $hasOAuth = true;
+	/**
+	 * Child classes must set this (e.g. 'https://api.example.com/v1/')
+	 */
+	protected ?string $baseOAuthUrl = null;
+	protected ?string $oauthRedirectUrl = null;
+	protected int $oauthTTL = 300;
+	protected array $oathScopes = [];
+
+
+	abstract protected function getOAuthRequestHeaders(): array;
+	abstract protected function handleOAuthResponse(array|WP_Error $response, string $method, string $endpoint): array;
+	abstract protected function refreshAccessToken(OAuthCredentials $credentials): array;
+
+	function addOAuthActions():void
+	{
+		$actions = [
+			[
+				'name'		=> 'Connect to Service',
+				'label'		=> 'Connect',
+				'icon'		=> 'plugs-connected',
+				'action'	=> [$this, 'handleOAuthConnect']
+			],
+			[
+				'name'		=> 'Disconnect from Service',
+				'icon'		=> 'plugs-disconnected',
+				'action'	=> [$this, 'handleOAuthDisconnect']
+			],
+			[
+				'name'		=> 'Refresh Token',
+				'icon'		=> 'arrows-clockwise',
+				'action'	=> [$this, 'handleOAuthRefreshToken']
+			]
+		];
+		foreach ($actions as $a) {
+			$this->addAction($a['name'], $a['action'], $a['slug']??null, $a['icon']??null, $a['label']??null);
+		}
+	}
+
+	protected function registerOAuthCallbacks():void
+	{
+		add_action( 'wp_ajax_'.BASE.$this->service_name.'_oauth_callback', [$this, 'handleAjaxResponse'] );
+		add_action( 'wp_ajax_nopriv_'.BASE.$this->service_name.'_oauth_callback', [$this, 'handleAjaxResponse'] );
+	}
+
+	protected function getOAuthRequest(string $endpoint, array $data = [], bool $force = false, ?int $ttl = null): array
+	{
+		$key = [$this->service_name, $this->userID, $endpoint, $data];
+		if (!$force) {
+			$cached = $this->cache->get($key);
+			if ($cached) {
+				return $cached;
+			}
+		}
+		$response = $this->makeOAuthRequest('GET', $endpoint, $data);
+		if ($response && !is_wp_error($response)) {
+			$ttl = is_int($ttl) ? $ttl : $this->oauthTTL;
+			$this->cache->set($key, $response, $ttl);
+		}
+		return $response;
+	}
+
+	protected function postOAuthRequest(string $endpoint, array $data = []): array
+	{
+		return $this->makeOAuthRequest('POST', $endpoint, $data);
+	}
+
+	protected function putOAuthRequest(string $endpoint, array $data = []): array
+	{
+		return $this->makeOAuthRequest('PUT', $endpoint, $data);
+	}
+
+	protected function deleteOAuthRequest(string $endpoint, array $data = []): array
+	{
+		return $this->makeOAuthRequest('DELETE', $endpoint, $data);
+	}
+
+	protected function makeOAuthRequest(string $method, string $endpoint, array $data = []): array
+	{
+		if (is_null($this->baseOAuthUrl)) {
+			error_log('Can not make request. No $baseOAuthUrl set for '.$this->service_name);
+			return [];
+		}
+		if ($this->oauthLimiter && !$this->awaitOAuthRateLimit($endpoint)) {
+			$error = new WP_Error('rate_limit_exceeded', "Rate limit exceeded for {$endpoint}");
+			return $this->handleOAuthResponse($error, $method, $endpoint);
+		}
+
+		$credentials = $this->ensureFreshToken();
+		if (!$credentials->isValid()) {
+			$error = new WP_Error('invalid_credentials', "Missing or invalid OAuth credentials for {$this->service_name}");
+			return $this->handleOAuthResponse($error, $method, $endpoint);
+		}
+
+		if (!empty($this->requiredScopes) && !$credentials->hasScopes($this->requiredScopes)) {
+			$error = new WP_Error('insufficient_scope', "Missing required scopes for {$this->service_name}");
+			return $this->handleOAuthResponse($error, $method, $endpoint);
+		}
+
+		$url = rtrim($this->baseOAuthUrl, '/') . '/' . ltrim($endpoint, '/');
+
+		$args = [
+			'method'	=> $method,
+			'headers'	=> $this->getOAuthRequestHeaders(),
+			'timeout'	=> 15,
+		];
+
+		if (!empty($data)) {
+			if (in_array($method, ['GET', 'DELETE'], true)) {
+				$url = add_query_arg($data, $url);
+			} else {
+				$args['body'] = wp_json_encode($data);
+			}
+		}
+
+		$response = wp_remote_request($url, $args);
+
+		$this->oauthLimiter?->recordRequest($endpoint);
+
+		return $this->handleOAuthResponse($response, $method, $endpoint);
+	}
+
+	/**
+	 * Checks stored credentials, refreshes via refreshAccessToken() if expired,
+	 * and persists the refreshed credentials back to Auth
+	 */
+	protected function ensureFreshToken(): OAuthCredentials
+	{
+		$auth = Auth::getInstance();
+		$credentials = new OAuthCredentials($auth->getCredentials($this->service_name, $this->userID));
+
+		if (!$credentials->isValid() || !$credentials->isExpired()) {
+			return $credentials;
+		}
+
+		$refreshed = new OAuthCredentials($this->refreshAccessToken($credentials));
+		if ($refreshed->isValid()) {
+			$auth->storeCredentials($this->service_name, $refreshed->toArray(), $this->userID);
+			return $refreshed;
+		}
+
+		return $credentials;
+	}
+
+	public function hasValidOAuth():bool
+	{
+		$auth = Auth::getInstance();
+		$credentials = new OAuthCredentials($auth->getCredentials($this->service_name, $this->userID));
+		return $credentials->isValid();
+	}
+
+	protected function awaitOAuthRateLimit(string $endpoint, int $maxAttempts = 3): bool
+	{
+		$attempts = 0;
+
+		if (is_null($this->oauthLimiter)) {
+			return false;
+		}
+
+		while (!$this->oauthLimiter->checkRateLimit($endpoint)) {
+			$attempts++;
+			if ($attempts >= $maxAttempts) {
+				return false;
+			}
+			usleep(random_int(250_000, 750_000));
+		}
+
+		return true;
+	}
+
+	/***************************************************************
+	 * ACTIONS CALLBACKS
+	***************************************************************/
+	#[NoReturn] public function handleAjaxResponse():void
+	{
+
+		$code = $_GET['code'];
+		$state =  $_GET['state'];
+
+
+		$state_parts = explode('|', $state);
+		$state_key = $state_parts[0] ?? '';
+		$user_id = intval($state_parts[1] ?? 0);
+		$user_id = ($user_id === 0) ? null : $user_id;
+		$return_url = isset($state_parts[2]) ? base64_decode($state_parts[2]) : admin_url('admin.php?page=jvb-integrations');
+
+		$state_data = get_transient('oauth_state_' . $state_key);
+		if (!$state_data || $state_data['service'] !== $this->service_name) {
+			wp_die('Invalid state parameter', 'OAuth Error');
+		}
+
+		// Delete the transient to prevent reuse
+		delete_transient('oauth_state_' . $state_key);
+		// Handle error from OAuth provider
+		if (array_key_exists('error', $_GET)) {
+			$error_description = $_GET['error_description'] ?? 'Authorization denied';
+
+			wp_redirect(add_query_arg([
+				'error' => 'OAuth authorization denied: ' . $error_description
+			], $return_url));
+			exit;
+		}
+
+		// Get integration instance
+		$integration = JVB()->connect($this->service_name, $user_id);
+
+		if (!$integration) {
+			wp_die('Invalid service: ' . esc_html($this->service_name), 'OAuth Error');
+		}
+
+
+		// Exchange code for tokens
+		$result = $integration->handleOAuthCode($code, $state);
+
+		// Redirect back with result
+		if ($result['success']) {
+			wp_redirect(add_query_arg([
+				'success' => 'Successfully connected to ' . $integration->title
+			], $return_url));
+		} else {
+			// Handle failure
+			$error_message = $result['message'] ?? 'Failed to complete OAuth authorization';
+
+			wp_redirect(add_query_arg([
+				'error' => $error_message
+			], $return_url));
+		}
+		exit;
+	}
+
+	protected function renderOAuthConnectionForm():string
+	{
+		$credentials = $this->loadCredentials();
+		$hasCredentials = $this->hasValidOAuth();
+		$returnURL = $this->getReturnURL();
+
+		if ($hasCredentials) {
+			return sprintf(
+			'<div class="oauth-connect">
+				<h3>Connect to your %s account</h3>
+				<a href="%s" class="btn jvb-oauth-connect" data-service="%s">%s Authorize Connection</a>',
+				$this->title,
+				$this->getOAuthURL($returnURL),
+				esc_attr($this->service_name),
+				$this->outputIcon($this->icon)
+			);
+		}
+		$form = sprintf(
+			'<details open>
+			<summary>Connected Account
+				<div class="connection-status connected">
+					<span class="status-indicator">●</span>
+					<span>Connected</span>
+				</div>
+			</summary>
+			<span class="label">OAuth Redirect URL:</span>
+			<code>%s</code>',
+			$this->getRedirectUri()
+		);
+		if (!empty($credentials['updated_at'])) {
+			$form .= sprintf(
+				'<div class="oauth-meta"><small>Token expires: %s</small></div>',
+				array_key_exists('expires_at', $credentials) ?
+						human_time_diff($credentials['expires_at']) :
+						'Never'
+			);
+		}
+		//
+		if (method_exists($this, 'renderOAuthConnectedOptions')) {
+			$form .= $this->renderOAuthConnectedOptions();
+		}
+		$form .= '</details';
+		return $form;
+	}
+
+	/**
+	 * @return string Outputs additional information after the basic OAuth connection
+	 */
+	protected function renderOAuthConnectedOptions():string
+	{
+		return '';
+	}
+
+	public function getOAuthURL(?string $return_url = null):string
+	{
+		if (!$this->hasOAuth) {
+			return '';
+		}
+		$credentials = $this->loadCredentials();
+		if (empty($credentials['authorize'])) {
+			$this->logError('getOAuthUrl', 'OAuth authorize URL not configured');
+			return '';
+		}
+		$params = [
+			'client_id'		=> $credentials['client_id']??$credentials['application_id']??$credentials['app_id']??'',
+			'redirect_url'	=> $this->getRedirectUri(),
+			'response_type'	=> 'code',
+			'scope'			=> implode(' ', $credentials['scopes']??[])
+		];
+		$state_key = wp_generate_password(32, false);
+		$user_id = $this->userID??0;
+
+		//Store state data in transient, expiring in 10 minutes
+		set_transient(
+			BASE.'oauth_state_'.$state_key,
+			[
+				'service'	=> $this->service_name,
+				'user_id'	=> $user_id,
+				'created'	=> time()
+			],
+			600
+		);
+
+		$state_parts = [
+			$state_key,
+			$user_id
+		];
+
+		$state_parts[] = base64_encode(is_null($return_url) ? $this->getReturnURL() : $return_url);
+
+		$params['state'] = implode('|', $state_parts);
+
+		//Allow extensions to modify params
+		if (method_exists($this, 'addOAuthParams')) {
+			$params = $this->addOAuthParams($params);
+		}
+
+		return $credentials['authorize'] . '?' . http_build_query($params);
+	}
+
+	protected function getRedirectUri():string
+	{
+		if ($this->hasOAuth) {
+			return admin_url('admin-ajax.php?action='.BASE . $this->service_name . '_oauth_callback');
+		}
+		return rest_url('jvb/v1/oauth/callback?service=' . $this->service_name);
+	}
+}
diff --git a/inc/integrations/OAuthCredentials.php b/inc/integrations/OAuthCredentials.php
new file mode 100644
index 0000000..4664ab8
--- /dev/null
+++ b/inc/integrations/OAuthCredentials.php
@@ -0,0 +1,47 @@
+<?php
+namespace JVBase\integrations;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+class OAuthCredentials
+{
+	public string $access_token;
+	public int $expires_at;
+	public ?string $refresh_token;
+	public array $scopes;
+
+	public function __construct(array $data)
+	{
+		$this->access_token = $data['access_token'] ?? '';
+		$this->expires_at = (int)($data['expires_at'] ?? 0);
+		$this->refresh_token = $data['refresh_token'] ?? null;
+		$this->scopes = $data['scopes'] ?? [];
+	}
+
+	public function isValid(): bool
+	{
+		return !empty($this->access_token) && $this->expires_at > 0;
+	}
+
+	public function isExpired(int $buffer = 60): bool
+	{
+		return $this->expires_at <= (time() + $buffer);
+	}
+
+	public function hasScopes(array $required): bool
+	{
+		return empty(array_diff($required, $this->scopes));
+	}
+
+	public function toArray(): array
+	{
+		return [
+			'access_token'	=> $this->access_token,
+			'expires_at'	=> $this->expires_at,
+			'refresh_token'	=> $this->refresh_token,
+			'scopes'		=> $this->scopes,
+		];
+	}
+}
diff --git a/inc/integrations/RateLimits.php b/inc/integrations/RateLimits.php
new file mode 100644
index 0000000..9f470e5
--- /dev/null
+++ b/inc/integrations/RateLimits.php
@@ -0,0 +1,103 @@
+<?php
+namespace JVBase\integrations;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+class RateLimits {
+	protected array $history = [];
+	protected int $maxHistory = 100;
+	protected array $limits = [
+		'per_second'	=> 2,
+		'per_minute'	=> 30,
+		'per_hour'		=> 1000
+	];
+
+	protected string $key;
+
+	public function __construct(string $key, array $limits = []) {
+		$this->key = sanitize_title($key);
+		//Allow custom limits
+		foreach ($limits as $interval => $limit) {
+			$key = match($interval) {
+				'second', 'per_second','s' 	=> 'per_second',
+				'minute', 'per_minute', 'm' => 'per_minute',
+				'hour', 'per_hour', 'h'		=> 'per_hour',
+				default => false
+			};
+			if (!$key) {
+				error_log('Attempted to add custom limit: '.$interval.' for key '.$key);
+				continue;
+			}
+			$this->limits[$key] = (int)$limit;
+		}
+
+		$this->getHistory();
+		add_action('shutdown', [$this, 'saveHistory']);
+	}
+		protected function getHistory():void
+		{
+			$stored = get_transient(BASE.$this->key.'_limits');
+			if (!$stored) {
+				$stored = [];
+			}
+			$this->history = $stored;
+		}
+		public function recordRequest(string $endpoint = 'base'):void
+		{
+			if (!array_key_exists($endpoint, $this->history)) {
+				$this->history[$endpoint] = [];
+			}
+
+			$this->history[$endpoint][] = time();
+			$this->checkHistorySize();
+		}
+		public function saveHistory():void
+		{
+			set_transient(BASE.$this->key.'_limits', $this->history);
+		}
+		protected function checkHistorySize():void
+		{
+			foreach ($this->history as $endpoint => $history) {
+				if (count($history) > $this->maxHistory) {
+					$this->history[$endpoint] = array_slice($this->history[$endpoint], -$this->maxHistory);
+				}
+			}
+		}
+
+	public function checkRateLimit(string $endpoint = 'base'):bool
+	{
+		if (!array_key_exists($endpoint, $this->history)) {
+			return true;
+		}
+
+		$now = time();
+
+		$this->cleanHistory($now);
+		$counts = [
+			'per_second'	=> 0,
+			'per_minute'	=> 0,
+			'per_hour'		=> count($this->history)
+		];
+
+		foreach ($this->history[$endpoint]??[] as $timestamp) {
+			if ($now - $timestamp <=1) $counts['per_second']++;
+			if ($now - $timestamp <=60) $counts['per_minute']++;
+		}
+
+		foreach ($this->limits as $interval => $limit) {
+			if ($counts[$interval] >= $limit) return false;
+		}
+		return true;
+	}
+
+	protected function cleanHistory($now):void
+	{
+		$this->history = array_map(function ($endpointHistory) use ($now) {
+			return array_filter($endpointHistory, function($timestamp) use ($now) {
+				return ($now - $timestamp) < 3600; //Keep last hour
+			});
+		}, $this->history);
+	}
+}
diff --git a/inc/integrations/Requests.php b/inc/integrations/Requests.php
new file mode 100644
index 0000000..ad9ebfc
--- /dev/null
+++ b/inc/integrations/Requests.php
@@ -0,0 +1,114 @@
+<?php
+namespace JVBase\integrations;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+trait Requests
+{
+	protected bool $hasRequests = true;
+	/**
+	 * Child classes must set this (e.g. 'https://api.example.com/v1/')
+	 */
+	protected ?string $baseUrl = null;
+	protected int $ttl = 300; //5 minutes default
+
+	/**
+	 * Child classes define their auth headers (Bearer token, API key, etc.)
+	 */
+	abstract protected function getRequestHeaders(): array;
+
+	/**
+	 * Child classes inspect the wp_remote_request response,
+	 * decode it, and handle/log errors as needed
+	 */
+	abstract protected function handleResponse(array|\WP_Error $response, string $method, string $endpoint): array;
+
+	public function getRequest(string $endpoint, array $data = [], bool $force = false, ?int $ttl = null): array
+	{
+		$key = [$this->service_name, $this->userID, $endpoint, $data];
+		if (!$force) {
+			$cached = $this->cache->get($key);
+			if ($cached) {
+				return $cached;
+			}
+		}
+		$response = $this->makeRequest('GET', $endpoint, $data);
+
+		if ($response && !is_wp_error($response)) {
+			$ttl = is_int($ttl) ? $ttl : $this->ttl;
+			$this->cache->set($key, $response, $ttl);
+		}
+
+		return $response;
+	}
+
+	public function postRequest(string $endpoint, array $data = []): array
+	{
+		return $this->makeRequest('POST', $endpoint, $data);
+	}
+
+	public function putRequest(string $endpoint, array $data = []): array
+	{
+		return $this->makeRequest('PUT', $endpoint, $data);
+	}
+
+	public function deleteRequest(string $endpoint, array $data = []): array
+	{
+		return $this->makeRequest('DELETE', $endpoint, $data);
+	}
+
+	protected function makeRequest(string $method, string $endpoint, array $data = []): array
+	{
+		if (is_null($this->baseUrl)) {
+			error_log('Can not make request. No $baseUrl set for '.$this->service_name);
+			return [];
+		}
+		if ($this->requestLimiter && !$this->awaitRateLimit($endpoint)) {
+			$error = new \WP_Error('rate_limit_exceeded', "Rate limit exceeded for {$endpoint}");
+			return $this->handleResponse($error, $method, $endpoint);
+		}
+
+		$url = rtrim($this->baseUrl, '/') . '/' . ltrim($endpoint, '/');
+
+		$args = [
+			'method'	=> $method,
+			'headers'	=> $this->getRequestHeaders(),
+			'timeout'	=> 15,
+		];
+
+		if (!empty($data)) {
+			if (in_array($method, ['GET', 'DELETE'], true)) {
+				$url = add_query_arg($data, $url);
+			} else {
+				$args['body'] = wp_json_encode($data);
+			}
+		}
+
+		$response = wp_remote_request($url, $args);
+
+		$this->requestLimiter?->recordRequest($endpoint);
+
+		return $this->handleResponse($response, $method, $endpoint);
+	}
+
+	/**
+	 * Waits (with jitter) for the rate limit to clear, up to a few attempts.
+	 * Returns false if still limited after max attempts.
+	 */
+	protected function awaitRateLimit(string $endpoint, int $maxAttempts = 3): bool
+	{
+		$attempts = 0;
+
+		while (!$this->requestLimiter->checkRateLimit($endpoint)) {
+			$attempts++;
+			if ($attempts >= $maxAttempts) {
+				return false;
+			}
+			usleep(random_int(250_000, 750_000)); // 250-750ms jitter
+		}
+
+		return true;
+	}
+}
diff --git a/inc/integrations/SyncDefaults.php b/inc/integrations/SyncDefaults.php
new file mode 100644
index 0000000..383aa7b
--- /dev/null
+++ b/inc/integrations/SyncDefaults.php
@@ -0,0 +1,13 @@
+<?php
+namespace JVBase\integrations;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+/**
+ * Handles the getting and setting of default values for service fields.
+ */
+trait SyncDefaults {
+
+}
diff --git a/inc/integrations/SyncFrom.php b/inc/integrations/SyncFrom.php
new file mode 100644
index 0000000..a06bf5d
--- /dev/null
+++ b/inc/integrations/SyncFrom.php
@@ -0,0 +1,41 @@
+<?php
+namespace JVBase\integrations;
+
+use Exception;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+trait SyncFrom {
+	use SyncHelpers;
+	/**
+	 * @param array $itemData
+	 * @return bool
+	 * @throws Exception
+	 */
+	protected function createItemFromService(array $itemData):bool
+	{
+		throw new Exception('createItemFromService must be implemented by child class '.$this->service_name);
+	}
+
+	/**
+	 * @param array $itemData
+	 * @return bool
+	 * @throws Exception
+	 */
+	protected function updateItemFromService(array $itemData):bool
+	{
+		throw new Exception('updateItemFromService must be implemented by child class '.$this->service_name);
+	}
+
+	/**
+	 * @param array $itemData
+	 * @return bool
+	 * @throws Exception
+	 */
+	protected function deleteItemFromService(array $itemData):bool
+	{
+		throw new Exception('deleteItemFromService must be implemented by child class '.$this->service_name);
+	}
+}
diff --git a/inc/integrations/SyncHelpers.php b/inc/integrations/SyncHelpers.php
new file mode 100644
index 0000000..f2d0ca0
--- /dev/null
+++ b/inc/integrations/SyncHelpers.php
@@ -0,0 +1,144 @@
+<?php
+namespace JVBase\integrations;
+
+use JVBase\meta\Meta;
+use WP_Query;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+trait SyncHelpers {
+	use _Base;
+	/**
+	 * Queue types
+	 * These types match with IntegrationExecutor
+	 */
+	protected static string $syncTo;
+	protected static string $deleteFrom;
+	protected static string $syncFrom;
+	protected static string $syncCustomer;
+	protected static string $import;
+
+	protected function setQueueTypes():void
+	{
+		self::$syncTo = $this->service_name.'_sync_to';
+		self::$deleteFrom = $this->service_name.'_delete_from';
+		self::$syncFrom = $this->service_name.'_sync_from';
+		//TODO: What is the difference between sync_from and import?
+		self::$import = $this->service_name.'import_from';
+	}
+
+	public function getServiceItemID(int $itemID, string $type = 'post'):string
+	{
+		return match($type) {
+			'post'	=> Meta::forPost($itemID)->get("_{$this->service_name}_item_id"),
+			'term','taxonomy'	=> Meta::forTerm($itemID)->get("_{$this->service_name}_item_id"),
+			'user'	=> Meta::forUser($itemID)->get("_{$this->service_name}_item_id"),
+			default => ''
+		};
+	}
+	protected function updateItemStatus(int|array $items, string $status = 'pending', string $type = 'post'):void
+	{
+		if (!is_array($items)) {
+			$items = [$items];
+		}
+		foreach ($items as $ID) {
+			match ($type) {
+				'post'	=> Meta::forPost($ID)->set('_'.$this->service_name.'_sync_status', $status),
+				'term'	=> Meta::forTerm($ID)->set('_'.$this->service_name.'_sync_status', $status),
+				'user'	=> Meta::forUser($ID)->set('_'.$this->service_name.'_sync_status', $status),
+				default => false
+			};
+		}
+	}
+
+
+	public function findWPItem(string $integrationID, ?string $type = null):array|false
+	{
+		if (empty($integrationID)) {
+			error_log('Integrations::findWPItem No Integration ID received.');
+			return false;
+		}
+		$types = ['post', 'term', 'user'];
+		if (!is_null($type) && !in_array($type, $types)) {
+			error_log('Integrations::findWPItem invalid type sent: '.$type.'; defaulting to checking all');
+			$type = null;
+		}
+		if (!is_null($type)) {
+			return $this->queryWPType($integrationID, $type);
+		} else {
+			foreach ($types as $type) {
+				$check = $this->queryWPType($integrationID, $type);
+				if ($check) {
+					return $check;
+				}
+			}
+		}
+		return false;
+	}
+
+	/**
+	 * @param string $integrationID
+	 * @param string|null $type
+	 * @return array|false if success, an array of $ID and $type
+	 */
+	public function queryWPType(string $integrationID, ?string $type = null):array|false
+	{
+		if (!in_array($type, ['post', 'term', 'user'])) {
+			return $this->findWPItem($integrationID);
+		}
+		return match($type) {
+			'post'	=> $this->findWPPost($integrationID),
+			'term'	=> $this->findWPTerm($integrationID),
+			'user'	=> $this->findWPUser($integrationID),
+			default => false
+		};
+	}
+	public function findWPPost(string $integrationID):array|false
+	{
+		if (empty($integrationID)) {
+			return false;
+		}
+		$get = new WP_Query([
+			'meta_query'	=> [
+				'key'	=> BASE."_{$this->service_name}_item_id",
+				'value'	=> $integrationID,
+			],
+			'fields'	=> 'ids',
+			'posts_per_page' => 1,
+		]);
+		return $get->have_posts() && !empty($get->posts) ? [$get->posts[0], 'post'] : false;
+	}
+	public function findWPTerm(string $integrationID):array|false
+	{
+		if (empty($integrationID)) {
+			return false;
+		}
+		$get = get_terms([
+			'meta_query'	=> [
+				'key'	=> BASE."_{$this->service_name}_item_id",
+				'value'	=> $integrationID,
+			],
+			'fields'	=> 'ids',
+			'hide_empty'=> true,
+			'number'	=> 1
+		]);
+		return is_wp_error($get) || empty($get) ? false : [$get[0], 'term'];
+	}
+	public function findWPUser(string $integrationID):array|false
+	{
+		if (empty($integrationID)) {
+			return false;
+		}
+		$get = get_users([
+			'meta_query'	=> [
+				'key'	=> BASE."_{$this->service_name}_item_id",
+				'value'	=> $integrationID,
+			],
+			'fields'	=> 'ids',
+			'number'	=> 1,
+		]);
+		return is_wp_error($get) || empty($get) ? false : [$get[0], 'user'];
+	}
+}
diff --git a/inc/integrations/SyncTo.php b/inc/integrations/SyncTo.php
new file mode 100644
index 0000000..dd1e9ca
--- /dev/null
+++ b/inc/integrations/SyncTo.php
@@ -0,0 +1,310 @@
+<?php
+namespace JVBase\integrations;
+
+use Exception;
+use WP_Error;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+trait SyncTo {
+	use SyncHelpers;
+	/**
+	 * Must be defined according to how each service needs it to be
+	 * @param int $itemID
+	 * @param string $type
+	 * @return array
+	 * @throws Exception
+	 */
+	protected function formatForService(int $itemID, string $type = 'post'):array
+	{
+		throw new Exception('formatForService must be implemented by child class');
+	}
+
+	/**
+	 * Defaults to an array of formatted items. Can be overridden in child classes
+	 * @param array $itemIDs
+	 * @param string $type
+	 * @return array
+	 */
+	protected function formatItems(array $itemIDs, string $type):array
+	{
+		$items = [];
+		foreach ($itemIDs as $ID) {
+			try {
+				$items[] = $this->formatForService($ID, $type);
+			} catch (Exception $e){
+				$this->logError('Could not format Item for service',[
+					'itemID' => $ID,
+					'type'	=> $type,
+					'message'	=> $e->getMessage(),
+				]);
+			}
+		}
+		return $items;
+	}
+	/***************************************************************
+	 * Item creation
+	***************************************************************/
+	public function createBatchToService($data):array
+	{
+		$type = $data['type']??'post';
+
+		//Check if any of the submitted ids are already created
+		$created = array_filter($data['items'],
+			function($ID) use ($type) {
+				return !empty($this->getServiceItemID($ID, $type));
+			});
+
+		$updated = [];
+		if (!empty($created)) {
+			$updateData = $data;
+			$updateData['items'] = $created;
+			$updated = $this->updateBatchToService($data);
+
+			//remove any updated items from the original items to process
+			$data['items'] = array_filter($data['items'], function ($ID) use ($created) {
+				return !in_array($ID, $created);
+			});
+		}
+
+		$items = $this->formatItems($data['items'], $type);
+		if (empty($items)) {
+			return $this->noCreatedItems($updated);
+		}
+
+		$response = [
+			'outcome'	=> 'failed',
+			'result'	=> [
+				'message'	=> 'No result'
+			]
+		];
+		if ($this->hasBatchCreate) {
+			$response = $this->sendBatchCreate($items);
+			if (!is_wp_error($response)) {
+				$result = $this->processBatchCreateResponse($data, $response);
+			} else {
+				$this->logError('Batch create failed',[
+					'method'	=> 'createBatchToService',
+					'item_ids'	=> $data['items'],
+					'error'		=> $response
+				]);
+				$this->updateItemStatus($data['items'], 'error');
+
+				$result = [
+					'outcome'	=> 'failed_permanent',
+					'result'	=> 'Could not update items'
+				];
+			}
+		} else {
+			$success = $errors = [];
+			foreach ($items as $item) {
+				$itemResult = $this->createOne($item);
+				if (!is_wp_error($itemResult)) {
+					$success[] = $itemResult;
+				} else {
+					$errors[] = $itemResult;
+				}
+			}
+			$result = [
+				'outcome'	=> empty($errors) ? 'success' : (empty($success) ? 'failed' : 'partial'),
+				'result'	=> [
+					'success'	=> $success,
+					'errors'	=> $errors
+				]
+			];
+		}
+
+		return array_merge($result, $updated);
+	}
+
+	/**
+	 * To be implemented by integration extension. Updates a single item
+	 * @param array $item
+	 * @return array|WP_Error
+	 */
+	protected function createOne(array $item):array|WP_Error
+	{
+		return new WP_Error('invalid', 'Integration '.$this->service_name.' must implement its own createOne');
+	}
+
+	/**
+	 * Overridden by child classes
+	 * @param array $items
+	 * @return array|WP_Error
+	 */
+	protected function sendBatchCreate(array $items):array|WP_Error
+	{
+		return new WP_Error('invalid', 'Integration '.$this->service_name.' must implement its own sendBatchCreate');
+	}
+
+	/**
+	 * To be implemented by extensions
+	 * @param array $data
+	 * @param array $response
+	 * @return array
+	 */
+	protected function processBatchCreateResponse(array $data, array $response):array
+	{
+		return [
+			'outcome'	=> 'failed',
+			'result'	=> [
+				'message'	=> $this->service_name.' should implement processBatchCreateResponse.'
+			]
+		];
+	}
+
+	/*****************************************************************
+	 * ITEM UPDATING
+	*****************************************************************/
+	public function updateBatchToService(array $data):array
+	{
+		$type = $data['type']??'post';
+
+		$newlyCreated = [];
+		if (!$this->canCreateOnUpdate) {
+			$created = array_filter($data['items'], function($ID) use ($type) {
+				return !empty($this->getServiceItemID($ID, $type));
+			});
+
+			//Test to see if we have any that haven't been created yet.
+			//For some services, items may have to be created before they can be updated
+			if (count($created) !== count($data['items'])) {
+				$notCreated = array_filter($data['items'], function($ID) use ($type) {
+					return empty($this->getServiceItemID($ID, $type));
+				});
+				$newData = $data;
+				$newData['items'] = $notCreated;
+				$newlyCreated = $this->createBatchToService($newData);
+			}
+
+			// If we don't have any that are created, just send the noUpdatedItems response, with any newly created items added
+			if (empty($created)) {
+				return $this->noUpdatedItems($newlyCreated);
+			}
+			$data['items'] = $created;
+		}
+
+
+		$items = $this->formatItems($data['items'], $type);
+
+		if (empty($items)) {
+			return $this->noUpdatedItems($newlyCreated);
+		}
+
+		if ($this->hasBatchUpdate) {
+			$response = $this->sendBatchUpdate($items);
+			if (!is_wp_error($response)) {
+				$result = $this->processBatchUpdateResponse($data, $response);
+			} else {
+				$this->logError('Batch update failed',[
+					'method'	=> 'updateBatchToService',
+					'item_ids'	=> $data['items'],
+					'error'		=> $response
+				]);
+				$this->updateItemStatus($data['items'], 'error');
+
+				$result = [
+					'outcome'	=> 'failed_permanent',
+					'result'	=> 'Could not update items'
+				];
+			}
+		} else {
+			$success = $errors = [];
+			//Does not have batch update, manually update each one
+			foreach ($items as $item) {
+				$itemResult = $this->updateOne($item);
+				if (!is_wp_error($itemResult)) {
+					$success[] = $itemResult;
+				} else {
+					$errors[] = $itemResult;
+				}
+			}
+
+			$result = [
+				'outcome'	=> empty($errors) ? 'success' : (empty($success) ? 'failed' : 'partial'),
+				'result'	=> [
+					'success'	=> $success,
+					'errors'	=> $errors
+				]
+			];
+		}
+
+		return array_merge($result, $newlyCreated);
+	}
+
+	/**
+	 * To be implemented by integration extension. Updates a single item
+	 * @param array $item
+	 * @return array|WP_Error
+	 */
+	protected function updateOne(array $item):array|WP_Error
+	{
+		return new WP_Error('invalid', 'Integration '.$this->service_name.' must implement its own updateOne method');
+	}
+
+	/**
+	 * Overridden by child classes
+	 * @param array $items
+	 * @return array|WP_Error
+	 */
+	protected function sendBatchUpdate(array $items):array|WP_Error
+	{
+		return new WP_Error('invalid', 'Integration '.$this->service_name.' must implement its own sendBatchUpdate');
+	}
+
+
+	/**
+	 * To be implemented by extensions
+	 * @param array $data
+	 * @param array $response
+	 * @return array
+	 */
+	protected function processBatchUpdateResponse(array $data, array $response):array
+	{
+		return [
+			'outcome'	=> 'failed',
+			'result'	=> [
+				'message'	=> $this->service_name.' should implement processBatchUpdateResponse.'
+			]
+		];
+	}
+	/*****************************************************************
+	 * UTILITY
+	*****************************************************************/
+	protected function noUpdatedItems(array $created):array
+	{
+		$result =  [
+			'outcome'	=> 'success',
+			'result'	=> [
+				'message'	=> 'No items to update',
+				'updated'	=> [],
+				'errors'	=> [],
+			]
+		];
+
+		if (!empty($created)) {
+			error_log('Result before: '.print_r($result, true));
+			$result = array_merge($result, $created);
+			error_log('Result after merge: '.print_r($result, true));
+		}
+		return $result;
+	}
+
+	protected function noCreatedItems(array $updated):array
+	{
+		$result = [
+			'outcome'	=> 'success',
+			'result'	=> [
+				'message'	=> 'No items to create',
+				'created'	=> [],
+				'errors'	=> [],
+			]
+		];
+		if (!empty($updated)) {
+			$result = array_merge($result, $updated);
+		}
+		return $result;
+	}
+}
diff --git a/inc/integrations/UserConnection.php b/inc/integrations/UserConnection.php
new file mode 100644
index 0000000..f1f61d1
--- /dev/null
+++ b/inc/integrations/UserConnection.php
@@ -0,0 +1,62 @@
+<?php
+namespace JVBase\integrations;
+
+use JVBase\meta\Meta;
+use JVBase\registrar\Registrar;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+trait UserConnection {
+	protected function determineUser():void
+	{
+		if (current_user_can('manage_options')) {
+			$this->switchUser();
+		}
+		$this->switchUser(get_current_user_id());
+	}
+	public function switchUser(?int $userID = null):bool
+	{
+		if (!$this->userCanConnect($userID)) {
+			return false;
+		}
+		$this->clearUser();
+		$this->userID = $userID;
+		$this->initializeRateLimiters();
+		return true;
+	}
+	protected function clearUser():void
+	{
+		$this->userID = false;
+		$this->credentials = [];
+		if (property_exists($this, 'rateLimiter')) {
+			$this->requestLimiter?->saveHistory();
+			$this->requestLimiter = null;
+		}
+		if (property_exists($this, 'oathLimiter')) {
+			$this->oauthLimiter?->saveHistory();
+			$this->oauthLimiter = null;
+		}
+	}
+
+	public function userCanConnect(?int $userID):bool
+	{
+		if (current_user_can('manage_options')) {
+			return true;
+		}
+
+		$user = get_userdata($userID);
+		if (!$user || is_wp_error($user)) {
+			return false;
+		}
+		$role = jvbUserRole($userID);
+		$registrar = Registrar::getInstance($role);
+		if (!$registrar || !$registrar->hasIntegration($this->service_name)) {
+			return false;
+		}
+
+		return (bool) Meta::forUser($userID)->get('integration_'.$this->service_name.'_connected');
+	}
+
+}
diff --git a/inc/integrations/Webhooks.php b/inc/integrations/Webhooks.php
new file mode 100644
index 0000000..0b8c56a
--- /dev/null
+++ b/inc/integrations/Webhooks.php
@@ -0,0 +1,125 @@
+<?php
+namespace JVBase\integrations;
+
+use Exception;
+use WP_REST_Request;
+use WP_REST_Response;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+trait Webhooks {
+	use _Base;
+	protected bool $hasWebhooks = true;
+
+	abstract protected function validateWebhook(array $payload):bool;
+	abstract protected function processWebhook(array $payload):bool;
+	abstract protected function extractWebhookId(array $payload):string;
+
+	/**
+	 * Register webhook endpoint
+	 */
+	protected function registerWebhookEndpoint(): void
+	{
+		// Register REST API endpoint for webhooks
+		add_action('rest_api_init', function() {
+			register_rest_route('jvb/v1', '/webhooks/' . $this->service_name, [
+				'methods' => 'POST',
+				'callback' => [$this, 'handleWebhookRequest'],
+				'permission_callback' => '__return_true', // Webhooks come from external services
+			]);
+		});
+	}
+
+	/**
+	 * Handle webhook REST API request
+	 */
+	public function handleWebhookRequest(WP_REST_Request $request): WP_REST_Response
+	{
+		$payload = $request->get_params();
+		$headers = $request->get_headers();
+
+		// Include headers in payload for signature validation
+		$payload['_headers'] = $headers;
+
+		$success = $this->handleWebhook($payload);
+
+		return new WP_REST_Response([
+			'success' => $success
+		], $success ? 200 : 400);
+	}
+
+	public function handleWebhook(array $payload):bool
+	{
+		error_log('[Integrations]::handleWebhook for '.$this->service_name.': '.print_r($payload, true));
+
+		if (!$this->validateWebhook($payload)) {
+			$this->logError('handleWebhook', 'Webhook validation failed.');
+			return false;
+		}
+
+		//Check for duplicate processing
+		if ($this->isWebhookProcessed($payload)) {
+			return true;
+		}
+
+		try {
+			$result = $this->processWebhook($payload);
+
+			if ($result) {
+				$this->markWebhookProcessed($payload);
+			} else {
+				$this->logError('handleWebhook', 'Webhook processing returned false.');
+			}
+			return $result;
+		} catch (Exception $e) {
+			$this->logError('handleWebhook', 'Webhook processing failed', [
+				'error'	=> $e->getMessage(),
+				'webhook_id'	=> $this->extractWebhookId($payload),
+				'trace' => $e->getTraceAsString()
+			]);
+
+			Auth::getInstance()->markUnhealthy($this->service_name, 'oauth', $this->userID);
+			return false;
+		}
+	}
+
+
+	protected function renderWebhookUrl(): string
+	{
+		if (!$this->hasWebhooks || !$this->isSetUp()) {
+			return '';
+		}
+
+		$webhook_url = rest_url('jvb/v1/webhooks/' . $this->service_name);
+
+		return sprintf(
+			'<div class="webhook-info">
+            <h4>Webhook URL</h4>
+            <code id="webhook-url-%s">%s</code>
+            <p class="hint">Add this URL to your %s webhook settings</p>
+        </div>',
+			esc_attr($this->service_name),
+			esc_html($webhook_url),
+			esc_html($this->title)
+		);
+	}
+
+	protected function isWebhookProcessed(array $payload):bool
+	{
+		$id = $this->extractWebhookId($payload);
+		$key = BASE."webhook_processed_{$this->service_name}_{$id}";
+		return get_transient($key)!==false;
+	}
+	protected function markWebhookProcessed(array $payload):void
+	{
+		$id = $this->extractWebhookId($payload);
+		if (!empty($id)) {
+			$key = BASE."webhook_processed_{$this->service_name}_{$id}";
+			set_transient($key, true, DAY_IN_SECONDS);
+		}
+
+
+	}
+}
diff --git a/inc/integrations/_Base.php b/inc/integrations/_Base.php
new file mode 100644
index 0000000..2d62970
--- /dev/null
+++ b/inc/integrations/_Base.php
@@ -0,0 +1,110 @@
+<?php
+namespace JVBase\integrations;
+
+use JVBase\managers\Cache;
+use JVBase\managers\IconsManager;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+trait _Base {
+	/**
+	 * @var string Unique identifier for this service (ex: square, facebook)
+	 */
+	protected string $service_name;
+	/**
+	 * @var int|null The user context. If it is an OAuth integration, users may have their own connections, accessible via their user id
+	 */
+	protected ?int $userID = null;
+	/**
+	 * @var string Human-readable service name (ex: 'Google My Business', 'Square', etc)
+	 */
+	public string $title;
+	/**
+	 * @var string Phosphoricons icon slug
+	 */
+	public string $icon = '';
+	/**
+	 * @var string API version string (ex: 'v2', '2024-01-01')
+	 */
+	protected string $apiVersion;
+	protected Cache $cache;
+	//Use Trait "Requests" to set this
+	protected bool $hasRequests = false;
+	protected ?RateLimits $requestLimiter = null;
+	//Use Trait "OAuth" to set this
+	protected ?RateLimits $oauthLimiter = null;
+	protected bool $hasOAuth = false;
+	//Use trait "Webhooks" to set this true
+	protected bool $hasWebhooks = false;
+
+	/**
+	 * @var array Each item in the array becomes a li element in the ordered list of instructions
+	 */
+	protected array $instructions = [];
+	/**
+	 * @var array The required fields for the credentials. Typically a site_key/secret_key or equivalent.
+	 */
+	protected array $credentialFields = [];
+	/**
+	 * @var array The required fields for OAuth connection
+	 */
+	protected array $oauthFields = [];
+	/**
+	 * @var array Optional fields that just tweak functionality
+	 */
+	protected array $optionalFields = [];
+	protected bool $supportsWebp = true;
+	/**
+	 * @var bool Flag to indicate there are extra options that warrant a dedicated options page for this service
+	 * Usually used for default field values for syncable services
+	 */
+	public bool $hasExtraOptions = false;
+
+
+
+
+	/**
+	 * Check if integration is properly configured
+	 */
+	public function isSetUp(): bool
+	{
+		return !empty($this->loadCredentials());
+	}
+
+	public function loadCredentials():array
+	{
+		return Auth::getInstance()->getCredentials($this->service_name, $this->userID);
+	}
+
+	protected function logError(string $method, string $message, array $context = []):void
+	{
+		$context['service'] = $this->service_name;
+		JVB()->error()->log('['.self::class.']'.$method, $message, $context);
+	}
+
+	protected function getReturnURL():string
+	{
+		//If we are on the main admin page, we want to go back to our Integrations page on admin
+		//If we are on the custom dashboard, we want to go back to that dashboard page
+		return is_admin() ?
+			admin_url('admin.php?page=jvb-integrations') :
+			get_home_url(null, '/dash/integrations/');
+	}
+	protected function determineOptionPage():string
+	{
+		return is_admin() ?
+			admin_url('admin.php?page=jvb-integrations-'.$this->service_name) :
+			get_home_url(null, '/dash/integrations/'.$this->service_name.'/');
+	}
+
+	protected function outputIcon(string $icon):string
+	{
+		if (is_admin()) {
+			$icons = IconsManager::for('admin');
+			return $icons->getRawSvg($icon);
+		}
+		return jvbDashIcon($icon);
+	}
+}
diff --git a/inc/integrations/_setup.php b/inc/integrations/_setup.php
index b9ed54e..9739293 100644
--- a/inc/integrations/_setup.php
+++ b/inc/integrations/_setup.php
@@ -1,47 +1,52 @@
 <?php
 use JVBase\base\Site;
+require(JVB_DIR . '/inc/integrations/Admin.php');
+require(JVB_DIR . '/inc/integrations/Ajax.php');
+require(JVB_DIR . '/inc/integrations/Auth.php');
+require(JVB_DIR . '/inc/integrations/OAuth.php');
+require(JVB_DIR . '/inc/integrations/RateLimits.php');
+require(JVB_DIR . '/inc/integrations/Requests.php');
 require(JVB_DIR . '/inc/integrations/Integrations.php');
-require(JVB_DIR . '/inc/integrations/CredentialsManager.php');
 
 if (Site::hasIntegration('maps')) {
-	require(JVB_DIR . '/inc/integrations/GoogleMaps.php');
+	require(JVB_DIR . '/inc/integrations/services/GoogleMaps.php');
 }
 
 if (Site::hasIntegration('umami')) {
-	require(JVB_DIR . '/inc/integrations/Umami.php');
+	require(JVB_DIR . '/inc/integrations/services/Umami.php');
 }
 if (Site::hasIntegration('cloudflare')) {
-	require(JVB_DIR . '/inc/integrations/Cloudflare.php');
+	require(JVB_DIR . '/inc/integrations/services/Cloudflare.php');
 }
 
 if (Site::hasIntegration('square')) {
-	require(JVB_DIR . '/inc/integrations/Square.php');
+	require(JVB_DIR . '/inc/integrations/services/Square.php');
 }
 
 if (Site::hasIntegration('facebook')) {
-	require(JVB_DIR . '/inc/integrations/Facebook.php');
+	require(JVB_DIR . '/inc/integrations/services/Facebook.php');
 }
 
 if (Site::hasIntegration('instagram')) {
 	if (!Site::hasIntegration('facebook')) {
-		require(JVB_DIR . '/inc/integrations/Facebook.php');
+		require(JVB_DIR . '/inc/integrations/services/Facebook.php');
 	}
-	require(JVB_DIR . '/inc/integrations/Instagram.php');
+	require(JVB_DIR . '/inc/integrations/services/Instagram.php');
 }
 
 if (Site::hasIntegration('bluesky')) {
-	require(JVB_DIR . '/inc/integrations/BlueSky.php');
+	require(JVB_DIR . '/inc/integrations/services/BlueSky.php');
 }
 
 if (Site::hasIntegration('gmb')) {
-	require(JVB_DIR . '/inc/integrations/GoogleMyBusiness.php');
+	require(JVB_DIR . '/inc/integrations/services/GoogleMyBusiness.php');
 }
 
 if (Site::hasIntegration('postmark')) {
-	require(JVB_DIR . '/inc/integrations/PostMark.php');
+	require(JVB_DIR . '/inc/integrations/services/PostMark.php');
 }
 
 
 if (Site::hasIntegration('helcim')) {
-	require(JVB_DIR . '/inc/integrations/Helcim.php');
+	require(JVB_DIR . '/inc/integrations/services/Helcim.php');
 }
diff --git a/inc/integrations/BlueSky.php b/inc/integrations/services/BlueSky.php
similarity index 98%
rename from inc/integrations/BlueSky.php
rename to inc/integrations/services/BlueSky.php
index 2691efe..dadabb8 100644
--- a/inc/integrations/BlueSky.php
+++ b/inc/integrations/services/BlueSky.php
@@ -1,9 +1,7 @@
 <?php
-namespace JVBase\integrations;
-
+namespace JVBase\integrations\services;
 use Exception;
-use WP_Error;
-
+use JVBase\integrations\Integrations;
 if (!defined('ABSPATH')) {
 	exit;
 }
@@ -361,7 +359,7 @@
 	public function disconnectUser(int $user_id): bool
 	{
 		$key = "user_{$user_id}_{$this->service_name}";
-		$credentials_manager = CredentialsManager::getInstance();
+		$credentials_manager = Auth::getInstance();
 		return $credentials_manager->deleteCredentials($key);
 	}
 
diff --git a/inc/integrations/Cloudflare.php b/inc/integrations/services/Cloudflare.php
similarity index 98%
rename from inc/integrations/Cloudflare.php
rename to inc/integrations/services/Cloudflare.php
index ed37f69..1425725 100644
--- a/inc/integrations/Cloudflare.php
+++ b/inc/integrations/services/Cloudflare.php
@@ -1,7 +1,7 @@
 <?php
-namespace JVBase\integrations;
 
-use WP_Error;
+namespace JVBase\integrations\services;
+use JVBase\integrations\Integrations;
 if (!defined('ABSPATH')) {
 	exit;
 }
diff --git a/inc/integrations/Facebook.php b/inc/integrations/services/Facebook.php
similarity index 98%
rename from inc/integrations/Facebook.php
rename to inc/integrations/services/Facebook.php
index 2812607..842cc96 100644
--- a/inc/integrations/Facebook.php
+++ b/inc/integrations/services/Facebook.php
@@ -3,12 +3,12 @@
  * Facebook Integration
  * Handles Facebook posts, page information, events, and OAuth authentication
  */
-namespace JVBase\integrations;
 
-use Exception;
+namespace JVBase\integrations\services;
+use JVBase\integrations\Integrations;
 use JVBase\meta\Meta;
-use WP_Error;
 use WP_Post;
+use WP_Error;
 
 if (!defined('ABSPATH')) {
 	exit;
@@ -127,8 +127,9 @@
 		$this->defaults = [
 
 		];
-		$this->handleWebhooks = true;
-		parent::__construct($userID);
+		$this->hasWebhooks = true;
+
+		parent::__construct();
 
 		$this->actions = array_merge($this->actions, [
 			'sync_pages'	=> 'getUserPages'
diff --git a/inc/integrations/GoogleMaps.php b/inc/integrations/services/GoogleMaps.php
similarity index 99%
rename from inc/integrations/GoogleMaps.php
rename to inc/integrations/services/GoogleMaps.php
index 545e323..6477a0d 100644
--- a/inc/integrations/GoogleMaps.php
+++ b/inc/integrations/services/GoogleMaps.php
@@ -3,10 +3,10 @@
  * Google Maps Integration
  * File: /inc/integrations/GoogleMapsIntegration.php
  */
-namespace JVBase\integrations;
 
+namespace JVBase\integrations\services;
+use JVBase\integrations\Integrations;
 use JVBase\registrar\Registrar;
-use WP_Error;
 
 if (!defined('ABSPATH')) {
 	exit;
diff --git a/inc/integrations/GoogleMyBusiness.php b/inc/integrations/services/GoogleMyBusiness.php
similarity index 99%
rename from inc/integrations/GoogleMyBusiness.php
rename to inc/integrations/services/GoogleMyBusiness.php
index a66a81a..cd80f51 100644
--- a/inc/integrations/GoogleMyBusiness.php
+++ b/inc/integrations/services/GoogleMyBusiness.php
@@ -1,8 +1,8 @@
 <?php
-namespace JVBase\integrations;
-
+namespace JVBase\integrations\services;
+use JVBase\integrations\Integrations;
 use JVBase\meta\Meta;
-use WP_Error;
+
 if (!defined('ABSPATH')) {
 	exit;
 }
@@ -125,7 +125,7 @@
 			'Use the "Authorize" button below to complete OAuth flow'
 		];
 
-		parent::__construct($userID);
+		parent::__construct();
 
 		$this->actions = array_merge(
 			$this->actions,
diff --git a/inc/integrations/Helcim.php b/inc/integrations/services/Helcim.php
similarity index 99%
rename from inc/integrations/Helcim.php
rename to inc/integrations/services/Helcim.php
index f5dc8a5..47d97fd 100644
--- a/inc/integrations/Helcim.php
+++ b/inc/integrations/services/Helcim.php
@@ -1,13 +1,11 @@
 <?php
-namespace JVBase\integrations;
 
-use Exception;
-use JVBase\meta\Meta;
-use WP_Error;
-use WP_Post;
-use JVBase\ui\Checkout;
-use JVBase\managers\queue\TypeConfig;
+namespace JVBase\integrations\services;
+use JVBase\integrations\Integrations;
 use JVBase\managers\queue\executors\IntegrationExecutor;
+use JVBase\managers\queue\TypeConfig;
+use JVBase\meta\Meta;
+use JVBase\ui\Checkout;
 
 if (!defined('ABSPATH')) {
 	exit;
@@ -103,9 +101,9 @@
 			'delete' => false,
 		];
 
-		$this->handleWebhooks = false;
+		$this->hasWebhooks = false;
 
-		parent::__construct($userID);
+		parent::__construct();
 
 		// Helcim-specific actions (processAction dispatches these)
 		$this->actions = array_merge($this->actions, [
diff --git a/inc/integrations/Instagram.php b/inc/integrations/services/Instagram.php
similarity index 98%
rename from inc/integrations/Instagram.php
rename to inc/integrations/services/Instagram.php
index 3968e45..838246f 100644
--- a/inc/integrations/Instagram.php
+++ b/inc/integrations/services/Instagram.php
@@ -3,14 +3,10 @@
  * Instagram Integration
  * File: /inc/integrations/Instagram.php
  */
-namespace JVBase\integrations;
 
+namespace JVBase\integrations\services;
 use JVBase\integrations\Integrations;
 use JVBase\meta\Meta;
-use WP_Error;
-use WP_REST_Request;
-use WP_REST_Response;
-use Exception;
 
 if (!defined('ABSPATH')) {
 	exit;
@@ -99,7 +95,7 @@
 		$this->defaults = [
 
 		];
-		$this->handleWebhooks = true;
+		$this->hasWebhooks = true;
 		parent::__construct($userID);
 
 		$this->actions = array_merge(
@@ -192,8 +188,8 @@
 	}
 
 	public function getCredentials():array {
-		parent::getCredentials();
-		if (empty($this->credentials)) {
+		$credentials = parent::getCredentials();
+		if (empty($credentials)) {
 			$facebook = JVB()->connect('facebook');
 
 			if (!$facebook || !$facebook->isSetUp()) {
@@ -202,12 +198,12 @@
 
 			$fbCredentials = $facebook->getCredentials();
 
-			$this->credentials = [
+			return [
 				'client_id'	=> $fbCredentials['client_id'],
 				'client_secret' => $fbCredentials['client_secret'],
 			];
 		}
-		return $this->credentials;
+		return $credentials;
 	}
 
 	private function switchInstagramAccount(array $data): WP_Error|array
diff --git a/inc/integrations/PostMark.php b/inc/integrations/services/PostMark.php
similarity index 99%
rename from inc/integrations/PostMark.php
rename to inc/integrations/services/PostMark.php
index a06f65f..4f6f2ee 100644
--- a/inc/integrations/PostMark.php
+++ b/inc/integrations/services/PostMark.php
@@ -1,8 +1,7 @@
 <?php
-namespace JVBase\integrations;
 
-use WP_Error;
-
+namespace JVBase\integrations\services;
+use JVBase\integrations\Integrations;
 if (!defined('ABSPATH')) {
 	exit;
 }
@@ -117,7 +116,7 @@
 			]
 		];
 
-		parent::__construct($userID);
+		parent::__construct();
 
 		// Hook into WordPress mail system if integration is healthy
 		if ($this->is_healthy && $this->isSetUp()) {
diff --git a/inc/integrations/Square.php b/inc/integrations/services/Square.php
similarity index 98%
rename from inc/integrations/Square.php
rename to inc/integrations/services/Square.php
index c0191a3..0d18f51 100644
--- a/inc/integrations/Square.php
+++ b/inc/integrations/services/Square.php
@@ -1,18 +1,19 @@
 <?php
-namespace JVBase\integrations;
 
+namespace JVBase\integrations\services;
+use Exception;
+use JVBase\integrations\Integrations;
+use JVBase\managers\queue\executors\IntegrationExecutor;
+use JVBase\managers\queue\TypeConfig;
 use JVBase\meta\Form;
 use JVBase\meta\Meta;
-use Exception;
-use JVBase\registrar\Fields;
-use JVBase\registrar\Posts;
 use JVBase\registrar\Registrar;
+use JVBase\ui\Checkout;
 use JVBase\ui\CRUDSkeleton;
 use WP_Error;
-use JVBase\ui\Checkout;
-use JVBase\managers\queue\TypeConfig;
-use JVBase\managers\queue\executors\IntegrationExecutor;
+use WP_Post;
 use WP_Query;
+use WP_User;
 
 if (!defined('ABSPATH')) {
 	exit;
@@ -86,16 +87,14 @@
 	protected string $locationId = '';
 	protected array $locations = [];
 
-	protected static array $instances = [];
+	protected static Square $instance;
 
-	public static function getInstance(?int $userID = null):self
+	public static function getInstance():self
 	{
-		$key = is_null($userID) ? 'base' : $userID;
-
-		if (!array_key_exists($key, self::$instances)) {
-			self::$instances[$key] = new self($userID);
+		if (!isset(self::$instance)) {
+			self::$instance = new self();
 		}
-		return self::$instances[$key];
+		return self::$instance;
 	}
 
 	protected function __construct(?int $userID = null)
@@ -190,9 +189,9 @@
 			'delete' => true
 		];
 		$this->supportsWebp = false;
-		$this->handleWebhooks = true;
+		$this->hasWebhooks = true;
 
-		parent::__construct($userID);
+		parent::__construct();
 
 		// Add Square-specific actions
 		$this->actions = array_merge(
@@ -858,6 +857,7 @@
 	protected function registerAdditionalHooks(): void
 	{
 		if (!$this->isSetUp()) {
+			error_log('Square is not setup');
 			return;
 		}
 
@@ -932,7 +932,7 @@
 	/**
 	 * Handle post save for Square sync
 	 */
-	protected function handleTheSavePost(int $postID, \WP_Post $post, bool $update, array $settings): void
+	protected function handleTheSavePost(int $postID, WP_Post $post, bool $update, array $settings): void
 	{
 		error_log('==== [Square]::handleTheSavePost ====');
 		$this->queueOperation(self::$syncTo, [
@@ -1113,7 +1113,7 @@
 			if (!in_array($post_id, $success)) {
 				$found_in_errors = false;
 				foreach ($errors as $error) {
-					if (strpos($error, "Post $post_id") !== false) {
+					if (str_contains($error, "Post $post_id")) {
 						$found_in_errors = true;
 						break;
 					}
@@ -1542,7 +1542,7 @@
 		}
 
 		// Set user role (assuming you have a customer role defined)
-		$user = new \WP_User($user_id);
+		$user = new WP_User($user_id);
 		$user->set_role(BASE.'foodie');
 
 		// Generate password reset key
@@ -1576,7 +1576,7 @@
 	/**
 	 * Send welcome email with password setup
 	 */
-	private function sendWelcomeEmail(\WP_User $user, string $reset_key): void
+	private function sendWelcomeEmail(WP_User $user, string $reset_key): void
 	{
 		$site_name = get_bloginfo('name');
 		$reset_url = get_home_url(null, "wp-login.php?action=rp&key=$reset_key&login=" . rawurlencode($user->user_login), 'login');
@@ -2450,7 +2450,7 @@
 				$sanitized[$key] = sanitize_key($value);
 			}
 			// Handle boolean fields
-			elseif ($key === 'use_sandbox' || strpos($key, 'enable_') === 0) {
+			elseif ($key === 'use_sandbox' || str_starts_with($key, 'enable_')) {
 				$sanitized[$key] = (bool) $value;
 			}
 			// Handle numeric fields
@@ -3346,8 +3346,7 @@
 		// Fetch from Square
 		$response = $this->getRequest('orders/' . $order_id);
 		if (!is_wp_error($response)) {
-			$state = $response['order']['state'] ?? null;
-			return $state;
+			return $response['order']['state'] ?? null;
 		}
 
 		return null;
@@ -3430,7 +3429,7 @@
 	}
 	public function renderDashPage():void
 	{
-		$instance = $this->determineInstance();
+		$this->determineUser();
 
 		$crud = new CRUDSkeleton();
 		$crud->icon('receipt');
@@ -3452,13 +3451,7 @@
 
 		$crud->render();
 	}
-	protected function determineInstance():self
-	{
-		if (current_user_can('manage_options')) {
-			return self::$instances['base'];
-		}
-		return self::getInstance(get_current_user_id());
-	}
+
 
 	public function getOrderPost(string $order_id):int|false
 	{
diff --git a/inc/integrations/Umami.php b/inc/integrations/services/Umami.php
similarity index 98%
rename from inc/integrations/Umami.php
rename to inc/integrations/services/Umami.php
index 4afb1d5..807ec3b 100644
--- a/inc/integrations/Umami.php
+++ b/inc/integrations/services/Umami.php
@@ -1,10 +1,6 @@
 <?php
-namespace JVBase\integrations;
-
-use JVBase\managers\Cache;
-use WP_Error;
-use WP_Post;
-use Exception;
+namespace JVBase\integrations\services;
+use JVBase\integrations\Integrations;
 
 if (!defined('ABSPATH')) {
 	exit;
@@ -88,7 +84,7 @@
 			'Login to your <a href="https://cloud.umami.is/settings/websites" target="_blank">umami.is</a> account, select your website, and copy the website ID'
 		];
 
-		parent::__construct($userID);
+		parent::__construct();
 
 		$this->actions = array_merge(
 			$this->actions,
@@ -705,7 +701,7 @@
 	/**
 	 * Process queued operations
 	 */
-	public function processOperation(WP_Error|array $result, object $operation, array $data): WP_Error|array
+	public function processOperation(array|\WP_Error $result, object $operation, array $data): array|\WP_Error
 	{
 		$prefix = strtolower($this->service_name) . '_';
 
diff --git a/inc/managers/Dashboard/DashboardManager.php b/inc/managers/Dashboard/DashboardManager.php
index 2a09547..2f7c43e 100644
--- a/inc/managers/Dashboard/DashboardManager.php
+++ b/inc/managers/Dashboard/DashboardManager.php
@@ -1,19 +1,7 @@
 <?php
 namespace JVBase\managers\Dashboard;
 
-use JetBrains\PhpStorm\NoReturn;
-use JVBase\forms\TaxonomySelector;
-use JVBase\base\Site;
-use JVBase\managers\Cache;
-use JVBase\managers\IconsManager;
-use JVBase\managers\LoginManager;
-use JVBase\managers\RoleManager;
-use JVBase\meta\Form;
-use JVBase\meta\Meta;
-use JVBase\registrar\Fields;
-use JVBase\registrar\Registrar;
-use JVBase\ui\Navigation;
-use JVBase\ui\Tabs;
+use JetBrains\PhpStorm\NoReturn;use JVBase\base\Site;use JVBase\forms\TaxonomySelector;use JVBase\managers\Cache;use JVBase\managers\IconsManager;use JVBase\managers\LoginManager;use JVBase\managers\RoleManager;use JVBase\meta\Form;use JVBase\meta\Meta;use JVBase\registrar\Fields;use JVBase\registrar\Registrar;use JVBase\ui\Navigation;use JVBase\ui\Tabs;
 
 if (!defined('ABSPATH')) {
 	exit;
@@ -524,16 +512,16 @@
 
 				foreach (array_keys(Site::getIntegrations()) as $integration) {
 					$integration = match($integration) {
-						'maps' => 'JVBase\integrations\GoogleMaps',
-						'square' => 'JVBase\integrations\Square',
-						'facebook' => 'JVBase\integrations\Facebook',
-						'helcim' => 'JVBase\integrations\Helcim',
-						'instagram' => 'JVBase\integrations\Instagram',
-						'bluesky' => 'JVBase\integrations\BlueSky',
-						'gmb' => 'JVBase\integrations\GoogleMyBusiness',
-						'cloudflare' => 'JVBase\integrations\Cloudflare',
-						'umami' => 'JVBase\integrations\Umami',
-						'postmark' => 'JVBase\integrations\PostMark',
+						'maps' => 'JVBase\integrations\services\GoogleMaps',
+						'square' => 'JVBase\integrations\services\Square',
+						'facebook' => 'JVBase\integrations\services\Facebook',
+						'helcim' => 'JVBase\integrations\services\Helcim',
+						'instagram' => 'JVBase\integrations\services\Instagram',
+						'bluesky' => 'JVBase\integrations\services\BlueSky',
+						'gmb' => 'JVBase\integrations\services\GoogleMyBusiness',
+						'cloudflare' => 'JVBase\integrations\services\Cloudflare',
+						'umami' => 'JVBase\integrations\services\Umami',
+						'postmark' => 'JVBase\integrations\services\PostMark',
 					};
 					$title = $integration::title();
 					$icon = $integration::icon();
diff --git a/inc/rest/routes/FeedRoutes.php b/inc/rest/routes/FeedRoutes.php
index b0fcbe2..814b821 100644
--- a/inc/rest/routes/FeedRoutes.php
+++ b/inc/rest/routes/FeedRoutes.php
@@ -1,17 +1,17 @@
 <?php
 namespace JVBase\rest\routes;
 
+use JVBase\base\Site;
 use JVBase\meta\Meta;
 use JVBase\registrar\Registrar;
 use JVBase\rest\Rest;
-use JVBase\integrations\Umami;
 use JVBase\rest\Route;
-use JVBase\base\Site;
-use WP_Query;
+use Umami;
 use WP_Post;
-use WP_Term;
+use WP_Query;
 use WP_REST_Request;
 use WP_REST_Response;
+use WP_Term;
 
 if (!defined('ABSPATH')) {
     exit; // Exit if accessed directly
diff --git a/inc/rest/routes/IntegrationsSquareRoutes.php b/inc/rest/routes/IntegrationsSquareRoutes.php
index f60dc34..79cbd0d 100644
--- a/inc/rest/routes/IntegrationsSquareRoutes.php
+++ b/inc/rest/routes/IntegrationsSquareRoutes.php
@@ -2,10 +2,10 @@
 namespace JVBase\rest\routes;
 
 
-use JVBase\integrations\Square;
+use Exception;
+use JVBase\integrations\services\Square;
 use JVBase\meta\Meta;
 use JVBase\rest\Rest;
-use Exception;
 use JVBase\rest\Route;
 use WP_REST_Request;
 use WP_REST_Response;
diff --git a/inc/ui/Checkout.php b/inc/ui/Checkout.php
index ad21146..82c96a6 100644
--- a/inc/ui/Checkout.php
+++ b/inc/ui/Checkout.php
@@ -31,11 +31,13 @@
 {
     public static function getProvider():Integrations|false
     {
-        foreach (Site::getIntegrations() as $integration => $active) {
-            if (in_array($integration, ['square', 'helcim']) && $active) {
-                return JVB()->connect($integration);
-            }
-        }
+		$int = ['square','helcim'];
+		foreach ($int as $i) {
+			if (Site::hasIntegration($i)) {
+				error_log('Site has '.$i);
+				return JVB()->connect($i);
+			}
+		}
         return false;
     }
 	/**
diff --git a/src/menu/style.scss b/src/menu/style.scss
index cea5a9f..f2858ff 100644
--- a/src/menu/style.scss
+++ b/src/menu/style.scss
@@ -4,7 +4,7 @@
 	font-size: var(--txt-medium);
 }
 
-.field.quantity > label {
+.field.quantity.quantity > label {
 	position: absolute;
 	left: var(--offScreen);
 }
@@ -27,7 +27,7 @@
 	}
 
 	> section * {
-		max-width: var(--wide);
+		max-width: var(--wide)!important;
 	}
 }
 

--
Gitblit v1.10.0