Easy Digital Downloads 3.0-beta2 now available

Since releasing 3.0-beta1 back in February, we’ve been hard at work updating our extensions for 3.0 compatibility and fixing any issues that popped up in core. We have since closed over 100 issues!

The beta2 release is now available for testing, and in this blog post we’ll go over three of the major changes.

  1. The new refunds API has been finalized
  2. Templates have been reworked
  3. The migration now has step recovery

Important Note: This beta release is NOT production ready. It should only be tested on development sites. Do not install it on your live site.

The new refunds API has been finalized

When we released 3.0-beta1, we made it clear that refunds were still a work in progress, but they have now been completed!

Modal for submitting a refund, with a checkbox to "Refund transaction in PayPal".

Here are a few notable changes:

  • Each order item and fee will have a listing in the modal, and you can select which items to refund. It’s possible to refund a fee without refunding the order items, and vice versa.
  • For each item, you can also specify a quantity (if applicable), and how much of that item you want to refund. As an example, for that $10 item I could choose to refund only $5 of it.
  • We have added a hook to the modal, which you can use to output custom fields below the table. You can see how we’ve added a checkbox that gives you the option to “Refund transaction in PayPal.”

Let’s talk about that last item for a moment. If you’ve built a custom gateway, you probably want to know how to add your own checkbox and execute custom code when the refund runs. Here’s our implementation for PayPal:

Adding the checkbox to the UI

Here’s what the code looks like to add a checkbox to the UI:

 * Shows a checkbox to automatically refund payments in PayPal.
 * @param \EDD\Orders\Order $order
 * @since 3.0
 * @return void
