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 **************************************************************************/ }