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