From a6413da67a4579866f0fd0323ddb23a7b6b9654a Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Fri, 18 Sep 2026 17:00:53 +0000
Subject: [PATCH] =Cleaning up the Integrations setup a bit, started the Helcim integration work too

---
 inc/integrations/OAuth.php |  210 +++++++++++++++++++++++++++++++++++++--------------
 1 files changed, 151 insertions(+), 59 deletions(-)

diff --git a/inc/integrations/OAuth.php b/inc/integrations/OAuth.php
index 5f8085f..5750df4 100644
--- a/inc/integrations/OAuth.php
+++ b/inc/integrations/OAuth.php
@@ -10,8 +10,8 @@
 
 trait OAuth
 {
-	use _Base, Admin;
-	protected bool $hasOAuth = true;
+	use _Base, Admin, ServiceMap;
+	protected OAuthURLs $oauth;
 	/**
 	 * Child classes must set this (e.g. 'https://api.example.com/v1/')
 	 */
@@ -20,35 +20,122 @@
 	protected int $oauthTTL = 300;
 	protected array $oathScopes = [];
 
+	protected ?RateLimits $oauthLimiter;
 
-	abstract protected function getOAuthRequestHeaders(): array;
-	abstract protected function handleOAuthResponse(array|WP_Error $response, string $method, string $endpoint): array;
+	protected function initializeOAuthRateLimiter():void
+	{
+		$key = $this->service_name;
+		if (!is_null($this->userID)) {
+			$key .= '_'.$this->userID;
+		}
+		$key .= '_oauth';
+		$this->oauthLimiter = new RateLimits($key, ['s' => 1, 'm' => 10, 'h' => 100]);
+	}
+	/**
+	 * @return array Defaults to the base getRequestHeaders, but can optionally be overridden
+	 */
+	protected function getOAuthRequestHeaders(): array
+	{
+		return $this->getRequestHeaders();
+	}
+	public function getOAuthBaseURL():?string
+	{
+		return $this->baseOAuthUrl;
+	}
+
+	/**
+	 * Default to the base handleResponse method, but extensions can modify accordingly
+	 * @param array|WP_Error $response
+	 * @param string $method
+	 * @param string $endpoint
+	 * @return array
+	 */
+	protected function handleOAuthResponse(array|WP_Error $response, string $method, string $endpoint): array
+	{
+		return $this->handleResponse($response, $method, $endpoint);
+	}
 	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);
+		if ($this->isSetUp()) {
+			$actions = [
+				[
+					'name'		=> 'Connect to Service',
+					'label'		=> 'Connect',
+					'icon'		=> 'plugs-connected',
+					'action'	=> [$this, 'handleOAuthConnect']
+				],
+				[
+					'name'		=> 'Disconnect from Service',
+					'label'		=> 'Disconnect',
+					'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);
+			}
 		}
 	}
+		public function hasOAuthCredentials(?int $userID = null):bool
+		{
+			if ($userID !== $this->userID) {
+				$this->switchUser($userID);
+			}
+			return $this->hasValidOAuth();
+		}
+		public function handleOAuthConnect():array
+		{
+			$url = $this->getOAuthUrl();
+			if ($url) {
+				return [
+					'success'	=> true,
+					'redirect'	=> $url,
+				];
+			}
+			return $this->response(false, 'Failed to generate OAuth URL');
+		}
+
+		public function handleOAuthDisconnect():array
+		{
+			$revoked = $this->revokeOAuth();
+			return $this->response($revoked, $revoked ?
+			'Successfully disconnected from '.$this->title :
+			'Failed to disconnect from '.$this->title);
+		}
+
+		public function revokeOAuth():bool
+		{
+			if (empty($this->oauth->getRevoke())) {
+				error_log('No revoke url set for '.$this->service_name);
+				return false;
+			}
+			$credentials = $this->loadCredentials();
+			if (empty($credentials['access_token'])) {
+				error_log('Could not send revoke request, no access_token for '.$this->service_name);
+			} else {
+				$response = $this->postOAuthRequest($this->oauth->getRevoke(), [
+					'body'	=> [
+						'token'	=> $credentials['access_token']
+					]
+				]);
+				$empty = new OAuthCredentials([]);
+				foreach ($empty->toArray() as $key => $value) {
+					if (array_key_exists($key, $credentials)) {
+						unset($credentials[$key]);
+					}
+				}
+
+				return Auth::getInstance()->storeCredentials($this->service_name, $credentials, $this->userID);
+			}
+			return false;
+		}
 
 	protected function registerOAuthCallbacks():void
 	{
@@ -105,10 +192,6 @@
 			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, '/');
 
