Meet the GUY WHO Makes WordPress Be What You Need It To be

Full Stack WordPress Developer Who Loves to <Create>, new Innovate && Obliterate($bugs)

For the past 10 years I have been heavily involved in WordPress & Woo Commerce projects.

On this page I would like to share examples of my work. Themes, Plugins, Bug Fixing, Migrations, Speed Optimization and SEO – I’ve lived and breathed it all.

dw-display-pic
Dale Woods

WordPress Wizard – WooCommerce God – ISTJ

Syncing 2 WordPress Logins With Forum & Chatroom
WooCommerce License Key Manager For My Software

Multi WordPress & Apps Account/Login Sync, License Key Manager Integration, WooCommerce Enhancements, Account Management, Custom Login Page,

A passion business of mine is a Forex education website that specializes in sharing my knowledge and tools.

There is a membership area which traders can sign up to enter a private community with benefits – handled by custom WooCommerce enhancements.

The plugin will also sync account creations, membership upgrades, logins (including login cookies) from all websites/apps involved so the customer experiences only one login.

I also create custom made Forex trading tools which are sold via WooCommerce enhancements on the front end site.

This account management plugin will also communicate with other custom plugins I made, like my software license manager.

Plugin Model

A 10000ft view of the plugin’s responsibilities

The idea was to make all these complex operations hidden from the user and allow them to experience seamless operations.
<?php

namespace TFGToolsIntergration;

final class AccountsAPI {

    private static $_instance;

    public function __clone() {}
    public function __wakeup() {}

    public static function getInstance() {
        if( !isset(self::$_instance) ) {
            self::$_instance = new self();
        }

        return self::$_instance;
    }


    public static function loadHooks() {
        add_action( 'clear_auth_cookie', [__CLASS__, 'beforeLogout'], 10, 2);
    }

   
    public static function usernameExists( string $username ): bool {
        return username_exists( $username );
    }


    public static function emailExists( string $email ): bool {
        return email_exists( $email );
    }


    public static function deleteAccount(int $userID): bool {
        require_once(ABSPATH.'wp-admin/includes/user.php');   
        
        $userapi = new UserAPI($userID);
        $userEmail = $userapi->getEmail();
        

        // Tell War Room server to delete account
        $warroomServer = new WarRoomRestAPI(
            SettingsAPI::getWarRoomDelEndPoint(),
            SettingsAPI::getSecret()
        );

        $warroomResult = $warroomServer->call([       
            'email' => $userapi->getEmail()
        ]);

        if( empty($warroomResult) ) {
            $errorMsg = 'War Room Server sent an empty response for remove account request!';
            return false;
        }


        $success = wp_delete_user($userID);
        return $success;
    }
    

    /*
     *  Create a standard woo commerce TFG customer
     */
    static function createStdTFGCustomerAccount(string $user, string $pass, string $email, string &$errorMsg): int {
        $result = wc_create_new_customer( $email, $user, $pass );

        if( $result instanceof \WP_Error ) {
            $errorMsg = $result->get_error_message();
            return false;
        }

        return $result;
    }


    /*
     *  Upgrade the User ID to be a standard War Room member
     */
    static function promoteToWarRoomMember(int $userID, string &$errorMsg, string $usePass = ''): bool {

        if( $userID <= 0 ) { 
            printf("<br>Bad user id passed to promoteToWarRoomMember");
            return false;
        }

        $user = new UserAPI($userID);

        // Update War Room flag meta data on TFG
        $user->promoteToWarRoomMember();

        // Tell War Room server to create account
        $warroomServer = new WarRoomRestAPI(
            SettingsAPI::getWarRoomCreateEndPoint(),
            SettingsAPI::getSecret()
        );

        $pass = ( empty($usePass) ) ? wp_generate_password() : $usePass;

        $warroomResult = $warroomServer->call([
            'username' => $user->getUserName(),
            'party' => $pass,
            'email' => $user->getEmail()
        ]);

        if( empty($warroomResult) ) {
            $errorMsg = 'War Room Server sent an empty response for creating an account!';
            return false;
        }

        if( $warroomResult['error'] ?? false === true ) { 
            $errorMsg = $warroomResult['errorMsg'];
            return false;
        }

        return true;
    }
	
	
	static public function revokeWarRoomMember(int $userID, string &$errorMsg):bool { 
		
		if( $userID <= 0 ) { 
            printf("<br>Bad user id passed to revokeWarRoomMember");
            return false;
        }
		
		$userapi = new UserAPI($userID);
        $userEmail = $userapi->getEmail();

        // Tell War Room server to delete account
        $warroomServer = new WarRoomRestAPI(
            SettingsAPI::getWarRoomDelEndPoint(),
            SettingsAPI::getSecret()
        );

        $warroomResult = $warroomServer->call([       
            'email' => $userapi->getEmail()
        ]);

        if( empty($warroomResult) ) {
            $errorMsg = 'War Room Server sent an empty response for remove account request!';
            return false;
        }
		
		$userapi->revokeWarRoomMembership();	
		
		return true;
	}
    
	

    static public function warRoomServerAccountExists(string $email) {
        
        // Construct War Room API
        $warroomServer = new WarRoomRestAPI(
            SettingsAPI::getWarRoomInfoEndPoint(),
            SettingsAPI::getSecret()
        );

        $warroomResult = $warroomServer->call([
            'username' => '',
            'email' => $email
        ]);

        // No data returned from WR Server
        if( empty($warroomResult) ) {
            throw new \Exception('<b>War Room Server Error:</b> Empty response from War Room server, please contact support. Or wait a moment and try again.');
        }

        // Server returned error flag
        if( isset($warroomResult['error']) && $warroomResult['error'] == true ) {
            
            // Contains error message
            if( isset($warroomResult['errorMsg']) && strlen(isset($warroomResult['errorMsg']) > 0) ) {
                throw new \Exception( '<b>War Room Server Error:</b> ' . $warroomResult['errorMsg'] );
            }

            // Error flag, with no error message
            else {
                throw new \Exception('<b>War Room Server Error:</b> Server responded with an error, but didn\'t provide a reason.');  
            }
        }

        // War Room servers responds whether an account exists or not already
        return $warroomResult['email-exists'] == true;
    }


