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/services/GoogleMyBusiness.php |  475 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 files changed, 464 insertions(+), 11 deletions(-)

diff --git a/inc/integrations/services/GoogleMyBusiness.php b/inc/integrations/services/GoogleMyBusiness.php
index 89f4f58..44002c7 100644
--- a/inc/integrations/services/GoogleMyBusiness.php
+++ b/inc/integrations/services/GoogleMyBusiness.php
@@ -2,13 +2,15 @@
 namespace JVBase\integrations\services;
 use Exception;
 use JVBase\integrations\Admin;
-use JVBase\integrations\Instance;
+use JVBase\Instance;
 use JVBase\integrations\Integrations;
 use JVBase\integrations\OAuth;
 use JVBase\integrations\OAuthCredentials;
 use JVBase\integrations\OAuthURLs;
 use JVBase\integrations\Requests;
+use JVBase\integrations\SyncToPost;
 use JVBase\integrations\UserConnection;
+use WP_Error;
 
 
 if (!defined('ABSPATH')) {
@@ -17,10 +19,10 @@
 
 class GoogleMyBusiness extends Integrations
 {
-	use Admin, Instance, OAuth, Requests, UserConnection;
+	use Admin, Instance, OAuth, Requests, UserConnection, SyncToPost;
 
 	protected string $readMask = 'name,title,storefrontAddress,metadata,openInfo,storeCode,categories,phoneNumbers,labels,specialHours';
-
+	protected string $businessV4;
 	protected function __construct()
 	{
 		$this->service_name = 'gmb';
@@ -34,6 +36,9 @@
 		];
 		$this->supportsWebp = false;
 
+		$this->baseUrl = 'https://mybusinessbusinessinformation.googleapis.com';
+		$this->businessV4 = 'https://mybusiness.googleapis.com/v4';
+
 		$this->defineFields();
 		$this->defineInstructions();
 		$this->defineOAuth();
@@ -51,6 +56,8 @@
 			);
 				$client_id->setRequired();
 				$client_id->setHint('From your Google Cloud Console OAuth credentials.');
+				$client_id->makeSetupField();
+				$this->updateField('client_id', $client_id);
 
 			$secret = $this->addField(
 				'client_secret',
@@ -60,6 +67,8 @@
 			);
 				$secret->setSubType('password');
 				$secret->setRequired();
+				$secret->makeSetupField();
+				$this->updateField('client_secret', $client_id);
 
 			if ($this->getAccessToken()) {
 				$accs = $this->addField('accounts', 'Accounts', 'select', [$this, 'testUserCanConnect']);
@@ -96,6 +105,25 @@
 				'Add redirect URI: <code>'.esc_html($this->getRedirectUri()).'</code>',
 				'Use the "Authorize" button below to complete OAuth flow'
 			];
+
+			$this->instructions = [
+				'Go to <a href="https://console.cloud.google.com/apis/credentials" target="_blank">Google Cloud Console</a>',
+				'Create OAuth 2.0 Client ID credentials',
+				'Enable the following APIs in your project:',
+				[
+					'<a href="https://console.cloud.google.com/apis/library/mybusinessaccountmanagement.googleapis.com" target="_blank">My Business Account Management API</a>',
+					'<a href="https://console.cloud.google.com/apis/library/mybusinessbusinessinformation.googleapis.com" target="_blank">My Business Business Information API</a>',
+					'<a href="https://console.cloud.google.com/apis/library/mybusiness.googleapis.com" target="_blank">Google My Business API</a> (legacy v4, required for reviews)',
+				],
+				'On the OAuth consent screen, add these scopes:',
+				[
+					'<code>https://www.googleapis.com/auth/business.manage</code>',
+					'<code>https://www.googleapis.com/auth/userinfo.email</code>',
+					'<code>https://www.googleapis.com/auth/userinfo.profile</code>',
+				],
+				'Add redirect URI: <code>'.esc_html($this->getRedirectUri()).'</code>',
+				'Use the "Authorize" button below to complete OAuth flow'
+			];
 		}
 		protected function defineOAuth():void
 		{
@@ -114,11 +142,13 @@
 		}
 		protected function defineActions():void
 		{
-			$this->addAction(
-				'Update Location',
-				[$this, 'handleUpdateLocation'],
-				'update_location',
-			);
+			if ($this->hasOAuthCredentials($this->userID)) {
+				$this->addAction(
+					'Update Location',
+					[$this, 'handleUpdateLocation'],
+					'update_location',
+				);
+			}
 		}
 			public function handleUpdateLocation(array $data):array
 			{
@@ -139,10 +169,15 @@
 
 	protected function getRequestHeaders(): array
 	{
-		// TODO: Implement getRequestHeaders() method.
+
+		return [
+			'Authorization' => 'Bearer ' . $this->getAccessToken(),
+			'Content-Type' => 'application/json',
+			'Accept' => 'application/json'
+		];
 	}
 
-	protected function handleResponse(\WP_Error|array $response, string $method, string $endpoint): array
+	protected function handleResponse(WP_Error|array $response, string $method, string $endpoint): array
 	{
 		if (is_wp_error($response) && $response->get_error_message() === 'rate_limit_exceeded') {
 			return $this->response(false, 'Rate limit exceeded');
@@ -150,19 +185,90 @@
 
 		return match($endpoint) {
 			'https://mybusinessaccountmanagement.googleapis.com/v1/accounts' => $this->handleGetAccountsResponse($response, $method),
+			sprintf('%s/%s/%s/reviews', $this->businessV4, $this->getAccountID()??'', $this->getLocationID()??'') => $this->handleGetReviewResponse($response, $method),
 			sprintf('https://mybusinessbusinessinformation.googleapis.com/v1/%s/locations', $this->getAccountID()??'') => $this->handleGetLocationsResponse($response, $method),
+			sprintf('%s/v1/%s', $this->baseUrl,$this->getLocationID()??'') => $this->handleGetLocationResponse($response, $method),
 			default => $this->response(false, 'Unhandled response for '.$endpoint.' endpoint, with method: '.$method)
 		};
 	}
 
 	protected function refreshAccessToken(OAuthCredentials $credentials): array
 	{
-		// TODO: Implement refreshAccessToken() method.
+		if (empty($this->getClientID()) || empty($this->getClientSecret()) || empty($credentials->refresh_token)) {
+			$this->logError('refreshAccessToken', 'Missing refresh token');
+			return $this->response(false, 'Missing client ID, client Secret, or refresh token');
+		}
+		$response = wp_remote_post(
+			$this->oauth->getToken(),
+			[
+				'timeout'	=> 15,
+				'body'	=> [
+					'grant_type' 	=> 'refresh_token',
+					'refresh_token'	=> $credentials->refresh_token,
+					'client_id'		=>$this->getClientID(),
+					'client_secret'	=>$this->getClientSecret()
+				],
+				'headers'	=> $this->getOAuthRequestHeaders()
+			]
+		);
+		if (is_wp_error($response)) {
+			$this->logError(
+				'refreshAccessToken',
+				'Failed to refresh GMB Token',
+				[
+					'error'	=> $response->get_error_message()
+				]
+			);
+			return $this->response(false, 'Failed to refresh GMB Token');
+		}
+
+		$data = json_decode(wp_remote_retrieve_body($response), true);
+		if (empty($data['access_token'])) {
+			$this->logError('refreshAccessToken', 'No access token in response');
+			return $this->response(false, 'No access token in response');
+		}
+		$credentials = [
+			'access_token'	=> $data['access_token'],
+			'expires_at'	=> time() + (int)($data['expires_in']??3600),
+			// Google usually omits refresh_token on refresh — keep the old one
+			'refresh_token' => $body['refresh_token'] ?? $credentials->refresh_token,
+		];
+		$this->saveCredentials($credentials);
+
+		return $this->response(true, 'Successfully refreshed token');
 	}
 
 	/*********************************************************
 	 * UTILITY
 	*********************************************************/
+	protected function getClientID():string|false
+	{
+		$switch = false;
+		if ($this->userID !== null) {
+			$switch = $this->userID;
+			$this->switchUser(null);
+		}
+
+		$ID =  $this->loadCredentials()['client_id']??false;
+		if ($switch) {
+			$this->switchUser($switch);
+		}
+		return $ID;
+	}
+	protected function getClientSecret():string|false
+	{
+		$switch = false;
+		if ($this->userID !== null) {
+			$switch = $this->userID;
+			$this->switchUser(null);
+		}
+
+		$ID = $this->loadCredentials()['client_secret']??false;
+		if ($switch) {
+			$this->switchUser($switch);
+		}
+		return $ID;
+	}
 	protected function getAccountID():string|false
 	{
 		return $this->loadCredentials()['account_id']??false;
@@ -176,6 +282,31 @@
 		return $stored;
 	}
 
+	protected function getLocation(bool $force = false):?array
+	{
+		$location_name = $this->getLocationID();
+		if (empty($location_name)) {
+			return null;
+		}
+		$params = [
+			'readMask'	=> $this->readMask
+		];
+
+		return $this->getRequest(
+			sprintf(
+				'%s/v1/%s',
+				$this->baseUrl,
+				$location_name,
+			),
+			$params,
+			$force
+		);
+
+	}
+		protected function handleGetLocationResponse(WP_Error|array $response, string $method):?array
+		{
+			return $response??null;
+		}
 	protected function getLocationID():string|false
 	{
 		return $this->loadCredentials()['location_id']??false;
@@ -250,4 +381,326 @@
 			}
 			return $options;
 		}
+
+	protected function setContentTypes(): void
+	{
+		$this->contentTypes = [
+			'menu_item'	=> $this->buildMenuItemContentType(),
+			'post'		=> $this->buildPostContentType(),
+			'event'		=> $this->buildEventContentType(),
+			'offer'		=> $this->buildOfferContentType(),
+		];
+	}
+		protected function buildMenuItemContentType():array
+		{
+			return [
+				'nutritionFacts'	=> [
+					'type'	=> 'repeater',
+					'label'	=> 'Nutrition',
+					'fields'	=> [
+						'name'	=> [
+							'type'	=> 'select',
+							'label'	=> 'Name',
+							'options'	=> [
+								'calories' => 'Calories',
+								'totalFat'	=> 'Total Fat',
+								'cholesterol' => 'Cholesterol',
+								'sodium'	=> 'Sodium',
+								'totalCarbohydrate' => 'Total Carbohydrates',
+								'protein'	=> 'Protein'
+							]
+						],
+						'amount'	=> [
+							'type'	=> 'number',
+							'label'	=> 'Amount',
+						],
+						'unit'	=> [
+							'type' => 'select',
+							'label'	=> 'Unit',
+							'options'	=> [
+								'MILLIGRAM' => 'Milligram',
+								'GRAM'	=> 'Gram',
+								'CALORIE'	=> 'Calorie'
+							]
+						]
+					],
+					'description'	=> 'Recommended. Example: Total Fat: 3g',
+					'section'	=> 'gmb',
+				],
+				'servesNumPeople'	=> [
+					'type'	=> 'number',
+					'label'	=> 'Number of People this Serves',
+					'bulkEdit'	=> true,
+					'section'	=> 'gmb',
+				],
+				'portionSize'	=> [
+					'type'	=> 'text',
+					'label'	=> 'Portion size',
+					'description' => 'Example: 8-piece of nuggets',
+					'bulkEdit'	=> true,
+					'section'	=> 'gmb',
+				],
+				'preparationMethods' => [
+					'type'	=> 'textarea',
+					'label'	=> 'Preparation Methods',
+					'description'	=> 'Specific methods that the food can be prepared in',
+					'bulkEdit'	=> true,
+					'section'	=> 'gmb',
+				],
+				'cuisines'	=> [
+					'type'	=> 'text',
+					'label'	=> 'Cuisines',
+					'description'	=> 'Recommended. The specific cuisine of the food item',
+					'bulkEdit'	=> true,
+					'section'	=> 'gmb',
+				],
+				'spiciness'	=> [
+					'type'	=> 'select',
+					'label'	=> 'Spiciness',
+					'options'	=> [
+						'none'	=> 'None',
+						'mild'	=> 'Mild',
+						'medium'	=> 'Medium',
+						'hot'	=> 'Hot',
+					],
+					'bulkEdit'	=> true,
+					'section'	=> 'gmb',
+				],
+				'allergen'	=> [
+					'type'	=> 'checkbox',
+					'label'	=> 'Allergens',
+					'options'	=> [
+						'dairy'	=> 'Dairy',
+						'egg'	=> 'Egg',
+						'fish'	=> 'Fish',
+						'peanut'	=> 'Peanut',
+						'shellfish'	=> 'Shellfish',
+						'soy'		=> 'Soy',
+						'tree nut'	=> 'Tree Nut',
+						'wheat'		=> 'Wheat'
+					],
+					'description'	=> 'Recommended',
+					'bulkEdit'	=> true,
+					'section'	=> 'gmb',
+				],
+				'dietaryRestriction'	=> [
+					'type'	=> 'checkbox',
+					'label'	=> 'Dietary Restrictions',
+					'description' => 'Recommended.',
+					'options'	=> [
+						'halal' => 'Halal',
+						'kosher'	=> 'Kosher',
+						'organic'	=> 'Organic',
+						'vegan'		=> 'Vegan',
+						'vegetarian'	=> 'Vegetarian',
+						'gluten-free'	=> 'Gluten Free'
+					],
+					'bulkEdit'	=> true,
+					'section'	=> 'gmb',
+				]
+			];
+		}
+		protected function buildPostContentType():array
+		{
+			return [
+				'summary' => [
+					'type' => 'textarea',
+					'label' => 'Post Summary',
+					'required' => true
+				],
+				'language_code' => [
+					'type' => 'text',
+					'label' => 'Language',
+					'default' => 'en-CA'
+				],
+				'media' => [
+					'type' => 'image',
+					'label' => 'Photo',
+				],
+				'call_to_action' => [
+					'type' => 'group',
+					'fields' => [
+						'type' => [
+							'type' => 'select',
+							'options' =>
+								[
+									'BOOK' => 'Book Now',
+									'ORDER' => 'Order Now',
+									'SHOP' => 'Shop Now',
+									'LEARN_MORE' => 'Learn More',
+									'SIGN_UP' => 'Sign up',
+									'CALL' => 'Call'
+								]
+						],
+						'url' => [
+							'type' => 'url',
+							'label' => 'Action URL'
+						]
+					]
+				]
+			];
+		}
+		protected function buildEventContentType():array
+		{
+			return [
+				'summary' => [
+					'type' => 'textarea',
+					'label' => 'Event Description',
+					'required' => true
+				],
+				'event' => [
+					'title' => [
+						'type' => 'text',
+						'label' => 'Event Title',
+						'required' => true
+					],
+					'start_date' => [
+						'type' => 'date',
+						'label' => 'Start Date',
+						'required' => true
+					],
+					'end_date' => [
+						'type' => 'date',
+						'label' => 'End Date'
+					],
+					'start_time' => [
+						'type' => 'time',
+						'label' => 'Start Time'
+					],
+					'end_time' => [
+						'type' => 'time',
+						'label' => 'End Time'
+					]
+				],
+				'media' => [
+					'type' => 'image',
+					'label' => 'Event Photo'
+				]
+			];
+		}
+		protected function buildOfferContentType():array
+		{
+			return [
+				'summary' => [
+					'type' => 'textarea',
+					'label' => 'Offer Description',
+					'required' => true
+				],
+				'offer' => [
+					'coupon_code' => [
+						'type' => 'text',
+						'label' => 'Coupon Code'
+					],
+					'redeem_online_url' => [
+						'type' => 'url',
+						'label' => 'Redemption URL'
+					],
+					'terms_conditions' => [
+						'type' => 'textarea',
+						'label' => 'Terms & Conditions']
+				],
+				'media' => ['type' => 'image', 'label' => 'Offer Photo']
+			];
+		}
+	protected function formatForService(int $itemID, string $type = 'post'): array
+	{
+		// TODO: Implement formatForService() method.
+	}
+
+
+	public function getReviews(int $number = 5):array
+	{
+		if (!$this->getLocationID() || !$this->getAccountID()){
+			return [];
+		}
+		return $this->cache->remember(
+			['reviews', $this->getLocationID(), $number],
+			function() use ($number) {
+				try {
+					return $this->getRequest(
+						sprintf(
+							"%s/{$this->getAccountID()}/{$this->getLocationID()}/reviews",
+							$this->businessV4
+						),
+						[
+							'orderBy'	=> 'updateTime desc'
+						],
+					);
+				} catch (Exception $e) {
+					$this->logError(
+						'getReviews',
+					'Failed to fetch Reviews',
+					[
+						'error'	=> $e->getMessage()
+					]);
+					return [];
+				}
+			}
+		);
+	}
+		protected function handleGetReviewResponse(WP_Error|array $response, string $method):array
+		{
+			return $response;
+		}
+
+		/**********************************************************************
+		 * REVIEW UTILITY
+		**********************************************************************/
+		/**
+		 * Get the URL to view all Google reviews for the current location
+		 * @return string|null The reviews viewing URL or null if not available
+		 */
+		public function getReviewsViewUrl():?string
+		{
+			$location = $this->getLocation();
+			if (!$location) {
+				return null;
+			}
+
+			if (!empty($location['metadata']['mapsUrl']??'')) {
+				return $location['metadata']['mapsUrl'];
+			}
+
+			if (!empty($location['metadata']['placeId']??'')) {
+				return sprintf(
+					'https://search.google.com/local/reviews?placeid=%s',
+					urlencode($location['metadata']['placeId'])
+				);
+			}
+
+			return null;
+		}
+
+	/**
+	 * Get the URL to leave a review for the current location
+	 * @return string|null The review URL or null if not available
+	 */
+		public function getReviewUrl():?string
+		{
+			$location = $this->getLocation();
+			if (!$location) {
+				return null;
+			}
+
+			if (!empty($location['metadata']['placeId']??'')) {
+				return sprintf(
+					'https://search.google.com/local/writereview?placeid=%s',
+					urlencode($location['metadata']['placeId'])
+				);
+			}
+
+			if (!empty($location['metadata']['mapsUrl']??'')) {
+				return sprintf(
+					'%s/reviews',
+					$location['metadata']['mapsUrl']
+				);
+			}
+
+			return null;
+		}
+
+
+	/**************************************************************************
+	 * FOOD MENU HELPERS
+	**************************************************************************/
 }

--
Gitblit v1.10.0