Jake Vanderwerf
2026-09-16 a239ef33ec489ce6d0f25ac2f256df0ac54a7e50
inc/integrations/services/Square.php
@@ -4,7 +4,7 @@
use Exception;
use JVBase\integrations\Admin;
use JVBase\integrations\APIEndpoint;
use JVBase\integrations\Instance;
use JVBase\Instance;
use JVBase\integrations\Integrations;
use JVBase\integrations\OAuth;
use JVBase\integrations\OAuthCredentials;
@@ -14,10 +14,13 @@
use JVBase\integrations\SyncFrom;
use JVBase\integrations\SyncTo;
use JVBase\integrations\Webhooks;
use JVBase\managers\Dashboard\Dashboard;
use JVBase\managers\queue\executors\IntegrationExecutor;
use JVBase\managers\queue\Queue;
use JVBase\managers\queue\TypeConfig;
use JVBase\meta\Sanitizer;
use JVBase\registrar\Registrar;
use JVBase\ui\CRUDSkeleton;
use WP_Error;
if (!defined('ABSPATH')) {
@@ -36,18 +39,16 @@
      Webhooks;
   protected string $orderPostType = '_square_order';
   protected string $apiVersion = '2026-07-15';
   public bool $hasBatchCreate = true;
   public bool $hasBatchUpdate = true;
   public bool $hasBatchDelete = true;
   public array $apiEndpoints = [
   ];
   protected function __construct()
   {
      $this->service_name = 'square';
      $this->title = 'Square';
      $this->icon = 'square-logo';
      $this->apiVersion = '2026-07-15';
      $this->supportsWebp = false;
      $this->canSync = [
@@ -55,6 +56,9 @@
         'update' => true,
         'delete' => true,
      ];
      $this->hasBatchCreate = true;
      $this->hasBatchUpdate = true;
      $this->hasBatchDelete = true;
      $this->defineFields();
      $this->defineInstructions();
@@ -89,8 +93,8 @@
         $locs->setHidden();
         $locs->setOptions($this->getLocationOptions());
      $loc = $this->addField('location_id', 'Default Location', 'select', 'manage_options');
         $loc->setOptions($this->loadCredentials()['locations']);
      $loc = $this->addField('location_id', 'Set Location', 'select', 'manage_options');
         $loc->setOptions($this->loadCredentials()['locations']??[]);
         $loc->setHint('Select default Square location for transactions');
   }
@@ -162,6 +166,37 @@
      add_action(BASE.'dashboard_page_orders', [$this, 'renderDashPage']);
   }
      public function addDashboardPages():void
      {
         $page = Dashboard::getInstance()->addPage('Your Orders', 'orders', 'receipt');
         $page->setScripts(['jvb-crud']);
      }
   public function renderDashPage():void
   {
      $this->determineUser();
      $crud = new CRUDSkeleton();
      $crud->icon('receipt');
      $crud->title('Your Orders','Here you can see your past orders, and reorder from there if you\'d like');
      $crud->content($this->orderPostType, 'Order', 'Orders');
      $crud->addSearch();
      $crud->addCapabilities(['view']);
      $crud->setEmptyState(sprintf(
         '<div class="empty-state">
            <h3>%sNothing here%s</h3>
            <p>It doesn\'t look like you have any orders yet.</p>
            <p>Head on over to <a href="%s">our menu</a> and make your first order!</p>
         </div>',
         jvbDashIcon($crud->getIcon()),
         jvbDashIcon($crud->getIcon()),
         get_post_type_archive_link(BASE.'menu_item')
      ));
      $crud->render();
   }
   protected function initialize(): void
   {
      $this->defineEndpoints();
@@ -219,10 +254,15 @@
   protected function refreshAccessToken(OAuthCredentials $credentials): array
   {
      if (!$this->getClientID() || !$this->getClientSecret())
      if (!$this->getClientID() || !$this->getClientSecret() || empty($credentials->refresh_token)) {
         $this->logError('refreshAccessToken', 'Missing client credentials or refresh token');
         return $this->response(false, 'No client ID or client secret provided');
      }
      $response = wp_remote_post(
         $this->oauth->getToken(),
         [
            'timeout'   => 15,
            'body'   => [
               'client_id'    => $this->getClientID(),
               'client_secret' => $this->getClientSecret(),
@@ -244,16 +284,18 @@
      }
      $data = json_decode(wp_remote_retrieve_body($response));
      if (isset($data['access_token'])) {
         $credentials = [
            'access_token' =>  $data['access_token'],
            'expires_at'   => time() + ($data['expires_in'] ?? 2592000) // 30 days default
         ];
         $this->saveCredentials($credentials);
         return $this->response(true, 'Successfully refreshed access token');
      if (empty($data['access_token'])) {
         $this->logError('refreshAccessToken', 'No access_token in refresh response', ['body' => $data]);
         return $this->response(false, 'No Access Token in Response');
      }
      return $this->response(false, 'Something went wrong that wasn\'t caught - could not refresh access token');
      $credentials = [
         'access_token'  => $data['access_token'],
         'expires_at'    => strtotime($data['expires_at'] ?? '') ?: (time() + 2592000),
         'refresh_token' => $data['refresh_token'] ?? $credentials->refresh_token,
      ];
      $this->saveCredentials($credentials);
      return $this->response(true, 'Successfully refreshed access token');
   }
   protected function getRequestHeaders(): array
@@ -265,21 +307,18 @@
      ];
   }
   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');
      }
      $endpoint = str_replace($this->getBaseURL(), '', $endpoint);
      switch ($endpoint) {
         case '/v2/customers/search':
            return $this->handleEmailSearchResponse($response, $method);
         case '/v2/customers':
            return $this->handleCreateUserResponse($response, $method);
         case '/v2/locations':
            return $this->handleGetLocationResponse($response, $method);
      }
      return $this->response(false, 'Unhandled Response for '.$endpoint.' endpoint, with method: '.$method);
      return match ($endpoint) {
         '/v2/customers/search' => $this->handleEmailSearchResponse($response, $method),
         '/v2/customers' => $this->handleCreateUserResponse($response, $method),
         '/v2/locations' => $this->handleGetLocationResponse($response, $method),
         default => $this->response(false, 'Unhandled Response for ' . $endpoint . ' endpoint, with method: ' . $method),
      };
   }
   protected function setContentTypes(): void
@@ -720,7 +759,7 @@
   ********************************************************************/
   protected function registerAdditionalQueueTypes(IntegrationExecutor $executor): void
   {
      $queue = JVB()->queue();
      $queue = Queue::getInstance();
      $queue->registry()->register(
         self::$syncCustomer,
         new TypeConfig(
@@ -865,7 +904,7 @@
   {
      return $this->loadCredentials()['location_id']??false;
   }
   protected function getLocations():array|false
   public function getLocations():array|false
   {
      if (!$this->getAccessToken()) {
         return false;
@@ -914,10 +953,7 @@
         ? 'https://connect.squareup.com'
         : 'https://connect.squareupsandbox.com';
   }
   protected function getAccessToken():string
   {
      return $this->loadCredentials()['access_token']??'';
   }
   /***************************************************************
    * ACTIONS
   ***************************************************************/
@@ -959,7 +995,17 @@
      return $this->contentTypes[$content_type];
   }
   public function handleImportFromSquare():array
   {
      return $this->response(true, 'Did a thing');
   }
   public function handleSyncToSquare():array
   {
      return $this->response(true, 'Did a thing');
   }
}