Integrate EMP EDD Plugin Updater with Ajax
You sell plugins on EMP (Elegant Marketplace) and don’t use the many advantages of the “EDD_SL_Plugin_Updater” class yet? Then this post is surely interesting for you. In this post you will see how to create an input field for the license key directly below the plugin entry in the plugin index and how to handle activation and deactivation using Ajax.
If you are not yet sure about the advantages of the “EDD_SL_Plugin_Updater” class, here are some arguments:
- You can handle license keys with it.
- It’s very easy to integrate into your plugins
- You can manage the display in the update info via readme.txt
- You can integrate images into the update info as you know it from wp.org
- It is compatible with a fixed activation limit and more
I explain the process here with the popular plugin “Divi – Filterable Blog Module”, but I provide a more general version via Github at the end of the post. This is what the input field looks like after it has been implemented:
To add the field behind the plugin entry, use the hook “‘after_plugin_row'”. We hook into these and check by means of the first parameter whether it is our plugin. In my case the following string is passed there: “divi-filterable-blog-module/divi-filterable-blog-module.php”
Since the domain was defined with “divi-filterable-blog-module”, you can easily check with it:
<?php // check in "current_screen" if we're in the plugin index add_action( 'current_screen', function( $screen ) { if ( 'plugins' == $screen->base ) : // DFBM() references a singletone object in which all constant values can also be retrieved $slug = DFBM()->prefix() . '-plugins'; // If there is no license key, show the field if ( ! $this->licenseKey ) add_action( 'after_plugin_row', [ $this, 'licenseField' ] ); else add_filter( 'plugin_action_links_' . plugin_basename( DFBM()->file() ), [ $this, 'removeLink' ] ); // A JS file to execute the Ajax instructions later ( new dfbmControllerEnqueue )->enqueueScript( $slug, [ 'jquery' ] ); wp_localize_script( $slug, DFBM()->prefix() . 'PHP', $this->localize() ); endif; }); // end add_action // Show the field /** * Add the license field * * @since 1.0.10 */ public function licenseField( $plugin ) { // $plugin == "divi-filterable-blog-module/divi-filterable-blog-module.php" if ( false !== strpos( $plugin, DFBM()->domain() ) ) : $prefix = DFBM()->prefix(); $text01 = esc_html__( 'Enter Your License Key for future updates', DFBM()->domain() ); $text02 = esc_html__( 'Submit', DFBM()->domain() ); $text03 = esc_html__( 'You can get it ', DFBM()->domain() ); $text04 = esc_html__( 'here.', DFBM()->domain() ); echo <<<FORM </tr><tr class="plugin-update-tr"> <td colspan="3" class="plugin-update" style="border-left: 4px solid #00a0d2"> <div class="update-message" style="margin-top:5px;margin-bottom:5px;"> <form action="" method="POST"> <input type="hidden" name="{$prefix}_license" value="license_key"> <div class="about-text" style="display:inline-block"> <div> <input id="{$prefix}-license-field" type="text" class="regular-text" id="license_key" value="" name="license_key" placeholder="{$text01}" style="min-height:30px;"> </div> </div> <div style="display:inline-block"> <button id="{$prefix}-license-submit" class="button button-primary" type="submit" value="Submit" name="_submit" style="margin-top:-6px;border-radius:0">{$text02}</button> </div> </form> <div style="display:inline-block;margin-left:5px"> <span style="margin-right:5px">-</span><span>{$text03}</span><a href="https://elegantmarketplace.com/checkout/purchase-history/" target="_blank">{$text04}</a> </div> </div> </td> FORM; endif; } // end licenseField /** * Link to remove the license * * @since 1.0.10 */ public function removeLink( $items ) { $items[] = '<a id="' . DFBM()->prefix() . '-remove-license' . '" href="#">' . esc_html__( 'Remove License', DFBM()->domain() ) . '</a>'; return $items; } // end removeLink
Now the input field has already been inserted
And you’ve probably noticed the last method: “removeLink”. This is used if the user wants to remove his license for a page. After entering a license key, it looks like this:
Setting the hooks for the Ajax requests
Now we have implemented the input field and the remove link, taking into account whether a license key is available or not. Now we need the appropriate hooks to accept the Ajax requests:
<?php if ( $this->licenseKey ) : $this->args->args['license'] = $this->licenseKey; add_action( 'wp_ajax_' . DFBM()->prefix() . '_deactivate', [ $this, 'deactivateLicense' ] ); else : add_action( 'wp_ajax_' . DFBM()->prefix() . '_activate', [ $this, 'activateLicense' ] ); endif;
With this we can now receive the requests via the JavaScript events and trigger the activation or deactivation of the license:
(function($){$(function(){ class dfbmUpdater { setProperties() { // Set the color values for the user feedback and accept the values from the localization this.red = '#fff0f0'; this.green = '#f1fff0'; this.localize = dfbmPHP; } // end setProperties constructor() { this.setProperties(); this.bindEvents(); } // end constructor // This colors the input field according to the response // and inserts the corresponding feedback as placeholder resp( color, message ) { return this.field .css( 'background', color ) .attr( 'value', '' ) .attr( 'placeholder', message ); } // The handler for Ajax requests ajax( action, key, self, that = this ) { let data = { nonce : self.localize.nonce, action : action, license : key, } data[action] = true; $.ajax( { data : data, type : 'post', dataType : 'json', url : self.localize.url, success: function( data ) { if ( self.localize.prefix + '_activate' == action ) { if ( 'success' in data ) { self.resp.apply( that, [ self.green, data.success ] ); setTimeout( () => { $(that).closest( 'div.update-message' ).slideUp( 800, function() { $(this).closest( 'tr' ).remove(); location.reload(); }); }, 3000 ); } else self.resp.apply( that, [ self.red, data.failed ] ); } else { if ( 'delete' in data ) { alert( data.delete ); location.reload(); } else alert( data.failed ); } }, error: function( data ) { alert( self.localize.error ); location.reload(); } }); } // end ajax bindEvents( self = this ) { // The Event Handler for license activation (Submit button) $( '#' + self.localize.prefix + '-license-submit' ).on( 'click', function( e ) { e.preventDefault(); e.stopImmediatePropagation(); this.field = $( '#' + self.localize.prefix + '-license-field' ); this.val = this.field.val(); // The EMP license keys are all 32 characters long if ( this.val && 32 == this.val.length ) { // Valid request self.ajax.apply( this, [ self.localize.prefix + '_activate', this.val, self ] ); } else // Invalid request self.resp.apply( this, [ self.red, self.localize.valid, self ] ); }); // The Event Handler for license deactivation (Remove link) $( '#' + self.localize.prefix + '-remove-license' ).on( 'click', function( e ) { e.preventDefault(); self.ajax.apply( this, [ self.localize.prefix + '_deactivate', self.localize.key, self ] ); }); } // end bindEvents } // end class dfbmUpdater new dfbmUpdater; });}(jQuery));
This is what happens when an incorrect key is entered:
This is what it looks like when the license has been accepted. The field then stops for 3 seconds, after which it is hidden and removed:
If no license key has been entered and an update is available, it cannot be performed:
If you set the meta data in the download settings using a readme.txt file and place banners, the update info looks quite similar to the original wp.org plugins:
The activation and deactivation methods behave mainly like those from the EDD demo files, but have been adapted to handle the Ajax requests. The whole class looks like this:
<?php /** * Divi - Filterable Blog Module - Update Initialize * * @since 1.0.0 */ if ( ! class_exists( 'dfbmUpdateInitialize' ) ) { class dfbmUpdateInitialize { /** * Define properties * * @since 1.0.10 */ private $args; private $licenseKey; /** * Constructor * * @since 1.0.0 */ public function __construct() { /** * Initialize * * @since 1.0.10 */ if ( is_admin() ) // We just need it in the admin area $this->initialize(); } // end constructor /** * Initialize * * @since 1.0.10 */ public function initialize() { // Get the args for the EDD Updater $this->args = DFBM()->updater(); // check if a license exist $this->licenseKey = ( new dfbmModelGet )->option( DFBM()->prefix() . '_license_key' ); // Register the Ajax request handlers if ( $this->licenseKey ) : // Add the license key to the args $this->args->args['license'] = $this->licenseKey; add_action( 'wp_ajax_' . DFBM()->prefix() . '_deactivate', [ $this, 'deactivateLicense' ] ); else : add_action( 'wp_ajax_' . DFBM()->prefix() . '_activate', [ $this, 'activateLicense' ] ); endif; // Initiate the updater add_action( 'admin_init', [ $this, 'updater' ], 0 ); // Just add the input field and remove link in the plugin index add_action( 'current_screen', function( $screen ) { if ( 'plugins' == $screen->base ) : $slug = DFBM()->prefix() . '-plugins'; if ( ! $this->licenseKey ) add_action( 'after_plugin_row', [ $this, 'licenseField' ] ); else add_filter( 'plugin_action_links_' . plugin_basename( DFBM()->file() ), [ $this, 'removeLink' ] ); // Enqueue the script to handle the Ajax requests ( new dfbmControllerEnqueue )->enqueueScript( $slug, [ 'jquery' ] ); // Localize the script wp_localize_script( $slug, DFBM()->prefix() . 'PHP', $this->localize() ); endif; }); // end add_action } // end initialize /** * Data for Localization * * @since 1.0.10 */ public function localize() { return [ 'prefix' => DFBM()->prefix(), 'error' => $this->getMessage()->error, 'url' => admin_url( 'admin-ajax.php' ), 'key' => $this->licenseKey ? $this->licenseKey : '', 'nonce' => wp_create_nonce( DFBM()->prefix() . "-nonce-value" ) , 'valid' => esc_html__( "Insert a valid key.", DFBM()->domain() ), ]; } // end localize /** * Instantiate the updater class * * @since 1.0.10 */ public function updater() { if ( ! class_exists( 'EDD_SL_Plugin_Updater' ) ) include( dirname( __FILE__ ) . '/EDD_SL_Plugin_Updater.php' ); // Initiate the updater class new EDD_SL_Plugin_Updater( $this->args->url, $this->args->file, $this->args->args ); } // end updater /** * Add the license field * * @since 1.0.10 */ public function licenseField( $plugin ) { if ( false !== strpos( $plugin, DFBM()->domain() ) ) : $prefix = DFBM()->prefix(); $text01 = esc_html__( 'Enter Your License Key for future updates', DFBM()->domain() ); $text02 = esc_html__( 'Submit', DFBM()->domain() ); $text03 = esc_html__( 'You can get it ', DFBM()->domain() ); $text04 = esc_html__( 'here.', DFBM()->domain() ); echo <<<FORM </tr><tr class="plugin-update-tr"> <td colspan="3" class="plugin-update" style="border-left: 4px solid #00a0d2"> <div class="update-message" style="margin-top:5px;margin-bottom:5px;"> <form action="" method="POST"> <input type="hidden" name="{$prefix}_license" value="license_key"> <div class="about-text" style="display:inline-block"> <div> <input id="{$prefix}-license-field" type="text" class="regular-text" id="license_key" value="" name="license_key" placeholder="{$text01}" style="min-height:30px;"> </div> </div> <div style="display:inline-block"> <button id="{$prefix}-license-submit" class="button button-primary" type="submit" value="Submit" name="_submit" style="margin-top:-6px;border-radius:0">{$text02}</button> </div> </form> <div style="display:inline-block;margin-left:5px"> <span style="margin-right:5px">-</span><span>{$text03}</span><a href="https://elegantmarketplace.com/checkout/purchase-history/" target="_blank">{$text04}</a> </div> </div> </td> FORM; endif; } // end licenseField /** * Link to remove the license * * @since 1.0.10 */ public function removeLink( $items ) { $items[] = '<a id="' . DFBM()->prefix() . '-remove-license' . '" href="#">' . esc_html__( 'Remove License', DFBM()->domain() ) . '</a>'; return $items; } // end removeLink /** * Verify the Ajax request * * @since 1.0.10 */ private function verifyRequest( $opt ) { $r = $_REQUEST; $nonce = isset( $r['nonce'] ) ? esc_attr( $r['nonce'] ) : false; if ( $nonce && wp_verify_nonce( $nonce, DFBM()->prefix() . '-nonce-value' ) ) : if ( isset( $r[$opt], $r['license'] ) && 'true' == $r[$opt] ) return true; endif; wp_send_json( (object) [ 'failed' => $this->getMessage()->error ] ); wp_die(); } // end verifyRequest /** * Get the process messages * * @since 1.0.10 */ private function getMessage() { $message = 'The license has been successfully '; return (object) [ 'error' => esc_html__( 'That fails. Please reload the page and try it again.', DFBM()->domain() ), 'success' => esc_html__( $message . 'activated.', DFBM()->domain() ), 'delete' => esc_html__( $message . 'deactivated.', DFBM()->domain() ), ]; } // end getMessage /** * Activate the license via Ajax * * @since 1.0.10 */ public function activateLicense() { if ( $this->verifyRequest( DFBM()->prefix() . '_activate' ) ) : $licenseKey = trim( esc_attr( $_REQUEST['license'] ) ); $response = $this->apiRemoteRequest( 'activate_license', $licenseKey ); if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) : if ( is_wp_error( $response ) ) $message = $response->get_error_message(); else $message = esc_html__( 'An error occurred, please try again.', DFBM()->domain() ); else : $license = json_decode( wp_remote_retrieve_body( $response ) ); if ( false === $license->success ) : switch( $license->error ) : case 'expired' : $message = sprintf( esc_html__( 'Your license key expired on %s.', DFBM()->domain() ), date_i18n( get_option( 'date_format' ), strtotime( $license->expires, current_time( 'timestamp' ) ) ) ); break; case 'revoked' : $message = esc_html__( 'Your license key has been disabled.', DFBM()->domain() ); break; case 'missing' : $message = esc_html__( 'Invalid license.', DFBM()->domain() ); break; case 'invalid' : case 'site_inactive' : $message = esc_html__( 'Your license is not active for this URL.', DFBM()->domain() ); break; case 'item_name_mismatch' : $message = sprintf( esc_html__( 'This appears to be an invalid license key for %s.', DFBM()->domain() ), DFBM()->title() ); break; case 'no_activations_left': $message = esc_html__( 'Your license key has reached its activation limit.', DFBM()->domain() ); break; default : $message = esc_html__( 'An error occurred, please try again.', DFBM()->domain() ); break; endswitch; endif; endif; if ( isset( $message ) ) : wp_send_json( (object) [ 'failed' => $message ] ); wp_die(); endif; // Set the license information ( new dfbmModelSet )->option( DFBM()->prefix() . '_license_status', $license->license ); ( new dfbmModelSet )->option( DFBM()->prefix() . '_license_key', $licenseKey ); $this->clearCaches(); endif; wp_send_json( (object) [ 'success' => $this->getMessage()->success ] ); wp_die(); } // end activateLicense /** * Deactivate the license via Ajax * * @since 1.0.10 */ public function deactivateLicense() { if ( $this->verifyRequest( DFBM()->prefix() . '_deactivate' ) ) : $response = $this->apiRemoteRequest( 'deactivate_license', $this->licenseKey ); if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) : if ( is_wp_error( $response ) ) $message = $response->get_error_message(); else $message = __( 'An error occurred, please try again.' ); if ( isset( $message ) ) : wp_send_json( (object) [ 'failed' => $message ] ); wp_die(); endif; endif; $licenseData = json_decode( wp_remote_retrieve_body( $response ) ); if( $licenseData->license == 'deactivated' ) : // Remove the license data ( new dfbmModelDelete )->option( DFBM()->prefix() . '_license_status' ); ( new dfbmModelDelete )->option( DFBM()->prefix() . '_license_key' ); $this->clearCaches(); endif; endif; wp_send_json( (object) [ 'delete' => $this->getMessage()->delete ] ); wp_die(); } // end deactivateLicense /** * Clear EDD and plugin caches * * @since 1.0.10 */ public function clearCaches() { $slug = basename( $this->args->file, '.php' ); $beta = empty( $this->args->args['beta'] ) ? false : true; $key = md5( serialize( $slug . $this->args->args['license'] . $beta ) ); $key2 = md5( serialize( $slug . $beta ) ); ( new dfbmModelDelete )->option( '_site_transient_update_plugins' ); ( new dfbmModelDelete )->option( 'edd_api_request_' . $key2 ); ( new dfbmModelDelete )->option( 'edd_api_request_' . $key ); ( new dfbmModelDelete )->option( $key2 ); ( new dfbmModelDelete )->option( $key ); } // end clearCaches /** * Handle the API remote request * * @since 1.0.10 */ public function apiRemoteRequest( $request, $license ) { $args = [ 'url' => home_url(), 'license' => $license, 'edd_action' => $request, 'item_name' => urlencode( DFBM()->title() ), ]; return wp_remote_post( DFBM()->host(), array( 'timeout' => 15, 'sslverify' => false, 'body' => $args ) ); } // end apiRemoteRequest } // end class } // end if
Final words for this implementation
As you can see here, the implementation of the EMP EDD Plugin Updater is quite simple. Adding some simple Ajax instructions makes it very easy for users to use. This makes sense if you don’t want to implement your own settings page for your plugin, or if you want to separate it from each other like I do. As promised, here is a link to a gist that you can easily integrate into your workflow. Have fun with it!
Have you suggestions for improvements to this article? Just use the comment area below. Do you want support for implementation or do you need help elsewhere? You can book us. For this, simply use our contact form to get in touch with us.
Divi is a registered trademark of Elegant Themes, Inc. This website is not affiliated with nor endorsed by Elegant Themes.
Get the best out of your web!