Our current apps no longer compatible

Our current apps no longer compatible

FutureAndAHope
Shopify Partner
39 1 16

I develop apps for a single store, so basically custom apps. Our previous developer did the authentication needed for the apps to show in the Shopify Admin. But he has since left our company. These apps are some years old and we are now getting a message in our partners account (where we have the apps) that says the following:

 

Our records indicate that one or more of your apps have not been updated to meet Shopify’s requirements to load in admin.shopify.com, and will need to be updated by September 20, 2024 in order to continue working in Shopify.

 

But that is as detailed as it gets, there is no indication as to which apps are effected. We have a PHP backend for the apps, and need to retain that. Could you give me some direction as to what the current authentication flow is and how to use it. 

Reply 1 (1)

FutureAndAHope
Shopify Partner
39 1 16

Here is the code we are currently using. What would be the best way to upgrade.

 

<?php
    defined('BASEPATH') OR exit('No direct script access allowed');
    class Auth extends CI_Controller{
        
        public function __construct() {
            parent::__construct();
			$this->load->model('admin/freesample_model', 'freesample_model');            
            $this->load->helper('array');
        } 
            
        public function access() {
            
            $shop = $this->config->item('shopify_store_url');

            if(isset($shop)) {
                $this->session->set_userdata($shop);
            }

            if(($this->session->userdata('access_token'))) {  
                $data['new_customers_count'] = $this->freesample_model->count_all_customers();
                $data['all_freesample_products'] = $this->freesample_model->count_all_products();          
                $data['all_freesample_orders'] = $this->freesample_model->count_all_orders();
                $data['view'] = 'dashboard';
                $this->load->view('layout',$data);
            } else {
                $this->auth($shop);
            }
        }

        public function auth($shop) {

            $data = array(
                'API_KEY' => $this->config->item('shopify_api_key'),
                'API_SECRET' => $this->config->item('shopify_secret'),
                'SHOP_DOMAIN' => $shop,
                'ACCESS_TOKEN' => ''
            );
            $this->load->library('Shopify' , $data); //load shopify library and pass values in constructor

            $scopes = array('read_products','write_products', 'read_customers', 'write_customers', 'read_orders', 'write_orders', 'read_themes', 'write_themes', 'read_content', 'write_content'); //what app can do
            $redirect_url = $this->config->item('redirect_url'); //redirect url specified in app setting at shopify
            $paramsforInstallURL = array(
                'scopes' => $scopes,
                'redirect' => $redirect_url
            );

            $permission_url = $this->shopify->installURL($paramsforInstallURL);
            
            $this->load->view('auth/escapeIframe', ['installUrl' => $permission_url]);
            
        }

        public function authCallback(){
            $code = $this->input->get('code');
            $shop = $this->input->get('shop');

            if(isset($code)){
                $data = array(
                'API_KEY' => $this->config->item('shopify_api_key'),
                'API_SECRET' => $this->config->item('shopify_secret'),
                'SHOP_DOMAIN' => $shop,
                'ACCESS_TOKEN' => ''
            );
                $this->load->library('Shopify' , $data); //load shopify library and pass values in constructor
            }

            $accessToken = $this->shopify->getAccessToken($code);
            $this->session->set_userdata(['shop' => $shop , 'access_token' => $accessToken]);

            redirect('https://'.$shop.'/admin/apps/vpa-my-admin');
        }

        
    }

Ans

<?php
defined('BASEPATH') OR exit('No direct script access allowed');


class Shopify {

    private $_API = array();
    private $_KEYS  = array('API_KEY' , 'API_SECRET', 'ACCESS_TOKEN' , 'SHOP_DOMAIN');

    
    public function __construct($data = FALSE) {
        if(is_array($data)) {
			$this->setup($data);
			
        }
    }

    public function setup($data=array()) {
        foreach ($this->_KEYS as $key ) {
            if(array_key_exists($key,$data)){
				$this->_API[$key] = $this->verifySetup($key , $data[$key]);
				
            }
        }
    }

