EDD Stripe v3.0 Sticks it to Ya

Easy Digital Downloads (EDD) seems to be going the route of Freemius, etc., where you can use their open source plugin(s), but not without significant overhead. Except Freemius isn’t open source. 🤷‍♀️ With only a small share of the WP users needing file management plugins using their plugin and not a competitor’s, it’s a surprising move.

Stripe Gets Pricey

If you’re not paying for their annual license, or let your license term lapse, and wish to take payments with Stripe, you’re going to pay Stripe fees, plus a 3% “application fee” which EDD funnels off your sale. This fee used to be 2% until v.3.0. Now 3% — that’s significant!

With version 3.0 of EDD Stripe, EDD moved their entire Stripe integration over to Stripe Connect, which means all the sales on your private site go through Easy Digital Downloads Connect account first. Years ago the plugin used Stripe Charge API, then moved to Payments. Now this. There’s no going back. Your EDD Stripe plugin must be licensed ($$$) or you will be paying ~ 6% on every transaction.

But there’s a pretty simple way around all this if you have the plugin pre-version 3.0 … Version was 2.9.6 the latest before the jump. Add the following PHP code to your child theme functions.php file or add it using a plugin like WP Code Snippets (front and back end).

function edds_pro_edit_intent_args( $intent_args ) {
    if ( isset( $intent_args['application_fee_amount'] ) ) {
		    unset( $intent_args['application_fee_amount'] );
	  }
	  return $intent_args;
}
add_filter( 'edds_create_payment_intent_args', 'edds_pro_edit_intent_args', 99, 1 );
add_filter( 'edds_create_setup_intent_args', 'edds_pro_edit_intent_args', 99, 1 );

add_filter( 'edds_show_stripe_connect_fee_message', '__return_false' );

If you’re using the Stripe gateway plugin version 3.0 or newer, you’ll want to take a look at the has_application_fee() method in Src/Gateways/Stripe/ApplicationFees.php … and see how they’ve got it all wrapped up tight now. ⛓️ There are no filter hooks that can be used to wiggle out of that 3% fee. However, that method can be edited to get where you need. And would have to be edited again with every plugin update.

If you’d like the help of a developer for this issue, please get in touch.

Fix Easy Digital Downloads to Increase Sales

Over the years I’ve sure appreciated having Easy Digital Downloads (EDD) plugin to platform my plugin sales. It’s pretty cool, and handles most my needs including software licensing.

However, I have had numerous little issues with the plugin. I apply each update with trepidation, because often changes (“upgrades”) cause abrupt slumps in my sales. Of course the developers are well-meaning, and I’m sure these upgrades are in fact upgrades for certain people. But it’s becoming more and more clear that even though I’m a paying subscriber of EDD, their developers might not be “in touch” with the my reality or my customers’ realities.

There are things a customer just. does. not. want. to. see. when. shopping.

Case in Point

With the latest updates to the Stripe payment gateway plugin, EDD moved to the new API and allows small text to appear on screen just below the credit card fields, which easily would scare off a good number of customers:

“By providing your card information, you allow [Business Name] to charge your card for future payments in accordance with their terms.”

Oh dear God! How long has my website been telling people THAT? “Their terms?” What terms? Where does the customer now go to find those terms? Away from your checkout, that’s where.

Let’s Fix THAT

I know that EDD is stressing the subscription model and has a strong integration with Stripe for that model, but for those of us running EDD shops without subscription, this kind of small print is… not great. Luckily there’s an easy fix, but it does involve a few lines of PHP

add_filter( 'edds_stripe_payment_elements_terms', function( $terms ) {
    $terms['card'] = 'never';
    $terms['applePay'] = 'never';
    $terms['sofort'] = 'never';
    $terms['cashapp'] = 'never';
    $terms['bancontact'] = 'never';
    // view more methods here: https://stripe.com/docs/js/elements_object/create_payment_element#payment_element_create-options-terms 
	return $terms;
} );

Other payment methods might need to have these off-putting terms turned off too. You can view their keys here: https://stripe.com/docs/js/elements_object/create_payment_element#payment_element_create-options-terms

My Verdict

The card terms should be set to “never” by default, and set to the other ☠️☠️☠️ option when EDD store subscriptions are enabled. Developers should be serving the majority of their users, esp. their paying users. I can’t imagine that most EDD users are using the subscription model. It’s expensive, complex and controversial amongst merchants and customers.

After various other SNAFUs over the years, makes me very hesitant to apply site upgrades on anything but a development server. This is frustrating. I just don’t like the prospect of combing through upgrades to review UX each time. I’m paying them so it just doesn’t seem like it should be my job. </tirade>

Easy Digital Download Confusion

Once a customer has purchased more than one download from your Easy Digital Downloads (EDD) e-commerce shop, and especially if they have renewed their purchases year-over-year, there will be a source of confusion for them in their account pages, if/when they go to fetch a download file.

The list of downloads will include duplicate links in table rows, and the customer might not know which link to click to get the latest file. In fact, all the links with the same title are the same download. But how are they to know that? Why should they have to suss all that out, when it should just be a quick grab-n-go?

