| | |
| | | use JVBase\integrations\OAuthURLs; |
| | | use JVBase\integrations\Requests; |
| | | use JVBase\integrations\SyncTo; |
| | | use JVBase\integrations\SyncToPost; |
| | | use JVBase\integrations\UserConnection; |
| | | use WP_Error; |
| | | |
| | | |
| | | if (!defined('ABSPATH')) { |
| | |
| | | |
| | | class GoogleMyBusiness extends Integrations |
| | | { |
| | | use Admin, Instance, OAuth, Requests, UserConnection, SyncTo; |
| | | 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'; |
| | |
| | | ]; |
| | | $this->supportsWebp = false; |
| | | |
| | | $this->baseUrl = 'https://mybusinessbusinessinformation.googleapis.com'; |
| | | $this->businessV4 = 'https://mybusiness.googleapis.com/v4'; |
| | | |
| | | $this->defineFields(); |
| | | $this->defineInstructions(); |
| | | $this->defineOAuth(); |
| | |
| | | |
| | | 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 |
| | |
| | | |
| | | 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. |
| | | |
| | | } |
| | | |
| | | /********************************************************* |
| | |
| | | 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 setContentTypes(): void |
| | | { |
| | | // TODO: Implement setContentTypes() method. |
| | | $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 |
| | | **************************************************************************/ |
| | | } |