service_name = 'gmb';
$this->title = 'Google My Business';
$this->icon = 'storefront';
$this->canSync = [
'initial' => true,
'update' => true,
'delete' => true,
];
$this->supportsWebp = false;
$this->baseUrl = 'https://mybusinessbusinessinformation.googleapis.com';
$this->businessV4 = 'https://mybusiness.googleapis.com/v4';
$this->defineFields();
$this->defineInstructions();
$this->defineOAuth();
$this->defineActions();
parent::__construct();
}
protected function defineFields():void
{
$client_id = $this->addField(
'client_id',
'OAuth Client ID',
'text',
'manage_options'
);
$client_id->setRequired();
$client_id->setHint('From your Google Cloud Console OAuth credentials.');
$secret = $this->addField(
'client_secret',
'OAuth Client Secret',
'text',
'manage_options',
);
$secret->setSubType('password');
$secret->setRequired();
if ($this->getAccessToken()) {
$accs = $this->addField('accounts', 'Accounts', 'select', [$this, 'testUserCanConnect']);
$accs->setHidden();
$accs->setOptions($this->getAccountOptions());
$acc = $this->addField('account_id', 'Set Account', 'select', [$this, 'testUserCanConnect']);
$acc->setOptions($this->loadCredentials()['accounts']??[]);
$acc->setHint('Select the account to connect to.');
if ($this->getAccountID()) {
$locs = $this->addField('locations', 'Locations', 'select', [$this, 'testUserCanConnect']);
$locs->setHidden();
$locs->setOptions($this->getLocationOptions());
$loc = $this->addField('location_id', 'Set Location', 'select', [$this, 'testUserCanConnect']);
$loc->setOptions($this->loadCredentials()['locations']??[]);
$loc->setHint('Select the location to connect to.');
}
}
}
public function testUserCanConnect():bool
{
$user = get_current_user_id();
return $this->userCanConnect($user);
}
protected function defineInstructions():void
{
$this->instructions = [
'Go to Google Cloud Console',
'Create OAuth 2.0 Client ID credentials',
'Enable Google My Business API',
'Add redirect URI: '.esc_html($this->getRedirectUri()).'',
'Use the "Authorize" button below to complete OAuth flow'
];
}
protected function defineOAuth():void
{
$this->oauth = new OAuthURLs(
'https://accounts.google.com/o/oauth2/v2/auth',
'https://oauth2.googleapis.com/token',
'https://oauth2.googleapis.com/revoke'
);
$this->oathScopes = [
'https://www.googleapis.com/auth/business.manage',
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/plus.business.manage'
];
}
protected function defineActions():void
{
$this->addAction(
'Update Location',
[$this, 'handleUpdateLocation'],
'update_location',
);
}
public function handleUpdateLocation(array $data):array
{
if (empty($data['location_id']??'')) {
return $this->response(false, 'No location selected');
}
$credentials = [
'location_id' => sanitize_text_field($data['location_id'])
];
return $this->saveCredentials($credentials, false);
}
protected function initialize(): void
{
// TODO: Implement initialize() method.
}
protected function getRequestHeaders(): array
{
return [
'Authorization' => 'Bearer ' . $this->getAccessToken(),
'Content-Type' => 'application/json',
'Accept' => 'application/json'
];
}
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');
}
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
{
}
/*********************************************************
* UTILITY
*********************************************************/
protected function getAccountID():string|false
{
return $this->loadCredentials()['account_id']??false;
}
protected function getAccountOptions(bool $force = false):array
{
$stored = $this->loadCredentials()['accounts']??[];
if ($force || empty($stored)) {
$stored = $this->getAccounts();
}
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;
}
protected function getLocationOptions(bool $force = false):array
{
$stored = $this->loadCredentials()['locations']??[];
if ($force || empty($stored)) {
$stored = $this->getLocations();
}
return $stored;
}
protected function getLocations():array|false
{
if (!$this->getAccessToken() || !$this->getAccountID()) {
return false;
}
try {
return $this->getRequest(
sprintf(
'/v1/%s/locations',
$this->getAccountID()
),
[
'readMask' => $this->readMask
]
);
} catch (Exception $e)
{
$this->logError('getLocations', 'Could not get locations: '.$e->getMessage());
return [];
}
}
protected function handleGetLocationsResponse(array $response, string $method):array
{
$locations = [];
if (isset($response['locations'])) {
foreach ($response['locations'] as $loc) {
if (isset($loc['name']) && isset($loc['title'])) {
$locations[$loc['name']] = $loc['title'];
}
}
}
return $locations;
}
protected function getAccounts(bool $force = false):array|false
{
if (!$this->getAccessToken()) {
return false;
}
return $this->getRequest(
'https://mybusinessaccountmanagement.googleapis.com/v1/accounts',
[],
$force
);
}
protected function handleGetAccountsResponse(array $response, string $method):array
{
$accounts = [];
if (isset($response['accounts']) && is_array($response['accounts'])) {
$accounts = $response['accounts'];
} elseif (is_array($response) && isset($response['name'])) {
$accounts = [$response];
}
$options = [];
foreach ($accounts as $account) {
if (isset($account['name']) && isset($account['accountName'])) {
$options[$account['name']] = $account['accountName'];
}
}
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
**************************************************************************/
}