I get asked about this repeatedly, so I decided to code in a fix. I initially thought I’d solve this problem using the ‘edd_before_download_history‘ action hook in the ‘easy-digital-downloads/templates/history-downloads‘ template file, but that action hook doesn’t include the $orders variable, and that’s a shame because I can’t conditionally show a message if the customer has more than one download, e.g.

if ( count( $orders ) > 1 ) {
    // Then show an awkward message, like:
    echo '

awkward message explaining how all the links below with the same title are actually to the same file.

'; }

I’d have to put my awkward message in the template if I wanted to be at least minimally awkward about my message (I mean, only showing it to customers with more than one order). Sigh.

And so I ended up uploading a template override after all, which I try to avoid because updating templates every time EDD updates the parent templates is a pain. That said, I’ve updated EDD and WooCommerce templates so many hundreds of times I could almost do it with my eyes closed. That doesn’t mean it’s fun. I wish EDD would include some $variables in more of its action hooks.

Here’s what I did. I added some logic such that the $orders variable contains orders by DATE, in descending (DESC) order. That way, when the orders are looped through in the template, as they already are, I can collect item product IDs in an array. If that item product ID is already shown, I won’t show it again:

First, add two lines after line 31, to add to the edd_get_order query args:

'type'           => 'sale',
'status__not_in' => array( 'trash', 'refunded', 'abandoned' ),
'order'          => 'DESC', // add this line
'orderby'        => 'date_created', // add this line, too!

Next, add one line after line 51, so it looks more like this:

$prod_ids = [];
foreach ( $orders as $order ) :

Finally, add 6 lines of code after line 53, so it looks like this:

foreach ( $order->get_items_with_bundles() as $key => $item ) :

    if ( in_array( $item->product_id, $prod_ids ) ) {
        continue; // download link is already shown
    }
    if ( $item->is_deliverable() ) {
        $prod_ids[] = $item->product_id;
    }

Makes it much easier for a customer to just get their download without guesswork, which is what EDD is all about, right?

Prorated Software Upgrade Crack

It’s amazing how some customers’ behavior can highlight innovative ways of saving money.

It never occurred to me until I started selling software, and managing annual software licenses, that there’s a pretty simple way to save money when trying to renew software licenses, without a coupon. Many businesses pro-rate license upgrades, and prorate them by date, such that the closer you come to expiration, the cheaper an upgrade gets. So a frugal move to make is to wait until just days before your software license expires, then upgrade to the next level up… for a tiny fraction of what it would cost to renew the lesser license currently held. Voilà – we now have better access for another year, for cheaper than a straight renewal!

While this is pretty cool for the customer, this is a big problem for software sellers that causes significant loss of income. In particular it’s a weakness for some Easy Digital Downloads (EDD) admins using EDD’s Software Licensing extension. I wrote a PHP code snippet for use with EDD Software Licensing. It locks out prorated upgrades after a certain number of months of license ownership (3 months):

/**
 * Customers lose prorate upgrades if less than 9 months left on license
 *
 * @param float|int $prorated Calculated prorated price
 * @param int $license_id ID of license being upgraded
 * @param float|int $old_price Price of the original license being upgraded
 * @param float|int $new_price Price of the new license level
 * @return float|int
 */
function my_get_pro_rated_upgrade_cost( $prorated, $license_id, $old_price, $new_price ) {

    if ( ! class_exists( 'EDD_Software_Licensing' ) ) {
        require_once WP_PLUGIN_DIR . '/edd-software-licensing/includes/classes/class-edd-software-licensing.php';
    }
    // Get license object from license ID
    $license = EDD_Software_Licensing()->get_license( $license_id );
    if ( 'lifetime' === $license->expiration ) {
        return $prorated; // why lifetime licenses should get caught in this hook is beyond me
    }
    // 23670000 is the number of seconds in 9 months, change ad lib
    if ( ! is_object( $license ) || $license->is_expired() || ( ! $license->is_expired() && ( (int) $license->expiration - time() < 23670000 ) ) ) {
        return $new_price; 
    }
    return $prorated;

}
add_filter( 'edd_sl_get_pro_rated_upgrade_cost', 'my_get_pro_rated_upgrade_cost', 11, 4 );

Should a customer decide they need to upgrade the software, they need to do it soon after purchase, not as a way to obtain a deep discount. In the case of this EDD add-on snippet below, they need to upgrade within 3 months, otherwise prorating is not offered. If they try to upgrade an expired license, prorating is not offered.

I hope this helps you. If it does please reach out to say hi!

Send Easy Digital Downloads Transactions to Wave Apps without Zapier

I wrote a little WordPress plugin that connects Easy Digital Downloads (EDD) to Wave Apps bookkeeping using the Wave GraphQL API. If you are accepting Stripe payments using EDD’s Stripe extension or PayPal Payments using EDD’s PayPal Commerce Pro, this EDD extension will automatically move bookkeeping data into your Wave Apps transactions using Wave’s GraphQL API. It saves a ton of data-entry, and picks up where a free Zapier account leaves off.

In the plugin settings panel, you start by entering your Wave “full access token.” Get a token by creating an application in the Wave Apps developer portal. Once saved, this token allows the plugin to query its associated businesses. Choose your business (most cases there will only be one), and click save again. Now you can map your payment accounts and your download products to those matching in your Wave Chart of Accounts. Discounts and fees currently only go a Wave account you’ve set up to handle discounts or fees. If you’d like to see taxes, discounts, and fees developed out further to be more discriminating, get in touch or make a pull request.

I know what you’re thinking… You’re smart! Yes, it might be difficult to trust a third-party plugin with financial data. Sagehen Studio does not receive or transmit any (literally zero) of your data, sensitive or not. We don’t even know you’re using it unless you tell us (so drop a line)! Anyway, this plugin is very small, so it wouldn’t take very long to sift through the code and realize it’s only doing what it says it does: send transaction numbers via HTTP through a secure connection with Wave. And it does it for free.

Enjoy!

An Easy Digital Download Software Licensing Server Attack

If you’ve purchased the Easy Digital Downloads Software Licensing add-on (min price $99/yr) and you have your eyes on API server traffic, you might have noticed that if your site runs CRON jobs as frequently as every minute, you are potentially getting hit by thousands of HTTP requests every hour (depending of course on how many license keys you’ve issued to customers). This can lead to DB errors and worse. That’s just not cool for the price.

EDD provides some “free” example integration code to get users started. Great, but we really shouldn’t use any volunteered code without an audit. Here’s the code they’ve provided:

/**
 * Initialize the updater. Hooked into `init` to work with the
 * wp_version_check cron job, which allows auto-updates.
 */
function edd_sl_sample_plugin_updater() {

	// To support auto-updates, this needs to run during the wp_version_check cron job for privileged users.
	$doing_cron = defined( 'DOING_CRON' ) && DOING_CRON;
	if ( ! current_user_can( 'manage_options' ) && ! $doing_cron ) {
		return;
	}

	// retrieve our license key from the DB
	$license_key = trim( get_option( 'edd_sample_license_key' ) );

	// setup the updater
	$edd_updater = new EDD_SL_Plugin_Updater(
		EDD_SAMPLE_STORE_URL,
		__FILE__,
		array(
			'version' => '1.0',                    // current version number
			'license' => $license_key,             // license key (used get_option above to retrieve from DB)
			'item_id' => EDD_SAMPLE_ITEM_ID,       // ID of the product
			'author'  => 'Easy Digital Downloads', // author of this plugin
			'beta'    => false,
		)
	);

}
add_action( 'init', 'edd_sl_sample_plugin_updater' );

Lines 7-11 of this PHP code indicate that as long as a site administrator is logged into the site and loads any page, or every time a CRON job runs, fire the EDD_SL_Plugin_Updater() class, which runs calls to the server running the API. Some CRON jobs run every minute, plus there are all the other cron jobs. Some admin users can load A LOT OF PAGES every minute, too.

YOIKS!

I added some checks to their suggested code so that even though the function runs with every page load (on the ‘init’ hook), it only ends up running if:

  • CRON is not running yet an administrator is viewing a plugin update screen
  • CRON is running and WP auto updates are turned on for the plugin

If auto updates are turned on this could still mean a lot of requests, but this certainly cuts way down on pointless API calls. Transients could also certainly be set; some plugins rate limit that way.

Slightly Optimized Version:

/**
 * Initialize the updater. Hooked into `init` to work with the
 * wp_version_check cron job, which allows auto-updates.
 */
function edd_sl_sample_plugin_updater() {

    if ( wp_installing() ) {
        return;
    }

    $doing_cron = defined( 'DOING_CRON' ) && DOING_CRON;

    if ( $doing_cron ) {
        /** 
         * If running cron, only continue if plugin auto updates are on
         * (The 'wp_version_check' CRON job supports auto-updates)
         */ 
        if ( 'yes' !== get_option( 'my_plugin_auto_updates', 'yes' ) ) { // if your plugin doesn't have a setting for this you can probably fetch the setting using hooks
            return;
        } else {
            // Probably wise to get a transient right here to slow this down...
        }
    } else {
        // If not doing cron, only check for updates for admin users
        if ( ! current_user_can( 'manage_options' ) ) {
            return;
        }
        global $pagenow;
        // Cut back on requests by only continuing if on plugin updates page
        if ( isset( $pagenow ) && 'plugins.php' !== $pagenow && 'update-core.php' !== $pagenow ) {
            return;
        }
    }
    // retrieve our license key from the DB
    $license_key = trim( get_option( 'edd_sample_license_key' ) );

    // setup the updater
    $edd_updater = new EDD_SL_Plugin_Updater(
        EDD_SAMPLE_STORE_URL,
        __FILE__,
        array(
            'version' => '1.0',                    // current version number
            'license' => $license_key,             // license key (used get_option above to retrieve from DB)
            'item_id' => EDD_SAMPLE_ITEM_ID,       // ID of the product
            'author'  => 'Easy Digital Downloads', // author of this plugin
            'beta'    => false,
        )
    );

}
add_action( 'init', 'edd_sl_sample_plugin_updater' );

Get in touch if you need help with this or you have better ideas!