    private static function verifySetup($key='', $value=''){
        $value = trim($value);
        switch($key) {
            case 'SHOP_DOMAIN':
                  preg_match('/(https?:\/\/)?([a-zA-Z0-9\-\.])+/', $value, $matched);
                  return $matched[0];
                  break;
            default:
				  return $value;
				  
        }
    }
    
    public function installURL($data = array()) {
		// https://{shop}.myshopify.com/admin/oauth/authorize?client_id={api_key}&scope={scopes}&redirect_uri={redirect_uri}
		return 'https://' . $this->_API['SHOP_DOMAIN'] . '/admin/oauth/authorize?client_id=' . $this->_API['API_KEY'] . '&scope=' . implode(',', $data['scopes']) . (!empty($data['redirect']) ? '&redirect_uri=' . urlencode($data['redirect']) : '');
	}
    /**
	 * Verifies data returned by OAuth call
	 * @param array|string $data
	 * @Return bool
	 * @throws \Exception
	 */
	public function verifyRequest($data = NULL, $bypassTimeCheck = FALSE) {
		$da = array();
		if (is_string($data)) {
			$each = explode('&', $data);

			foreach($each as $e) {
				list($key, $val) = explode('=', $e);
				$da[$key] = $val;

			}
		} elseif (is_array($data)) {
			$da = $data;

		} else {
			throw new \Exception('Data passed to verifyRequest() needs to be an array or URL-encoded string of key/value pairs.');

		}
		// Timestamp check; 1 hour tolerance
		if (!$bypassTimeCheck) {
			if (($da['timestamp'] - time() > 3600)) {
				throw new \Exception('Timestamp is greater than 1 hour old. To bypass this check, pass TRUE as the second argument to verifyRequest().');

			}
		}

		if (array_key_exists('hmac', $da)) {
			// HMAC Validation
			$queryString = http_build_query(array('code' => $da['code'], 'shop' => $da['shop'], 'timestamp' => $da['timestamp']));
			$match = $da['hmac'];
			$calculated = hash_hmac('sha256', $queryString, $this->_API['API_SECRET']);

		} else {
			// MD5 Validation, to be removed
			$queryString = http_build_query(array('code' => $da['code'], 'shop' => $da['shop'], 'timestamp' => $da['timestamp']), NULL, '');
			$match = $da['signature'];
			$calculated = md5($this->_API['API_SECRET'] . $queryString);

		}
		return $calculated === $match;

	}

    /**
	 * Calls API and returns OAuth Access Token, which will be needed for all future requests
	 * @param string $code
	 * @Return mixed
	 * @throws \Exception
	 */
	public function getAccessToken($code = '') {
		$dta = array('client_id' => $this->_API['API_KEY'], 'client_secret' => $this->_API['API_SECRET'], 'code' => $code);
		$data = $this->call(['METHOD' => 'POST', 'URL' => 'https://' . $this->_API['SHOP_DOMAIN'] . '/admin/oauth/access_token', 'DATA' => $dta], FALSE);
		return $data->access_token;

	}

    /**
	 * Checks that data provided is in proper format
	 * @example Checks for presence of /admin/ in URL
	 * @param array $userData
	 * @Return array
	 */
	private function setupUserData($userData = array()) {
		$returnable = array();
		foreach($userData as $key => $value) {
			switch($key) {
				case 'URL':
					// Remove shop domain
					$url = str_replace($this->_API['SHOP_DOMAIN'], '', $value);
					// Verify it contains /admin/
					if (strpos($url, '/admin/') !== 0) {
						$url = str_replace('//', '/', '/admin/' . preg_replace('/\/?admin\/?/', '', $url));
					}

					$returnable[$key] = $url;
					break;

				default:
					$returnable[$key] = $value;
			}
		}
		return $returnable;
	}

