A lot of WordPress plugins, including Easy Digital Downloads and WordPress SEO offer discount codes in exchange for allowing anonymous usage tracking. It’s a smart offer to make as the data is valuable. But, it’s also a great option for users of the plugin.
This article is based on my experience setting up such a system for my plugin Caldera Forms. I will show you how I set up Easy Digital Downloads (EDD) on our site to dynamically generate the discount codes. I will also show how I set up the emails that provide the codes.
This should be a useful tutorial for those looking to learn how to work with EDD discounts programmatically and for a practical example of how WordPress’ psuedo-CRON system works.
Though I’m not covering it in this article, you may be interested in learning how to capture user emails of your plugin. For more information, you can check out posts from Freemius and WordImpress.
Getting Started
To begin, you will need to integrate a simple form into your site that makes a POST request to an external URL and sends the user’s email address along. Make sure that form is well integrated with your user interface and that you have an option to decline tracking that prevents the offer from being shown again.
Once that’s ready, create a must use plugin (mu-plugin) on your site. I like using a mu-plugin for this as it allows me to act very early. This reduces the amount of time the request takes. When someone opts into usage tracking, that request to the Caldera Forms site happens all in the background and there is no need to load the theme or anything.
Also, I always want this system to be turned on, thus “must use” plugin.
Before you start, make sure to create two HTML files for your emails. I used Bee to create the HTML for my email. The important thing to do is use an easily substituted placeholder in your email for the actual code.
The System
This is a pretty simple system, so I built mine all in one file of the mu-plugin. Your’s may get more sophisticated and require a directory, and that’s OK. Just remember, mu-plugins don’t load like regular plugins, you need a file in the root of mu-plugins that includes files in the folder.
The first version of what I did only had two functions, both of which I built in. Later I added tracking of emails in a custom table. I did that for two reasons. First I wanted conversion tracking and two I needed a way to prevent multiple emails from being sent to the same person. For now, I’ll just show you the basics.
The first function, which is hooked into it does four things. First, it validates the incoming request, then if that passes it creates the discount code. Then it sends an email with that code in it, and then it schedules the second email to be sent in a week. The second function is the callback for the wp-cron event that sends the second email.
Before I cover how these work, let’s do a quick primer on WP-CRON which is essential to this system.
WP-CRON Basics
Servers have a system called CRON that is used for scheduling tasks that need to run at a later date. WP-CRON is a system built into WordPress designed to handle scheduled tasks in a CRON like fashion.
The important distinction is that because CRON is a program running on your server it can run a task at exactly the time or interval it is set at. WP-CRON is part of your site and it can only run when a request is set. Therefore it’s important to keep in mind that when scheduling a WP-CRON event, you’re not saying “do this at X time.” You’re saying “do this as soon as possible after X time” and the earliest that will happen is after the first request to your site that happens after X time has passed.
WP-CRON is built around the WordPress hook system, but with delayed actions. Normally when you use add_action() the callback you use is executed in the same session, as soon as do_action() is called for that hook. With WP-CRON, you still use add_action for your callback but you specify a delay until that action is triggered.
Think of this as wp_schedule_single_event() as a delayed action. Instead of doing the action right away, you do it sometime in the future. Take a look at this example:
<?php //do the action ten_minutes_later_hook 600 seconds from now -- IE ten minutes. wp_schedule_single_event( time() + 600, 'ten_minutes_later_hook' ); /** The CRON Callback */ add_action( 'ten_minutes_later_hook', function(){ //it is now ten minutes later });
This is a one-time event, we could have also used wp_schedule_event() to create a recurring task, but that’s not needed here.
The other thing we could have done is passed data to the callback. That is done with the third argument of wp_schedule_single_event(). Keep in mind, this data gets stored in the database, and therefore affects performance. So if you need a WP_Post object in the event callback, it is better to just pass post ID, and get the post object back in the callback, like in this example, which will change a post’s status 4 weeks after it was created:
<?php add_action( 'save_post', 'my_update_post' ); function my_update_post( $post_id ) { if ( wp_is_post_revision( $post_id ) ){ return; } wp_schedule_single_event( strtotime("+1 week"), 'my_change_status' ); } add_action( 'my_change_status', 'my_change_status' ); function my_change_status( $args ){ if( isset( $args[0] ) && is_object( $post = get_post( $args[0] ) ) ){ $post->post_status = 'old'; wp_update_post( $post ); } }
The First Email
The first email is sent as soon as the initial request is sent. As I said earlier, we will need to validate the incoming request and create an EDD discount code.
Discount codes in EDD are generated using the function edd_store_discount(). Here is how I create a 10 percent discount code, using a random string. Notice that I use the user’s email address as the name of the discount. This was a design decision I made for tracking purposes, it is not technically necessary.
<?php add_action( 'init', function(){ //Don't do anything if wrong type of request if( ! isset( $_POST[ 'optin-email' ] ) ){ return; } //Return error if bad email if( ! is_email( urldecode( $_POST[ 'optin-email' ] ) ) ){ wp_send_json_error( [ 'error' => 'email-invalid' ] ); } //Sanitize email $email = sanitize_email( urldecode( $_POST[ 'optin-email' ] ) ); //Create random discount code $pool = array_merge(range(0,9), range('a', 'z'),range('A', 'Z')); $code = ''; for($x= 0; $x < 3; $x++) { for ( $i = 0; $i < 5; $i ++ ) { $code .= $pool[ mt_rand( 0, count( $pool ) - 1 ) ]; } $code .= '-'; } $code = 'thanks-' . rtrim( $code, '-' ); //Set expiration time -- 604800 == week in seconds $expires = time() + ( 2 * 604800 ); //create code and get it's ID $details = array( 'code' => $code, 'name' => $email, 'status' => 'active', 'is_single_use' => 1, 'amount' => '10', 'expiration' => date( 'm/d/Y H:i:s', $expires ), 'type' => 'percent', 'max' => 1, 'uses' => 1 ); $id = edd_store_discount( $details, null ); //Check for error if( ! is_numeric( $id ) ){ wp_send_json_error( ['error' => 'Failed to create discount' ] ); } /** SEND EMAIL HERE **/ });
This takes us to the point where we have our discount code and its ID. We also have a sanitized email address — remember trust no inputs. Now we need to add in sending of the email and scheduling an event:
<?php add_action( 'init', function(){ //Don't do anything if wrong type of request if( ! isset( $_POST[ 'optin-email' ] ) ){ return; } //Return error if bad email if( ! is_email( urldecode( $_POST[ 'optin-email' ] ) ) ){ wp_send_json_error( [ 'error' => 'email-invalid' ] ); } //Sanitize email $email = sanitize_email( urldecode( $_POST[ 'optin-email' ] ) ); //Create random discount code $pool = array_merge(range(0,9), range('a', 'z'),range('A', 'Z')); $code = ''; for($x= 0; $x < 3; $x++) { for ( $i = 0; $i < 5; $i ++ ) { $code .= $pool[ mt_rand( 0, count( $pool ) - 1 ) ]; } $code .= '-'; } $code = 'thanks-' . rtrim( $code, '-' ); //Set expiration time -- 604800 == week in seconds $expires = time() + ( 2 * 604800 ); //create code and get it's ID $details = array( 'code' => $code, 'name' => $email, 'status' => 'active', 'is_single_use' => 1, 'amount' => '10', 'expiration' => date( 'm/d/Y H:i:s', $expires ), 'type' => 'percent', 'max' => 1, 'uses' => 1 ); $id = edd_store_discount( $details, null ); //Check for error if( ! is_numeric( $id ) ){ wp_send_json_error( ['error' => 'Failed to create discount' ] ); } //get discount code $discount_code = edd_get_discount_code( $id ); //setup email $from_name = 'Your Name'; $from_address = '[email protected]'; $headers = "From: {$from_name} <{$from_address}>\r\n"; $headers .= "Reply-To: {$from_address}\r\n"; $headers .= "Content-Type: text/html; charset=utf-8\r\n"; ob_start(); include( dirname( __FILE__ ) . '/emails/email-first.html' ); $message = ob_get_clean(); $message = str_replace( '{CODE}', $discount_code, $message ); $sent = wp_mail( $email, 'Your Discount Code', $message, $headers ); if( $sent ) { //schedule reminder email $send = $expires - 82800; wp_schedule_single_event( $send, 'optinemail', [ $discount_code, $email ] ); wp_send_json_success( [ 'success' => true, 'error' => false ] ); }else{ wp_send_json_error( [ 'email' => 'Email did not send.' ] ); } });
This schedules the event optinemail to run 12 days later. That event is going to run once per user, but each time, it will have a different email address and discount code, which were passed to the third parameter of wp_schedule_single_event().
The Second Email
Now that the event will be scheduled, we need a callback function. It will need to be hooked, using add_action to the “optinemail” hook. In our callback, we can expect two arguments — email and discount code.
I said “expect” because it is never safe to assume that any program is working right or any data coming out of the database is correct. That’s why we will want to validate and sanitize the email and code first, before sending the second email.
<?php add_action( 'optinemail', 'send_second_email' ); function send_second_email( $args ){ if( isset( $args[0] ) && is_email( $args[0]) ){ $email = sanitize_email( $args[0] ); }else{ return; } if( isset( $args[1] ) && is_string( $args[1] ) ){ $code = strip_tags( $args[1] ); }else{ return; } ob_start(); include( dirname( __FILE__ ) . '/emails/email-day-before.html' ); $message = ob_get_clean(); $message = str_replace( '{CODE}', $code, $message ); $from_name = 'Your Name'; $from_address = '[email protected]'; $headers = "From: {$from_name} <{$from_address}>\r\n"; $headers .= "Reply-To: {$from_address}\r\n"; $headers .= "Content-Type: text/html; charset=utf-8\r\n"; $sent = wp_mail( $args[1], 'Last chance to use your Caldera Forms discount code!', $message, $headers ); }
Don’t Forget To Track
If you opt into Caldera Forms usage tracking, the email you receive has a lot of links. Every one of them has the right UTM tags for Google Analytics tracking. That is the minimum amount of tracking I would recommend.
I hope you will use this article and its example code as a start point for your own system. Make sure you’re providing real value to your users, tracking the results and not being spammy!
4 Comments