    static public function beforeLogout() { 
        if( !is_user_logged_in() ) { return; }

        $user = wp_get_current_user();
        $email = $user->user_email;

        // Add logout code here later for war room wp and xen foro if needed
    }
}
<script>
	var loginForm = {
		ajaxUrl: '<?php echo admin_url( 'admin-ajax.php' ); ?>',
		btnText: $('#signInBtn').val(),
		token: '',
		defaultPage: '<?php echo SettingsAPI::getDefaultFormRedirectPage(); ?>',
		
		init: function() { 
			$('#login-form').submit((event) => {
				event.preventDefault();
				this.onFormSubmit(event);
				return false;
			});
		},


		sanitizeText: function(dirtystr) {
			return dirtystr.replace(/<(|\/|[^>\/bi]|\/[^>bi]|[^\/>][^>]+|\/[^>][^>]+)>/g, '');
		},


		validateEmail: function(email) {
			var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
			return re.test(String(email).toLowerCase());
		},


		getFormInputs: function() {
			let inputs = {};

			inputs.email = this.sanitizeText( $('#inputEmail').val() ) ;
			inputs.pass = this.sanitizeText( $('#inputPassword').val() );

			return inputs;
		},


		validateInputs: function(inputs) { 
			this.clearNotices();

			// Email not valid
			if( !this.validateEmail(inputs.email) ) {
				this.printNotice('Email is not valid','error');
				return false;
			}
			
			// Username exists?
			if( inputs.pass.length < 3 ) {
				this.printNotice('Please enter a valid password','error');
				return false;
			}

			return true;
		},
		

		onFormSubmit: function(event) {
			
			let inputs = this.getFormInputs();

			// Failed, reset submit button
			if( !this.validateInputs(inputs) ) { 
				this.resetSubmitBtn();
				return;
			}

			this.tryLogin(inputs);
		},


		tryLogin: function(inputs) {
			inputs.action = 'tfg_login_form';
			inputs.token = this.token;
			this.post(inputs);
		},


		post: function(formData) {
			
			$.ajax({
				type: 'post',
				url: this.ajaxUrl,
				data: formData,
				dataType: 'json',
				beforeSend: () => {
					this.clearNotices();
					$('#signInBtn').val('Contacting Command Center... ');
					$('#signInBtn').prop('disabled', true);
				}                   
			})
				.done( (data, textStatus, jqXHR) => {    
					// Server error
					if( jqXHR.status !== 200 ){
						this.printNotice('Server Error Code: ' + jqXHR.status, 'error');
					}
					
					// There was a form error
					else if( data.error === true ) {
						this.printNotice(data.errorMsg, 'error');
						this.resetSubmitBtn();
					}

					// Success
					else {                             
						this.onSuccessResponse(data); 
					}
				}) 

				.fail( (jqXHR, textStatus, errorThrown) => { 
					console.log(jqXHR.responseText);
					this.printNotice('Request failed: ' + errorThrown);
					this.resetSubmitBtn();
				});  
		},


		resetSubmitBtn: function() { 
			this.getNewToken();
			$('#signInBtn').val(this.btnText);
			$('#signInBtn').prop('disabled', false);
		},


		getNewToken: async function() {
			...
		},


		clearNotices: function() {
			$('#form-notices').html('');
		},


		resetForm: function() {
			this.clearNotices();
			this.resetSubmitBtn();
		},


		printNotice: function(notice, type = 'notice')  {
			let css = {
				'background-color': '#000',
				'border': '#000 solid 1px',
				'color': '#FFF',
				'padding': '20px'
			}

			switch(type) {
				case 'notice': { 
					css['background-color'] = 'rgb(221, 153, 51)';
					css['border'] = '1px solid rgb(212, 142, 38)';
					break;
				}

				case 'success': { 
					css['background-color'] = 'rgb(133, 183, 30)';
					css['border'] = '1px solid rgb(137, 146, 118)';
					break;
				}					

				case 'error':
				default: {
					css['background-color'] = 'rgb(196, 50, 50)';
					css['border'] = '1px solid rgb(181, 43, 43);';
					break;
				}
			}
			
			css['text-align'] = 'center';
			css['border-radius'] = '15px';

			if( type === 'error' ) {
				$('#form-notices').append('<p>❌ ' + notice + '</p>');
			}

			else {
				$('#form-notices').append('<p>' + notice + '</p>');	
			}
			
			$('#form-notices p:last-child').css(css);
			
			// Shake if not a success message
			if( type !== 'success' ) {
				$('#form-notices').shake();
			}
		},				

		onSuccessResponse: function(response) { 
			
			let redirectTo = '<?php echo urldecode($_GET['redirect_to'] ?? ''); ?>';

			if( response['...'] == true && response['...'].length > 0 ) {
				
				$('#signInBtn').val('Logging into War Room...');

				$('#login-form').after('<iframe id="loader" width="1" height="1" frameBorder="0"></iframe>');

				// Iframe Method: attach the load event listener before adding the src of the iframe to prevent from the handler missing the laod event..
				$('iframe').on('load', function() {
					$('#signInBtn').val('War Room Login Complete...');
					$('#signInBtn').css({
						'background-color': '#53c121',
						'border-color': '#4e9b2b'
					});
				}); 
			
				// Launch iframe
				$('#loader').attr( 'src', atob(response['...']) ); //add iframe src

				// If no redirect exists, send them to war room
				if(redirectTo.length < 1) {							
					redirectTo = '<?php echo SettingsAPI::getWarRoomHomeUrl(); ?>';
				}
			}

			// Login without war room
			else {
				if(redirectTo.length < 1) {
					redirectTo = this.defaultPage;
				}
				
				// If war room is redirect target
				if( this.getSubdomain(redirectTo).includes('warroom') ) { 
					redirectTo = this.defaultPage;
				}
				
				$('#signInBtn').val('Success...');
				$('#signInBtn').css({
					'background-color': '#53c121',
					'border-color': '#4e9b2b'
				});
			}

			//console.log(`redirecting to: ${redirectTo}`);
			
			// Set time out here to give auth cookies a chance to load
			setTimeout(() => {
				window.location = redirectTo;
			}, 3000);
			

			// Debug print response to screen
			//this.printNotice( JSON.stringify(response) );
		},


		getSubdomain: function(url) {
			
			if( url.length < 1 ) { return ''; }
			let hostname = url;
			let lastChar = hostname.slice(-1);
			
			if( lastChar === '/' ) { 
				url = hostname.slice(0, -1)
			}
				
			if( url.includes('http') ) {
				let urlsplit = url.split('//');
				hostname = urlsplit[1];
			}
			
			let regexParse = new RegExp('[a-z\-0-9]{2,63}\.[a-z\.]{2,5}$');
			let urlParts = regexParse.exec(hostname);
			if( urlParts === null ) { return ''; }
			if( urlParts[0].length < 1 ) { return ''; }
			return hostname.replace(urlParts[0],'').slice(0, -1);
		}
	};


	jQuery(document).ready(function($){
		loginForm.init();
	});


	jQuery.fn.shake = function(interval, distance, times){
		interval = typeof interval == "undefined" ? 100 : interval;
		distance = typeof distance == "undefined" ? 10 : distance;
		times = typeof times == "undefined" ? 3 : times;
		let jTarget = $(this);
		jTarget.css('position','relative');

		for(let iter=0;iter<(times+1);iter++){
			jTarget.animate({ left: ((iter%2==0 ? distance : distance*-1))}, interval);
		}

		return jTarget.animate({left: 0},interval);
	}
</script>

Unit Tests

To sleep easy at night…

<?php

namespace TFGToolsIntergration;

$emailAccount = '[email protected]';
$username = 'fakeUser';
$pass = 'rando%mapss$y342';

UnitTest::printLabel('Starting Tests');

echo '<hr>';

$result = UnitTest::assert_false( AccountsAPI::emailExists($emailAccount) );

UnitTest::printResult(
    sprintf('%s Email Account Exists = %d', $emailAccount, !$result), 
    $result
);


$result = UnitTest::assert_false( AccountsAPI::usernameExists($username) );

UnitTest::printResult(
    sprintf('User %s Account Exists = %d', $username, !$result), 
    $result
);


echo '<br>';
echo '<hr>';
UnitTest::printLabel('Create account');

$errorMsg = '';
$userID = AccountsAPI::createStdTFGCustomerAccount( $username, $pass, $emailAccount, $errorMsg);

if( $userID <= 0) {
    $userID = email_exists($emailAccount);
}

$result = UnitTest::assert_true($userID > 0);

UnitTest::printResult(
    sprintf('Created new Std Account ID = %d', $userID), 
    $result
);


$result = UnitTest::assert_false( AccountsAPI::warRoomServerAccountExists($emailAccount) );

UnitTest::printResult(
    sprintf('Account Exists On War Room Server = %d', !$result), 
    $result
);

flush();


echo '<br>';
echo '<hr>';
UnitTest::printLabel('Warroom Priv Tests');
$userapi = new UserAPI($userID);

$result = UnitTest::assert_false( $userapi->isWarRoomAccount() );

UnitTest::printResult(
    sprintf('User %s is War Room Account = %d', $username, !$result), 
    $result
);


$error = '';

$result = UnitTest::assert_true( AccountsAPI::promoteToWarRoomMember($userID, $error, $pass) );

UnitTest::printResult(
    sprintf('Promote User %s to War Room Account = %d', $username, $result), 
    $result
);

if( !$result ) { 
    printf('<p>Promote to war room error: %s</p>', $error);
}


$result = UnitTest::assert_true( $userapi->isWarRoomAccount()) ;

UnitTest::printResult(
    sprintf('User %s is War Room Account = %d', $username, $result), 
    $result
);