function edd_paypal_refund_checkbox( \EDD\Orders\Order $order ) {
	if ( 'paypal' !== $order->gateway ) {

	// If our credentials are not set, return early.
	$key       = $order->mode;
	$username  = edd_get_option( 'paypal_' . $key . '_api_username' );
	$password  = edd_get_option( 'paypal_' . $key . '_api_password' );
	$signature = edd_get_option( 'paypal_' . $key . '_api_signature' );

	if ( empty( $username ) || empty( $password ) || empty( $signature ) ) {
	<div class="edd-form-group edd-paypal-refund-transaction">
		<div class="edd-form-group__control">
			<input type="checkbox" id="edd-paypal-refund" name="edd-paypal-refund" class="edd-form-group__input" value="1">
			<label for="edd-paypal-refund" class="edd-form-group__label">
				<?php esc_html_e( 'Refund transaction in PayPal', 'easy-digital-downloads' ); ?>
add_action( 'edd_after_submit_refund_table', 'edd_paypal_refund_checkbox' );
  • Hook into edd_after_submit_refund_table. This hook provides one parameter: the \EDD\Orders\Order object.
  • Be sure to check that the order is using your gateway.
  • We only show the option if the API keys have been configured.
  • Then output markup to show the custom field.

The next step is to actually process the refund. Here’s the code to make that happen:

 * If selected, refunds a transaction in PayPal when creating a new refund record.
 * @param int $order_id ID of the order we're processing a refund for.
 * @param int $refund_id ID of the newly created refund record.
 * @param bool $all_refunded Whether or not this was a full refund.
 * @since 3.0
function edd_paypal_maybe_refund_transaction( $order_id, $refund_id, $all_refunded ) {
	if ( ! current_user_can( 'edit_shop_payments', $order_id ) ) {

	if ( empty( $_POST['data'] ) ) {

	$order = edd_get_order( $order_id );
	if ( empty( $order->gateway ) || 'paypal' !== $order->gateway ) {

	// Get our data out of the serialized string.
	parse_str( $_POST['data'], $form_data );

	if ( empty( $form_data['edd-paypal-refund'] ) ) {
		// Checkbox was not checked.

	$refund = edd_get_order( $refund_id );
	if ( empty( $refund->total ) ) {

	edd_refund_paypal_purchase( $order, $refund );
add_action( 'edd_refund_order', 'edd_paypal_maybe_refund_transaction', 10, 3 );
  • Hook into edd_refund_order. This hook supplies three parameters: the ID of the order being refunded, the ID of the newly created refund object, and a boolean indicating if the entire order was refunded or not.
  • Be sure to include a permission check.
  • Be sure to check the relevant gateway.
  • To get the data from the form you’ll want to run: parse_str( $_POST['data'], $form_data ); Then the posted data will be available in $form_data.
  • As a sanity check, we make sure the newly created refund total isn’t empty (0.00).
  • Then, finally, you can execute your code to actually process the refund at the gateway. (We call edd_refund_paypal_purchase()).

You can see all our code for this in context in the PayPal Standard gateway file.

Templates have been reworked (but are still backwards compatible!)

We’ve updated the history-downloads.php and history-purchases.php templates to use all new 3.0 functions.

The main motivation behind this change was speed. Working with the legacy EDD_Payment object (and associated edd_get_payment() and edd_get_users_purchases() functions) is a performance hit. We have to navigate through multiple backwards compatibility layers to build up that object.

Prior to making the changes, the purchase history template was running approximately 300-400 queries — sometimes more. That definitely slowed the page down and resulted in a pretty negative experience. After reworking it, we’ve gotten it down to approximately 35 queries. That’s quite an improvement!

So what do these changes actually mean for you?

Every hook executed within those templates that passed through an EDD_Payment object (or array of objects) has been deprecated, and new replacements have been created. For example:

Here’s an example hook in the previous template version:

do_action( 'edd_before_purchase_history', $payments );

This hook has been deprecated because it was built to receive an array of EDD_Payment objects. It has been replaced with this new hook, which instead takes an array of \EDD\Orders\Order objects:

do_action( 'edd_before_order_history', $orders );

That being said, the old hook will still work. We hook into the new action to trigger the old:

add_action( 'edd_before_order_history', function( $orders ) {
	if ( ! has_action( 'edd_before_purchase_history' ) ) {

	$payments = array();

	if ( ! empty( $orders ) ) {
		$order_ids = wp_list_pluck( $orders, 'id' );
		$payments  = edd_get_payments(
				'id__in'  => $order_ids,
				'orderby' => 'date',

	do_action( 'edd_before_purchase_history', $payments );
} );

The only thing this means for you is that if you’re hooking into the old action you will see a performance hit, because we will need to load in the EDD_Payment objects for your hook.

Right now the old hooks will not trigger any deprecation notices, as it is still easiest to use them if you need to support both EDD 2.x and 3.0 at the same time. We are planning to add deprecation notices in EDD 3.1. We have no plans to stop the hooks from working entirely.

For a full list of all deprecated hooks, check out our new deprecated-hooks.php file.

The migration now has step recovery

The 3.0 migration has 8 different steps — or 9 if you include the removal of legacy data. In beta1, if the migration process was interrupted 50% through step 4 and you reloaded the page to start again, you would start over at 0% of step 4. So although your progress through the 8 (or 9) main steps could be recovered, the steps within each step could not. This has changed with beta2.

Now, if the migration is interrupted 50% through step 4 and you reload the page to initiate it again, you will resume at 50% through step 4.

Installing and testing 3.0-beta2

The 3.0-beta2 release can be downloaded from GitHub and installed as normal. If you encounter any bugs, please search through our existing issues. If there’s not already an open issue, you may create a new one (after reading our Contributor Guidelines).

What’s next for 3.0?

Our next major goal is to upgrade to 3.0 on one of our own Sandhills product sites. We’ve been hard at work updating extensions — first prioritizing the ones we use internally — so we can upgrade to 3.0. We will be doing this prior to the official 3.0 release.

We don’t have an exact date for this yet, and there are still a few extensions we need to update first, but I suspect you’ll see us with another blog post when it happens so we can tell you all about how it went!

2 responses... add one

Keep up the great work, team. You’re getting close!

I love how you upgrade one of your own sites before releasing to the public. Especially with such a big update like 3.0, this really instills confidence in the rest of us. Thank you for doing that 🙏.

We appreciate the work you guys are doing! Looking forward to all the changes coming in 3.0.

Somehow Dave always beats me here. 😂

Leave a Reply

Your email address will not be published. Required fields are marked *