@@ -215,12 +298,14 @@
 		}
 
 		// Get integration instance
-		$integration = JVB()->connect($this->service_name, $user_id);
+		$integration = $this->getService($this->service_name);
 
-		if (!$integration) {
+		if (!$integration || !method_exists($integration, 'switchUser')) {
 			wp_die('Invalid service: ' . esc_html($this->service_name), 'OAuth Error');
 		}
 
+		$integration->switchUser($user_id);
+
 
 		// Exchange code for tokens
 		$result = $integration->handleOAuthCode($code, $state);
@@ -246,20 +331,23 @@
 		$credentials = $this->loadCredentials();
 		$hasCredentials = $this->hasValidOAuth();
 		$returnURL = $this->getReturnURL();
-
-		if ($hasCredentials) {
+		if (!$this->isSetUp()) {
+			return '';
+		}
+		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>',
+				<a href="%s" class="btn jvb-oauth-connect" data-service="%s">%s Authorize Connection</a>
+				</div>',
 				$this->title,
 				$this->getOAuthURL($returnURL),
 				esc_attr($this->service_name),
 				$this->outputIcon($this->icon)
 			);
-		}
-		$form = sprintf(
-			'<details open>
+		} else {
+			$form = sprintf(
+				'<details open>
 			<summary>Connected Account
 				<div class="connection-status connected">
 					<span class="status-indicator">●</span>
@@ -268,22 +356,23 @@
 			</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) ?
+				$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;
 		}
-		//
-		if (method_exists($this, 'renderOAuthConnectedOptions')) {
-			$form .= $this->renderOAuthConnectedOptions();
-		}
-		$form .= '</details';
-		return $form;
 	}
 
 	/**
@@ -296,11 +385,9 @@
 
 	public function getOAuthURL(?string $return_url = null):string
 	{
-		if (!$this->hasOAuth) {
-			return '';
-		}
+
 		$credentials = $this->loadCredentials();
-		if (empty($credentials['authorize'])) {
+		if (empty($this->oauth->getAuthorize())) {
 			$this->logError('getOAuthUrl', 'OAuth authorize URL not configured');
 			return '';
 		}
@@ -308,7 +395,7 @@
 			'client_id'		=> $credentials['client_id']??$credentials['application_id']??$credentials['app_id']??'',
 			'redirect_url'	=> $this->getRedirectUri(),
 			'response_type'	=> 'code',
-			'scope'			=> implode(' ', $credentials['scopes']??[])
+			'scope'			=> implode(' ', $this->oauthScopes??[])
 		];
 		$state_key = wp_generate_password(32, false);
 		$user_id = $this->userID??0;
@@ -338,14 +425,19 @@
 			$params = $this->addOAuthParams($params);
 		}
 
-		return $credentials['authorize'] . '?' . http_build_query($params);
+		return $this->oauth->getAuthorize() . '?' . 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);
+		return admin_url('admin-ajax.php?action='.BASE . $this->service_name . '_oauth_callback');
+	}
+
+	/****************************************************
+	 * UTILITY
+	****************************************************/
+	protected function getAccessToken():string
+	{
+		return $this->loadCredentials()['access_token']??'';
 	}
 }

--
Gitblit v1.10.0