$result = UnitTest::assert_true( AccountsAPI::warRoomServerAccountExists($emailAccount) );

UnitTest::printResult(
    sprintf('User %s Exists | War Room Server Agrees = %d', $username, $result), 
    $result
);

flush();


echo '<br>';
echo '<hr>';
UnitTest::printLabel('Delete account');
$result = UnitTest::assert_true( AccountsAPI::deleteAccount($userID) );

UnitTest::printResult(
    sprintf('Deleting user id %d... Success = %d', $userID, $result), 
    $result
);


$result = UnitTest::assert_false( AccountsAPI::emailExists($emailAccount) );

UnitTest::printResult(
    sprintf('TFG Std Account Email Account Exists = %d', $emailAccount, !$result), 
    $result
);


$result = UnitTest::assert_false( AccountsAPI::warRoomServerAccountExists($emailAccount) );

UnitTest::printResult(
    sprintf('Account Exists On War Room Server = %d', !$result), 
    $result
);

echo '<hr>';


echo '<br>';
echo '<br>done';

Lead Generation Widget

Widget that follows user down the page, popup modal, ajax driven multistep form, on the spot trial key generation.

floating widget
popup login trial modal
<?php
class TFGToolsOfferWidget extends \WP_Widget {
    private string $_path;
    private string $_url;
    private $_adminMetaBox = null;

    public function __construct() {        
        parent::__construct(
            'tfg-tools-offer-widget',
            'TFG tools offer widget',
            'Offer my tools to customers, and sub them to lists'
        );

        $this->_path = MainApp::pluginPath() . '/Widget/ToolsTrialOfferWidget/';
	$this->_url = MainApp::pluginUrl() . 'Widget/ToolsTrialOfferWidget/';

        if( is_admin() )
            $this->_adminMetaBox = new PostProductMetaBox('tfgwo-admin-meta-box', 'tfgwo-admin-meta-box');

        add_action( 'wp_enqueue_scripts', [$this, 'loadJs'] );
  
        add_action( 'get_footer', [$this, 'loadFooter'] );

        add_action( 'wp_ajax_tfgwo-token', [$this, 'onTokenRequest'] );
        add_action( 'wp_ajax_nopriv_tfgwo-token', [$this, 'onTokenRequest'] );

        // Logged in form trial key req
        add_action( 'wp_ajax_tfgwo-gen-trial-btn', [$this, 'onTrialGenRequest'] );

        // Get logged in page content
        add_action( 'wp_ajax_tfgwo-get-priv-content', [$this, 'afterLoginContent'] );

        // Create account form submit
        add_action( 'wp_ajax_nopriv_tfgwo-ca-form', [$this, 'onCreateAccountFormReq'] );
    }

...

    /*
     *  Build modal output html
     */
    private function outputModal(\WC_Product $product): string {
        $user = new UserAPI();
        ob_start(); ?>

        <div class="tfgmodal">
            <div class="modal-content-container">                
                <div class="modal-content">
                    <p class="close">&times;</p>
                    <div class="msgs"></div>
                    <div class="inner-content">
                        <?php
                            // Not logged in
                            if( !$user->isLoggedIn() )
                                echo $this->outputGuestModalContent($product);

                            // Logged in but trial not elidgable
                            else if( ! \LicenseKeyManager\TrialKeyAPI::eligibleForTrail( $user->getID(), $product->get_id() ) ) 
                                echo $this->outputTrialNotEligable($product);

                            // Logged in and can download trial
                            else {
                                echo $this->outputTrialEligable($product);
                            }
                        ?>
                    </div>    
                </div>
            </div>    
        </div>

        <?php
        return ob_get_clean();
    }