    /**
	 * Executes the actual cURL : based on $userData
	 * @param array $userData
	 * @Return mixed
	 * @throws \Exception
	 */
    public function call($userData = array(), $verifyData = TRUE) {		
	    if ($verifyData) {
		    foreach ($this->_KEYS as $k) {				
			    if ((!array_key_exists($k, $this->_API)) || (empty($this->_API[$k]))) {
					throw new \Exception($k . ' must be set.');
					
			    }
		    }
	    }

	    $defaults = array(
		    'CHARSET'       => 'UTF-8',
		    'METHOD'        => 'GET',
		    'URL'           => '/',
			'HEADERS'       => array(),
	        'DATA'          => array(),
	        'FAILONERROR'   => TRUE,
	        'RETURNARRAY'   => FALSE,
	        'ALLDATA'       => FALSE
	    );

	    if ($verifyData) {
			$request = $this->setupUserData(array_merge($defaults, $userData));
			
	    } else {
			$request = array_merge($defaults, $userData);
			
	    }

	    // Send & accept JSON data
	    $defaultHeaders = array();
	    $defaultHeaders[] = 'Content-Type: application/json; charset=' . $request['CHARSET'];
		$defaultHeaders[] = 'Accept: application/json';
		
	    if (array_key_exists('ACCESS_TOKEN', $this->_API)) {
			$defaultHeaders[] = 'X-Shopify-Access-Token: ' . $this->_API['ACCESS_TOKEN'];
			
	    }

        $headers = array_merge($defaultHeaders, $request['HEADERS']);


	    if ($verifyData) {
			$url = 'https://' . $this->_API['API_KEY'] . ':' . $this->_API['ACCESS_TOKEN'] . '@' . $this->_API['SHOP_DOMAIN'] . $request['URL'];
			
	    } else {
			$url = $request['URL'];
			
	    }

	    // cURL setup
        $ch = curl_init();
        $options = array(
            CURLOPT_RETURNTRANSFER  => TRUE,
            CURLOPT_URL             => $url,
            CURLOPT_HTTPHEADER      => $headers,
            CURLOPT_CUSTOMREQUEST   => strtoupper($request['METHOD']),
            CURLOPT_ENCODING        => '',
            CURLOPT_USERAGENT       => 'RocketCode Shopify API Wrapper',
            //CURLOPT_FAILONERROR     => $request['FAILONERROR'],
            //CURLOPT_VERBOSE         => $request['ALLDATA'],
            CURLOPT_HEADER          => 1
        );

	    // Checks if DATA is being sent
	    if (!empty($request['DATA'])) {
		    if (is_array($request['DATA'])) {
				$options[CURLOPT_POSTFIELDS] = json_encode($request['DATA']);
				
		    } else {
			    // Detect if already a JSON object
			    json_decode($request['DATA']);
			    if (json_last_error() == JSON_ERROR_NONE) {
					$options[CURLOPT_POSTFIELDS] = $request['DATA'];
					
			    } else {
					throw new \Exception('DATA malformed.');
					
			    }
		    }
	    }

        curl_setopt_array($ch, $options);

        $response = curl_exec($ch);
        $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);

        // Data returned
        $result = json_decode(substr($response, $headerSize), $request['RETURNARRAY']);

        // Headers
        $info = array_filter(array_map('trim', explode("\n", substr($response, 0, $headerSize))));

        foreach($info as $k => $header) {
	        if (strpos($header, 'HTTP/') > -1) {
                $_INFO['HTTP_CODE'] = $header;
				continue;
				
            }

            list($key, $val) = explode(':', $header);
			$_INFO[trim($key)] = trim($val);
			
        }

        // cURL Errors
        $_ERROR = array('NUMBER' => curl_errno($ch), 'MESSAGE' => curl_error($ch));

        curl_close($ch);

	    if ($_ERROR['NUMBER']) {
			throw new \Exception('ERROR #' . $_ERROR['NUMBER'] . ': ' . $_ERROR['MESSAGE']);
			
	    }

	    // Send back in format that user requested
	    if ($request['ALLDATA']) {
		    if ($request['RETURNARRAY']) {
			    $result['_ERROR'] = $_ERROR;
			    $result['_INFO'] = $_INFO;
		    } else {
			    $result->_ERROR = $_ERROR;
			    $result->_INFO = $_INFO;
		    }
			return $result;
			
	    } else {
			return $result;
			
	    }

    }
}