    public function onTrialGenRequest() {
        
        $response = new AjaxResponse();
        $request = new TrialStartAjaxRequest($_POST, $this->_ajaxNonceKey);

        $error = 'Error not set';
        
        if( !$request->validate($error) ) {
            $response->sendError([
                'errorMsg'=> $error
            ]);
        }

        // Get meta details
        $productID = $request->getProductID();
        $user = wp_get_current_user();        
        $user_id = $user->ID;

        // Confirm user logged in
        if( $user_id <= 0 ) {
            $response->sendError([
                'errorMsg' => 'Cannot verify user'
            ]);
        }

        // Confirm product has trial enabled
	$productHasTrialEnabled = \LicenseKeyManager\TrialKeyAPI::trialKeyEnabledForProduct($productID);

        $product = wc_get_product($productID);
		
	if( !$productHasTrialEnabled ) {
            $response->sendError([
                'errorMsg' => 'Product trial not available at the moment'
            ]);
        }       

        // Make sure user is allowed to get a trial, if not display permission denied content
        if( ! \LicenseKeyManager\TrialKeyAPI::eligibleForTrail( $user_id, $productID ) )  {
            if( !($product instanceof \WC_Product) )
                $response->sendError([            
                    'errorMsg' => sprintf('Internal Server Implosion: Could not get the product from the database. <a class="error-link" href="%s">Please let Dale know!</a><br>', get_site_url(null, '/contact-us/'))
                ]);
            
            $response->sendSuccess([            
                'content' => base64_encode( $this->outputTrialNotEligable($product) )
            ]);
        }

        // Generate the trial license key
        $licence = ProductTrialAPI::generateTrialKey(
            $user_id,
            $productID,
            false,  // Send email?
            true   // Sendy subscribe?
        );

        $key = $licence->getKey();

	// Don't subscribe to Sendy lists if this is a dev environment
        if( !MainApp::isDevEnv() ) {
			
	// Subscribe to product specific list
	$product_specific_sendy_list_id = WC_ProductMeta::getSendyListID($productID);
			
        if( !empty($product_specific_sendy_list_id) && strlen($product_specific_sendy_list_id) > 0 )
            SendyAPI::subscribe($user->user_login, $user->user_email, $product_specific_sendy_list_id);
			
	// Add to the lead list (just for excluding in marketing, not the newsletter list)
	    SendyAPI::subscribe($user->user_login, $user->user_email, SettingsAPI::getSendyLeadListID());	

        // Subscribe to lead newsletter list if user selected they wanted it
        if( $request->userSubscribe() )
            SendyAPI::subscribe($user->user_login, $user->user_email, SettingsAPI::getSendyNewsletterListID());

        // Should we let the admin know?
        if( $this->getSetting('email-admin-on-lead') === 'yes' )
            $this->sendAdminNofifyEmail(wp_get_current_user(), $product, $key, $request->userSubscribe());
 
        $response->sendSuccess([            
            'content' => base64_encode($this->outputDownloadReady($key))
        ]);
    }

WooCommerce License Key Manager For My Software

Custom Software License Key Manager, WooCommerce Plugin, Custom Hooks/Filters, API

Most license key plugins were not suitable so I needed to create something for myself.

This plugin generates license keys when the user checks out with a product that has been associated with a license key enabled product.

There can be trial keys, lifetime keys or limited time keys.

User’s have the option to renew their key when expired.

<?php

namespace LicenseKeyManager;


final class CheckoutAPI {

    public function __clone() {}
    public function __wakeup() {}

    public static function getInstance() {
        if( !isset(self::$_instance) ) {
            self::$_instance = new self();
        }

        return self::$_instance;
    }


    public static function loadHooks() {        
        add_action( 'woocommerce_before_thankyou', [__CLASS__, 'thankyouPagePurchaseMessage']);
        add_action( 'woocommerce_order_status_completed', [ __CLASS__, 'onOrderComplete'], 10, 1 );

        add_action( 'license_keys_processed', [ __CLASS__, 'sendLicenseKeyPuchaseEmail'], 10, 2);
    }


    public static function onOrderComplete( $order_id ) {
        $order = wc_get_order($order_id);

        if( !($order instanceof \WC_Order) )
            return;

        self::processLicenseKeys($order);
    }


    private static function getParentID(int $productID): int {
		$wc_product = wc_get_product($productID);
		
		if( !($wc_product instanceof \WC_Product) ) { return $productID; }		
		
		if( $wc_product->get_parent_id() > 0 ) {			
			return $wc_product->get_parent_id();
		}
		
		else {
			return $wc_product->get_id();
		}
	}


    private static function isProductEnabledForLicensing(int $parent_product_ID): bool {
        return get_post_meta( $parent_product_ID, SettingsAPI::LICENSE_KEY_ENABLED_METAKEY, true ) === 'yes';
    }    


    public static function wasLicenseKeyPurchased(\WC_Order $order): bool {
        foreach( $order->get_items() as $item_id => $line_item ){
            $product = $line_item->get_product();

            if( !($product instanceof \WC_Product) )
                continue;    

            $parent_product_ID = self::getParentID($product->get_id());
            
            if( self::isProductEnabledForLicensing($parent_product_ID) )
                return true;
        }

        return false;
    }


    private static function processLicenseKeys(\WC_Order $order) {
        $keys = [
            'created' => [],
            'updated' => []
        ];

        foreach( $order->get_items() as $item_id => $line_item ) {
            $product = $line_item->get_product();

            if( !($product instanceof \WC_Product) )
                continue;

            $parent_product_ID = self::getParentID($product->get_id());

            if( !self::isProductEnabledForLicensing($parent_product_ID) )
                continue;

            $userID = $order->get_user_id();    

            self::onLicenseProductPurchase($parent_product_ID, $userID, $keys);
        }

        if( !empty($keys['created']) || !empty($keys['updated']) )
            do_action('license_keys_processed', $order, $keys);
    }


    public static function onLicenseProductPurchase(int $parent_product_ID, int $userID, array &$keys) {
        
        $license = null;
        $updateingOldLicense = false;

        // Does user already have license for this product
        $currentUserkeys = LicenseKeyModel::getUserKeys($userID);

        // See if there is an old license to update
        if( !empty($currentUserkeys) ) {   
            foreach($currentUserkeys as $key) {
                if( !$key->isTrial() && ProductAPI::getParentID($key->getProductID()) === $parent_product_ID ) {
                    $license = $key;
                    $updateingOldLicense = true;
                    break;
                }    
            }
        }
  
        if( !$updateingOldLicense ) {
            $license = new LicenseKey();
            
            // Generate new key
            $gen = new ProductLicenseKeyGenerator($parent_product_ID);
            $newKey = $gen->generateKeyUnique();
            $license->setKey($newKey);
            $license->setUserID($userID);
            $license->setProductID($parent_product_ID);
        }

        $license->setStatus('enabled');        
        $license->setTrial(false);

        $periodWeeks = (int) get_post_meta( $parent_product_ID, SettingsAPI::LICENSE_KEY_PERIOD_WEEKS, true );

        // Calculate new time stamp for license
        if($periodWeeks >= 52 && $periodWeeks % 52 === 0) {
            $periodyears = (int) ($periodWeeks / 52);
            if( $periodyears == 1 )
                $license->setExpireTimeStamp( strtotime( sprintf("+%d year", $periodyears) ) );
            else
                $license->setExpireTimeStamp( strtotime( sprintf("+%d years", $periodyears) ) );    
        }

        else {
            if( $periodWeeks == 1 )
                $license->setExpireTimeStamp( strtotime( sprintf("+%d week", $periodWeeks) ) ); 
            else
                $license->setExpireTimeStamp( strtotime( sprintf("+%d weeks", $periodWeeks) ) );       
        }

        if( !$updateingOldLicense ) {            
            $license = apply_filters('license_key_created', $license);
            LicenseKeyModel::addLicenseKey($license);
            $keys['created'][] = $license;
        }

        else {
            $license = apply_filters('license_key_updated', $license);
            LicenseKeyModel::updateLicense($license);
            $keys['updated'][] = $license;
        }
        
        // Remove any trial keys for this product or sister variations if a full license was purchased
        LicenseKeyModel::deleteTrialKey( $license->getUserID(), $license->getProductID() );     
    }


    public static function thankyouPagePurchaseMessage(int $order_id) { 
        if( empty($order_id) )
            return;

        $order = wc_get_order($order_id);
        
        if( !($order instanceof \WC_Order) )
            return;

        if( !self::wasLicenseKeyPurchased($order) )    
            return;

        $user = wp_get_current_user();
    ?>
        <aside class="woocommerce-message">
            <h3>License Keys Processing...</h3>
            <p>You've purchased some products which come with license keys to activate.</p>

            <p>When your order is finished being processed and marked as completed in the back end - you will recieve an email with your new keys and your download links</p>

            <p>If you don't get an email containing your license keys at: <b><?php echo $user->user_email; ?></b>, within the hour, contact support and let us know.</p>
        </aside>
    <?php }


    public static function sendLicenseKeyPuchaseEmail(\WC_Order $order, array $keys) {
        
        // Load the WC_Email class;
        $emails = wc()->mailer()->get_emails();
        
        $email = new WC_License_Key_Order_Email();
        $email->trigger($order, $keys);
    }
}

WordPress Virtual Page Builder

Generates pages based on URL, custom location schema, ~ 200,000 SEO optimized pages, Sitemap Generation

This website was designed to tap into the potential millions of keyword combinations with cities + servicies.

Standard WordPress blows up around the 50k page mark, so I needed another solution.

My plugin custom built for this website has the task of building the content of the page at the time of request, then serving it to the user.

By simulating real unique pages – it completely frees the database up and despite being able to generate over 200k keyword rich pages, it is very light weight.

Lightweight & Speed Optimized

High speed scores even though pages are built on request

<?php

namespace Dazamate\DazMovingServices;

final class VirtualPageRequest {

    private static array $_settings = [];

    private static string $_city = '';
    private static string $_stateCode = '';
    private static string $_service = '';
    private static array $_seviceData = [];
    private static array $_cityData = [];

    public static $_virtualPage = null;

    public static function loadHooks() { 
        self::$_settings = App::getSettings();
        add_action('parse_request', [__CLASS__, 'parse_request']);

        SiteMapModule::loadHooks();
    }

    
    public static function parse_request($wp) {

        if( strlen($wp->request) < 1 ) 
            return;

        self::scanForVirtualRequestSignature($wp->request);

        if( strlen(self::$_service) < 1 )
            return;

        // extract the city and state from the slug
        self::extractLocationData(self::$_service, $wp->request);

        // make sure it is valid and we have data for it
        if( !self::isValidLocationData() )
            return;

        // Process virual page request
        self::onVirtualPageRequest(self::$_service, $wp);
    }


    private static function scanForVirtualRequestSignature(string $request) {

        foreach( self::$_settings['templates'] as $virtualPageSig => $data ) {
            if( strpos($request, $virtualPageSig) === 0) {
                self::$_service     = $virtualPageSig;
                self::$_seviceData  = $data;                
                return;
            }
        }

        // Nothing found
        self::$_service     = '';
        self::$_seviceData  = [];
    }


    private static function extractLocationData(string $signatureFound, string $request) {
        
        // +1 to remove the right '-' separator
        $slugData = substr($request, strlen($signatureFound)+1);

        $data = explode('-', $slugData);

        // split state code which will be the last array el - Eg long-beach-ca will splice to ['ca']
        $statecode = array_splice($data, -1);
        self::$_stateCode = $statecode[0];

        // join remaining el with space so ['long','beach'] = long beach
        self::$_city = implode(' ', $data); 
        
        //Port St. Lucie
    }


    private static function isValidLocationData(): bool {
        if( strlen(self::$_city) < 1 || strlen(self::$_stateCode) < 1 ) 
            return false;

        $cityData = new CityData();
        self::$_cityData =  $cityData->get(self::$_city, self::$_stateCode);
        
        if( count(self::$_cityData) < 1 )
            return false;

        return true;    
    }


    private static function onVirtualPageRequest() {
        self::$_seviceData['signature'] = self::$_service;
        self::$_virtualPage = new VirtualPageBuilder(self::$_seviceData, self::$_cityData);
    }
    
}

Vanilla JS + Ajax Multi-step Lead From

Built from scratch to help qualify the lead in a UI friendly manner

Instead of having a giant lead form with a million questions about the move details, I decided to created a muiti-step JavaScript driven form that would make inputting the details a more pleasant experience.

The form is also driven by the JavaScript Fetch API so no page reloading takes place. I am also using the History API to make the back button functional with the multistep form.

var moveLeadForm =  {
	
	enum_index: {
		STEP_MOVE_TYPE: 1,
		STEP_ROOM_COUNT: 2,
		STEP_EXTRA_SERVICES: 3,
		STEP_BULKY_ITEMS_DETAILS: 4,
		STEP_MOVE_TIME: 5,
		STEP_CONTACT_DETAILS: 6
	},
	
	lastInputStyle: null,
	
	init: function() {
		this.initEventListeners();
		
		// Run this now in case a browser has selected one in the cache
		this.onMoveTypeRadioSelect(false);
			
		this.lastInputStyle = document.querySelector('#contactDetails input[name="contact_name"]').style;
		
		history.pushState({formStep: 1}, null, document.location);
		
		document.getElementById('thankyou_gif').src = ms_form_help.thankyou_gif;
	},	
	
	initEventListeners: function() {
		[...document.querySelectorAll('.next-step-btn')].forEach((item, index) => {
			item.addEventListener('click', (e)=> { this.onNextBtnClick(e) });
		});
		
		[...document.querySelectorAll('.move-type-option input[type="radio"]')].forEach((item, index) => {
			item.addEventListener('click', (e)=> { this.onMoveTypeRadioSelect(e) });
		});
		
		document.querySelector('#ms-move-lead-form input[type="submit"]').addEventListener( 'click', (e) => { this.onFormSumbit(e) } );
		
		window.addEventListener('popstate', (e) => { this.onBackButton(e); });
	},
	
	getActiveFormSection: function() {
		return document.querySelector('#move-lead-widget form .form-part.active');
	},
	
	getCurrentIndex: function(btn) {
		let wrapper = btn.parentNode;
		let section = wrapper.parentNode;		
		return parseInt(this.getActiveFormSection().getAttribute('form_index'));		
	},

	goToFormStep: function(index) {
		this.getActiveFormSection().classList.remove('active');		
		
		[ ...document.querySelectorAll('#move-lead-widget form .form-part')].some( (el) => {
			if( parseInt(el.getAttribute('form_index')) === index ) {
				el.classList.add('active');
				history.pushState({formStep: index}, null, document.location);
				return true;
			}
		});	
	},
	
	onNextBtnClick: function(event) {
		event.preventDefault();
		this.clearErrors();
		
		let index = this.getCurrentIndex(event.target);	
				
		switch(index) {
			case this.enum_index.STEP_MOVE_TYPE:
				this.onMoveTypeStepSubmit();
				break;
				
			case this.enum_index.STEP_ROOM_COUNT:
				this.onRoomCountStepSubmit();
				break;			

			case this.enum_index.STEP_EXTRA_SERVICES:
				this.onExtraServicesStepSubmit();
				break;
				
			case this.enum_index.STEP_BULKY_ITEMS_DETAILS:
				this.onBulkyItemsDetailsSubmit();
				break;
				
			case this.enum_index.STEP_MOVE_TIME:
				this.onMoveTimeStepSubmit();
				break;
				
			case this.enum_index.STEP_CONTACT_DETAILS:
				this.onContactDetailsSubmit();
				break;				
		}
	},
	
	clearErrors: function() {
		[...document.querySelectorAll('#move-lead-widget .errors')].forEach( (el, index) => {
			el.style.display = 'none';
			el.innerHTML = '';
		});
	},
	
	getActiveErrorBox: function() {
		return document.querySelector('#move-lead-widget form .form-part.active .errors');
	},
	
	addError: function(errorStr) {
		this.getActiveErrorBox().style.display = 'block';
		this.getActiveErrorBox().innerHTML += `<p>${errorStr}</p>`;
	},
	
	onMoveTypeStepSubmit: function() {
		let valueSelected = this.getSelectedMoveType();
		
		if( valueSelected === null ) {
			this.addError('Please select one of the options above');
			return false;
		}		
		
		if( valueSelected === 'bulky items' )
			this.goToFormStep( this.enum_index.STEP_BULKY_ITEMS_DETAILS );
		
		// An option that requires room count selection
		else if( valueSelected !== 'storage' && valueSelected !== 'garage' )
			this.goToFormStep( this.enum_index.STEP_ROOM_COUNT );
		
		// room count not needed for this option
		else {
			this.setRoomCount(1);
			this.goToFormStep( this.enum_index.STEP_EXTRA_SERVICES );
		}
	},
	
	onRoomCountStepSubmit: function() {
		let value = this.getSelectedRoomCount();
		
		if( value === null ) {
			this.addError('Please select a room count above');
			return false;
		}
		
		this.goToFormStep( this.enum_index.STEP_EXTRA_SERVICES );
	},
	
	onExtraServicesStepSubmit: function() {
		this.goToFormStep( this.enum_index.STEP_MOVE_TIME );
	},
	
	onBulkyItemsDetailsSubmit: function() {
		this.goToFormStep( this.enum_index.STEP_MOVE_TIME );
	},
	
	onMoveTimeStepSubmit: function() {
		if( !this.getMoveDate() ) {
			this.addError('Please select an estimated move date above');
			return false;
		}	
		
		this.goToFormStep( this.enum_index.STEP_CONTACT_DETAILS );	
	},
	
	onContactDetailsSubmit: function() {
		
	},	
	
	onMoveTypeRadioSelect: function(event) {
		
		let valueSelected = null;
		
		// See if there is a current selected one if called without a click event
		if( !event ) {
			valueSelected = this.getSelectedMoveType();
		}
		
		else {
			valueSelected = event.target.value;		
		}
		
		if( valueSelected === null ) 
			return;
		
		// Add checked to div
		[...document.querySelectorAll('.move-type-option')].forEach((item, index) => {
			let radioBtn = item.querySelector('input[type="radio"]');
			
			if( radioBtn.value === valueSelected )
				item.classList.add('checked');
			else
				item.classList.remove('checked');				
		});
	},
		
	getSelectedMoveType: function() {
		let value = null;
		
		[...document.querySelectorAll('.move-type-option')].some((item) => {
			let radioBtn = item.querySelector('input[type="radio"]');
			
			if( radioBtn.checked ) {
				value = (radioBtn.value.length > 0) ? radioBtn.value : null;
				return true;
			}
		});
		
		return value;
	},
		
	getSelectedRoomCount: function() {
		let value = null;
		
		[...document.querySelectorAll('#room-count-btns input[type="radio"]')].some((radioBtn) => {			
			if( radioBtn.checked ) {
				value = parseInt(radioBtn.value);
				return true;
			}		
		});
		
		return value;
	},
	
	setRoomCount: function(roomCount) {
		let index = roomCount;
		
		if( index < 1 )
			index = 1;
		
		let selector = `#room-count-btns input[type="radio"]:nth-of-type(${index})`;
		
		if( index >= 5 )
			selector = `#room-count-btns input[type="radio"]:last-of-type`;					
				
		[...document.querySelectorAll('#room-count-btns input[type="radio"]')].some((radioBtn) => {			
			radioBtn.checked = false;
		});		
				
		let radioBtn = document.querySelector(selector);
		
		if( !radioBtn )
			return;
		
		radioBtn.checked = true;
	},

	setMoveDate: function(dateStr) {
		document.querySelector('#bookMoveTime input').value = dateStr;
	},
	
	getMoveDate: function() {
		return document.querySelector('#bookMoveTime input').value;
	},

	onMoveTimeSelect: function(dateStr, picker) {
		if( dateStr === undefined )
			return;
		
		this.setMoveDate(dateStr);		
	},
		
	validateText: function(str) {
		return !(str === null || str.length < 1);		
	},
	
	validateEmail: function(email) {
		return /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/.test(email);
	},
	
	validateUSPhone: function(phone) {
		if( /^[+]*[(]{0,1}[0-9]{1,3}[)]{0,1}[-\s\./0-9]*$/g.test(phone) !== true )
			return false;
						
		if( phone.match(/\d/g).length !== 10 )
			return false;
			
		return true;
	},	
	
	markInputInvalid: function(el) {
		el.style.border = 'red solid 1px';
		el.style.background = '#fff5f5';
		el.style.color = 'red';
	},
	
	validateContactDetails: function() {
		
		let name 		= document.querySelector('#contactDetails input[name="contact_name"]');
		let email 		= document.querySelector('#contactDetails input[name="contact_email"]');
		let phone 		= document.querySelector('#contactDetails input[name="contact_phone"]');
				
		name.style		= this.lastInputStyle;	
		email.style		= this.lastInputStyle;
		phone.style		= this.lastInputStyle;		
				
		if( !this.validateText(name.value) ) {
			this.markInputInvalid(name);
			this.addError('Please add a contact name for the job.');
			return false;
		}
		
		if( !this.validateText(email.value) ) {
			this.markInputInvalid(email);
			this.addError('Please add a contact email for the job.');
			return false;
		}
		
		if( !this.validateText(phone.value) ) {
			this.markInputInvalid(phone);
			this.addError('Please add a contact number for the job.');
			return false;
		}
		
		if( !this.validateEmail(email.value) ){
			this.markInputInvalid(email);
			this.addError('That doesn\'t appear to be a valid email. Please check your input and enter a valid email we can reach you on.');
			return false;
		}
		
		if( !this.validateUSPhone(phone.value) ) {
			this.markInputInvalid(phone);
			this.addError('That doesn\'t appear to be a valid phone number. Please enter a valid phone number we can reach you on.');
			return false;
		}			
		
		return true;
	},
	
	objectifyForm: function() {
		return Object.fromEntries(new FormData(document.getElementById('ms-move-lead-form')))
	},
	
	onFormSumbit: function(event) {
		event.preventDefault();
		
		this.clearErrors();
		
		if( !this.validateContactDetails() )
			return;
		
		this.wrapItUp();
	},
	
	wrapItUp: function() {
		
		let formEl = document.getElementById('ms-move-lead-form');
		let completedSection  = document.getElementById('quote-completed');
		
		this.postIt();
		
		formEl.style.display = 'none';
		completedSection.style.display = 'block';
	},		

	postIt: function() {
		
		let formData = this.objectifyForm();
		
		formData.action = 'ms-move-form-submit';
			
		fetch(ms_form_help.ajax_url, {
			method: 'post',
			credentials: 'same-origin',
			headers: {
				'Content-Type': 'application/x-www-form-urlencoded'
			},
			body: new URLSearchParams(formData).toString()
		});
	},
	
	onBackButton: function(event) {
		
		if( !event.state.formStep )
			return;
		
		let step = event.state.formStep;
		
		this.goToFormStep(step);
	}
	
};

WooCommerce Product Review Reminder Plugin

Customer Email Reminder After Purchase, Enhancements On Product Review Comments

A plugin created for an old eCommerce website to increase customer engagement and get feedback about the products. This plugin was built with features like:

  • Send a product review reminder email after x days of purchase
  • Customer email contains items in there last order to review
  • Links in product review were designed to auto login the customer to reduce drop-off engagement when customers forgot passwords etc. They just click on the link and it takes them straight to the review form ready to go.
  • The plugin remembers which users have reviews which products to avoid repeat review requests on products they have already reviewed in the past
  • Enhancements to the WooCommerce product review comment system. Fun avatars, ajax submit, microdata schema markup etc.
  • Customer opt out option to prevent and review email reminders being sent in the future

Custom Settings Placed In WooCommerce Settings Area

Customer Review Reminder Email

Email that the customer will see

<?php 
require_once('includes/settings.php');
require_once('includes/orderAPI.php');
require_once('includes/userAPI.php');
require_once('includes/linkController.php');
require_once('includes/functions.php');
require_once($pluginRootDir . 'includes/PhpMailer/DazMailer.php');


class DazWCProductReviewReminder {

	private static $_instance;
	private static $_pluginPath;

    public function __construct() {		
		self::$_pluginPath = plugin_dir_url( __FILE__ );

		$routerBase = Settings::getPluginRoute();
		$router = new LinkController( $routerBase );
		add_action('parse_request', array($router, 'checkRequest'));

		add_action( 'wp_enqueue_scripts', array($this, 'loadCss') );
		
		add_action( 'init', function() {
			add_action( 'wp_enqueue_scripts', array($this, 'loadJS') );
		});
	}


	/*
	 *	Main method to call
	 */
	public static function process() {

		if( !Settings::isActivated() ) { return; }

		// Get the days exempted from query setting
		$daysEx = Settings::getDaysExemptOption();	
		
		// Get all the unprocessed order IDs during this period
		$unprocessedOrders = OrderAPI::getUnprocessedOrders($daysEx);
		
		// Build all data for email reminder tasks that are due to be done
		$worklist = self::buildWorkList($unprocessedOrders);

		// Should we mark orders as processed? ( A debug switch )
		$markOffOrdersAsProcessed = !(WP_DEBUG && DPRR_DEBUG_DONT_MARK_ORDERS_PROCESSED);

		// Process each item in the worklist, email them, mark as processed
		self::processWorkList($worklist, $markOffOrdersAsProcessed);
	}


	/*
	 *	Processes the worklist of orders to process into reminder emails and send them
	 */
	public static function processWorkList(array $worklist, bool $markOrderProcessed = true) {
		// No work to do
		if( count($worklist) === 0 ) { return; }

		// Trick to load WC_Email class (this can be removed when custom email is done)
		WC()->mailer();
		require_once('templates/product-review-email.php');
		
		// Process the work list
		foreach($worklist as &$task) {

			// Build Email Template
			$html = self::buildEmailHtml($task);

			// Write emails to files for debugging
			if( WP_DEBUG && DPRR_DEBUG_WRITE_EMAILS_TO_FILES ) {
				$fhnd = fopen( sprintf('%s/tests/htmlDump/%d-email.html', plugin_dir_path( __FILE__ ), $task['userID']), "w" );
				fwrite($fhnd, $html);
				fclose($fhnd);
			}

			// Send emails (production mode)
			else {
				if(WP_DEBUG && DPRR_DEBUG_OVERRIDE_RECIPIENT_EMAIL) { 
					$task['email'] = DPRR_DEBUG_EMAIL_ADDRESS; 
				}

				$subject = sprintf('Hey %s - How was your order?', $task['firstName']);

				self::sendMail($task['email'], $subject, $html);
			}	

			// Mark order as processed?					
			if( $markOrderProcessed ) {
				$task['orderAPI']->markOrderProcessed();
			}
		}
	}

	
	/*
	 *	Builds the worlist data from unprocessed orders
	 */
	public static function buildWorkList(array $ordersIDs): array {
		
		$worklist = [];
	
		foreach($ordersIDs as $oid) {
			
			$orderapi = new OrderAPI($oid);

			$userID = $orderapi->getUserID();
			$userapi = new UserAPI($userID);

			// skip user if they have opted out
			if( $userapi->isOptedOutOfProductReviews() ) { continue; }

			// Get the product IDs this user has not reviewed
			$orderProductIDs = $orderapi->getProductIDs();
			$userHasNotReviewedIDs = $userapi->getUnreviewedIDs($orderProductIDs);

			if( count($userHasNotReviewedIDs) === 0 ) { continue; }

			// Add product IDs to user in work list
			self::addToWorkList($userapi, $orderapi, $userHasNotReviewedIDs, $worklist);			
		}	
		
		// Remove users with no product IDs
		foreach($worklist as $key => &$task) {
			if( empty($task['productIDs']) ) { unset( $worklist[$key] ); }
		}

		return $worklist;
	}


	/*
	 *	Builds the work list data structure to compile emails for each user
	 */
	public static function addToWorkList(UserApi $user, OrderApi $order, array $productIDs, array &$worklist) {
		if( count($productIDs) === 0 ) { return; }

		// Get User ID
		$userID = $user->getID();

		// Hash user id for array index key, to help keep userID unique
		$hashedID = self::hashNum($userID); 

		// Set up data struct if not set yet for this user
		if( !isset( $worklist[$hashedID] ) ) {
			$worklist[$hashedID] = [
				'userID' => $userID,
				'firstName' => $order->getFirstName(),
				'email'	=> $order->getEmail(),
				'orderAPI' => $order,
				'productIDs' => [],
				'wcProducts' => []
			];
		}

		// Merge existing user's un-reviewed product IDs with passed un-reviewed IDs
		$mergedIDs = array_merge( $worklist[$hashedID]['productIDs'], $productIDs );

		// Remove duplicates
		$uniqueIDs = array_unique( $mergedIDs );
		
		// Check which IDs are unique new in this call
		$newUniqueIDs = array_diff($uniqueIDs, $worklist[$hashedID]['productIDs']);

		// Make a list of ID's we should not include, because user already reviewed their parent product ID
		$excludeIDs = [];

		// Get WC_Product objects for each new unique ID
		foreach($newUniqueIDs as $_pID) { 
			$wc_product = wc_get_product($_pID);

			// Check if product has parent ID
			$parentID = $wc_product->get_parent_id();
			if( $parentID == 0 || $parentID == null || $parentID == false ) { continue; }

			// Check if user already reviewed parent product ID
			$userReviewParent = $user->didUserReviewProduct($parentID);

			// If user already reviewed parent, add variation ID to exclude list
			if( $userReviewParent ) {
				$excludeIDs[] = $_pID;
				continue;
			}

			// Else add the WC_Product obj to the work list
			$worklist[$hashedID]['wcProducts'][] = $wc_product;
		}

		// Filter excluded Ids
		$uniqueIDs = array_diff($uniqueIDs, $excludeIDs);

		// Replace new unique ID list to user task data
		$worklist[$hashedID]['productIDs'] = $uniqueIDs;		
	}


	/*
	 *	Hash a number
	 */
	protected static function hashNum(int $num): string {
		return hash('sha384', $num);
	}


	/*
	 *	Builds the email html for each user
	 * 	$details[
	 * 		'userID'
	 * 		'firstName'
	 * 		'email'
	 * 		productIDs[]
	 * 		wcProducts[]
	 *  ]
	 */
	protected static function buildEmailHtml(array $details): string {
		//$templateFilePath = sprintf('%s/templates/%s', plugin_dir_path(__FILE__),$templateFile);

		//if( !file_exists($templateFilePath) ) { return ''; }
		$customerEmail = new Product_Review_Email($details);
		
		$html = $customerEmail->get_content_html();
		return $html;
	}


	/*
	 *	Build the product review link with review tab attached
	 */
	public static function buildProductReviewLink(\WC_Product $product): string {
		$productPermalink = $product->get_permalink();		
		$reviewTabId = Settings::getReviewTabID();

		// Tell JavaScript to focus on the review form
		$gotoFlag = 'revform';

		// Has query string, don't add trailing slash
		if( parse_url($productPermalink, PHP_URL_QUERY) ) {			
			$product_url = sprintf("%s&varid=%d&goto=%s#tab-%s", $productPermalink, $product->get_id(), $gotoFlag, $reviewTabId);
		}

		// Has query string, add trailing slash
		else {
			$product_url = rtrim($product_url, '/');
			$product_url = sprintf("%s/?varid=%d&goto=%s#tab-%s", $productPermalink, $product->get_id(),$gotoFlag, $reviewTabId);
		}

		return $product_url;
	}



	/*
	 *	Sends the email
	 */
	public static function sendMail(
		string $to,
		string $subject,
		string $bodyHtml,
		string $inPlainTxt = ''
	) {
		...
	}
	
	...
}

Enhancements To Product Review Comments

Show the product variant that was reviewed, ajax enhancements, avatar reaction face based of review rating

<?php

const COMMENT_VARIATION_MKEY = 'variation-reviewed';

/*
 *  Add Product variation selection to existing product review form
 */
add_action( 'comment_form_logged_in_after', 'add_variation_select_field_on_comment_form' );
add_action( 'comment_form_after_fields', 'add_variation_select_field_on_comment_form' );

function add_variation_select_field_on_comment_form() { 
    
    // If is not a single product page, abort
    if( !is_product() ) { return; }
    
    $product = wc_get_product();

    // Not a variable product
    if( !$product instanceof \WC_Product_Variable ) { return; }
    
    $variations = $product->get_available_variations(); 
    
    // If there are no variations, abort
    if( empty($variations) ) { return; }

    //foreach($variations as $variation) { dumpVar($variation); }

    $loadedVariation = ( isset( $_GET['varid'] ) ) ? (int) $_GET['varid'] : 0;
    
    // Build the select html for varations
    ?>
        <label for="<?php echo COMMENT_VARIATION_MKEY; ?>">Select which <?php echo $product->get_name(); ?> variation you would like to review:</label>
        <select id="<?php echo COMMENT_VARIATION_MKEY; ?>" name="<?php echo COMMENT_VARIATION_MKEY; ?>" required>
            <option value="">Please Select A Variation</option>
            <option value="<?php echo $product->get_id(); ?>">All <?php echo $product->get_name(); ?> Variations</option>
            <?php foreach($variations as $variation):
                
                if( !$variation['variation_is_visible'] || !$variation['variation_is_active'] ) { continue; }
                $variationID = (int) $variation["variation_id"];
                $imgsrc = $variation['image']['thumb_src'];
                $name = '';
                foreach($variation['attributes'] as $k => $v) { $name .= $v . ' '; } 
                $name = rtrim($name);
                
                // Is this option the loaded variation
                $selected = ( (int) $variationID === $loadedVariation ) ? 'selected' : '';                
                ?>
                <option value="<?php echo $variationID; ?>" <?php echo $selected; ?>><?php echo $name; ?></option>
            <?php endforeach; ?>
        </select>
        <script>
            jQuery(document).ready(function() {  
                jQuery('#submit').on('click', function(event) {
                   
                    let variationSelect = document.getElementById("<?php echo COMMENT_VARIATION_MKEY; ?>");
                    let variationCurrentVal = parseInt( variationSelect.options[ variationSelect.selectedIndex ].value) ;

                    // Force user to select a variation
                    if( Number.isNaN(variationCurrentVal) || variationCurrentVal <= 0 ) {
                        event.preventDefault();                     
                        alert("Please select a variation of this product to review.");
                        return false;
                    }
                });
               
                jQuery(window).load(function() {
                    const lookingForParam = "goto";
                    const urlParams = new URLSearchParams(window.location.search);
                    const gotoAction = urlParams.get(lookingForParam);
                    const jumpToID = "<?php echo Dazamate\DazWCProductReview\Settings::getReviewFormID(); ?>";    
                    
                    if( gotoAction == 'revform' ) {
                        const revForm = document.getElementById(jumpToID);
                        
                        jQuery([document.documentElement, document.body]).animate({
                            scrollTop: jQuery("#" + jumpToID).offset().top - 100
                        }, 1000);                        
                    }                     
                });
            });
        </script>
    <?php 
}


/*
 *  Update comment and user meta after successful product review
 */
add_action( 'comment_post', function( $comment_ID, $comment_approved ) {
    
    $wpComment = get_comment($comment_ID);

    if( isset($_POST[COMMENT_VARIATION_MKEY]) && $_POST[COMMENT_VARIATION_MKEY] > 0 ) {
        
        // Update user meta
        $userapi = new Dazamate\DazWCProductReview\UserAPI( $wpComment->user_id );
        $userapi->setProductReviewed( $_POST[ COMMENT_VARIATION_MKEY ] );
        
        // Update comment meta
        update_comment_meta( $comment_ID, COMMENT_VARIATION_MKEY, $_POST[ COMMENT_VARIATION_MKEY ] );

        // Check if we can send email to admin about product
        if( DPRR_DEBUG_WRITE_EMAILS_TO_FILES ) { return; }

        // User selected not to have emails sent
        if( !Dazamate\DazWCProductReview\Settings::sendAdminEmailOnReviewSubmit() ) {
            return;
        }

        // Check if we have a WC_Product obj available
        $wcProduct = wc_get_product($_POST[COMMENT_VARIATION_MKEY]);
        if( !is_object($wcProduct) ) { return; }

        // Mail admin about new comment
        $admin_email = get_option('admin_email');

        $html = sprintf('<p>%s has left a new product review about %s<p><br><br><p>%s</p>', 
            $wpComment->comment_author, 
            $wcProduct->get_name(), 
            $wpComment->comment_content 
        );

        $html .= '<br>';
        $html .= sprintf('<p>Product Page Link: <a href="%s">%s</a><p>',
            $wcProduct->get_permalink(), 
            $wcProduct->get_name()
        );

        $headers = array('Content-Type: text/html; charset=UTF-8');
        wp_mail($admin_email, $wpComment->comment_author . ' has submitted a product review!', $html, $headers);
    }  

}, 10, 2 );



/*
 *  Add "product reviewed" data to the comment
 */
add_filter( 'comment_text', function( string $comment_text, $comment, $args ) {
    
    // Comment not approved or not a single product page
    if( $comment->comment_approved != 1 ) { return $comment_text; }

    // Get variation id
    $variationReviewed = get_comment_meta( $comment->comment_ID, COMMENT_VARIATION_MKEY, true );

    // No meta data stored to work with
    if( $variationReviewed === false || $variationReviewed <= 0 ) { return $comment_text; }

    // Get product 
    $wcProduct = wc_get_product($variationReviewed);

    // Check if product object is valid
    if( !$wcProduct instanceof \WC_Product ) { return $comment_text; }

    // Get product thumbnail img
    $product_thumb = $wcProduct->get_image([64,64]);    

    // Create author name
    $author = '<b>' . apply_filters( 'comment_author', $comment->comment_author, $comment->comment_ID ) . '</b>';

    // Is variation
    if( $wcProduct->get_parent_id() > 0 )  { 
        $author .= ' reviewed the ' . $wcProduct->get_name();
    }

    // Is parent of variations
    else if( $wcProduct->has_child() > 0 ) {
        $author .= ' reviewed all ' . rtrim($wcProduct->get_name(), 's') . 's';
    }

    // assume single product
    else {
        $author .= ' reviewed ' . $wcProduct->get_name();
    }

    if( is_product_category() ) {
         // Category name
        $catName = get_queried_object()->name;
        $author .= sprintf(' in <em>%s</em>', $catName);        
    }

    if( is_product() ) {
        // Produxct name
        $author .= sprintf(' in <em>%s</em>', $wcProduct->get_title() );   
    }

    // Build html
    ob_start(); ?>
    <div class="product-variation-reviwed">
        <div class="pvr-img">
            <?php echo $product_thumb; ?>
        </div>

        <div class="pvr-desc">
            <p><?php echo $author; ?></p>
        </div>
    </div>
    <?php 
    
    $prepend = ob_get_clean();

    // Prepend the comment meta info to the comment html
    $comment_text = $prepend . $comment_text;

    return $comment_text;
}, 10, 3);



/*
 *  Auto approve comments from registered customers
 */
add_filter( 'pre_comment_approved', function($approved, $commentdata) {
    
    // Already approved, skip logic
    if( $approved === 1 ) { return $approved; }

    // Skip if user opted out of auto approve feature
    if( !Dazamate\DazWCProductReview\Settings::autoApproveCustomerReviews() ) { return $approved; }

    // Make sure we have a user id (set to 0 if no user) and we are reviewing a product
    if( $commentdata['user_ID'] > 0 && $commentdata['comment_type'] === 'review' ) { return 1; }

    return $approved;
}, '99', 2 );




/*
 *  --- ADMIN AREA HOOKS ---
 */

/*
 *  Editing comment meta in the admin section
 */
add_action( 'edit_comment', function($comment_id) { 
   
    if( !wp_verify_nonce( $_POST['dprr-variation-id-updated'], 'dprr-variation-id-updated' ) ) { return; }
    if( isset( $_POST[ COMMENT_VARIATION_MKEY ] ) && $_POST[ COMMENT_VARIATION_MKEY] > 0 ) {        
        
        // Update comment meta
        update_comment_meta( $comment_id, COMMENT_VARIATION_MKEY, $_POST[ COMMENT_VARIATION_MKEY ]);
    }    
});


/*
 *  Add an edit option to comment editing screen 
 */
add_action( 'add_meta_boxes_comment', function() { 
    add_meta_box( 'dprr-meta-box', 'Product Variation Reviewed', 'product_reviewed_comment_edit_meta_box', 'comment', 'normal', 'high' );
});


/*
 *  Add the html and form fields inside the meta box 
 */
function product_reviewed_comment_edit_meta_box( $comment ) {
    $productVariationID = get_comment_meta( $comment->comment_ID, COMMENT_VARIATION_MKEY, true );    
    wp_nonce_field( 'dprr-variation-id-updated', 'dprr-variation-id-updated', false ); ?>
    <p>
        <label for="add_comment_met">Product Variation Reviwed</label>
        <input type="text" name="<?php echo COMMENT_VARIATION_MKEY; ?>" value="<?php echo esc_attr( $productVariationID ); ?>" class="widefat" />
    </p>
    <?php
}

AJAX Contact / Customer Support Form

Contact 7 Alternative, WooCommerce Order Helper, A Lite Contact Form

Contact form 7 is a pain in the ass. It’s heavy, loads scripts on all pages and is always found to have exploits that hackers use to get into people’s WordPress sites.

When I was running an eCommerce store, the biggest annoyance was people sending support emails with questions about their order without providing any details to look their information up.

This simple customer support plugin was designed to:

  • Reduce customer support anxiety by requiring the customer to fill out the order number they were sending an email about
  • Make sure the form checked the email in the support form against the one on the order and only allow emails to be sent if the emails matched (as a soft verification that was actually the customer’s order and not a typo or something)
  • Provide all the order details to the person answering the support email
  • Custom email server option and other settings

Simple Ajax Customer Support Form

Detailed Support Email For Staff

WooCommerce Accounting Plugin

Invoicing, Low Stock Alerts, Product Ownership, Commission Payout, Most Profitable Products

Me and a friend went into an eCommerce business together. We both bought stock, shared expenses and duties.

The goal of this accounting plugin was to calculate what the eCommerce business owed each of us, our profit split and analytics to help us along the way.

  • Send a weekly email to all stake holders with a breakdown of sales, profits and payouts in pdf format
  • Provide analytics on the best selling products and most profitable products for that time period
  • Provide a low on stock alert at the product variations level
  • A customer waiting list for those who were waiting for products to come back in stock

PDF Invoice Generated

+ A Whole Lot More

To prevent this page getting too long and causing a browser fire 🔥, here is a list of some other things I’ve done with WordPress

  • Offline Google Ads conversion (acquire customer online but convert manually in psychical store)
  • Seo optimization hacks/plugins
  • WooCommerce back in stock notifications
  • Custom ajax comment system for WordPress theme
  • Comment caching system
  • 3rd party payment integration
  • Google analytics, Google Ads and Facebook conversion tracking

Thanks for Checking Out My WordPress/WooCommerce Portfolio

I look forward to the opportunity of working with you soon. If you have any questions, please don’t hesitate to contact me below.

My Mobile

0431 057 601