NOTE: This issue was resolved in EDD’s Recurring Payments in version 2.10.2
released on March 4, 2021 🎉
(Honestly I have to apologize for the title of this post but after spending five minutes trying to do it well I just gave up.)
I’m a big fan of Easy Digital Downloads and I use it to facilitate license sales for SearchWP. I’m bought into the EDD ecosystem and rely primarily on Software Licensing and Recurring Payments to handle the bulk of the ecommerce layer. There are many other pieces but those Extensions are mission critical.
License and subscription management challenges
I do not envy the challenge that is building a shopping cart, a licensing system, subscriptions, and rolling that all together
When it came to SearchWP I’ve had a recurring issue:
- Customer purchases license which activates subscription
- Subscription renewal fails due to expired card
- Customer receives email about failure and logs into their account
- Customer proceeds to manually renew the license using updated card information
At first glance this all seems okay, but trouble enters in the form of a duplicate subscription being created because of the manual license renewal. This is further complicated because the updated card information to create said redundant subscription triggers a card information update on the failing subscription which proceeds to renew at the next attempt period.
The end result is two active subscriptions for the same license and an extra charge on the customer’s card.
To date I’ve made efforts to explain this in the ‘failed payment’ email that is sent to customers, but the problem persists. I can’t fault the customer though, they’re busy people and just want things to work, and in my opinion it’s totally logical to receive a failed payment notice and log in to resolve the issue by manually renewing the license.
There are also other options such as dunning emails that recognize a card has expired and send an email with the sole purpose of updating payment information prior to the renewal being charged. I think it’s a great system but have found that with annual renewals (as is the case with SearchWP) it’s almost more confusing for customers in a way. Hard to write out my reasoning behind it, maybe it’s just a gut feeling for me.
Solution: prevent manual renewals for failing subscriptions
At first I was only resolving this situation here and there, but as SearchWP has grown it’s turned into one of those situations I felt deserved the time it took to fix.
I decided that in my case it makes the most sense to hook into EDD each time a product is added to the cart and check to see if it’s a license renewal that’s associated with a failing subscription.
If that is the case, we’re going to redirect to a page I’ve created that has a form for customers to update their payment information. This page also has instructions explaining the situation and why the license cannot be renewed directly.
Here’s the hook:
<?php | |
// Prevent license renewal when associated subscription is failing | |
// (it will cause double charges and double subscriptions and waste a lot of your time) | |
add_action( 'edd_pre_add_to_cart', function( $download_id, $options ) { | |
// Only applicable if this is a renewal. | |
if ( empty( $options['is_renewal'] ) ) { | |
return; | |
} | |
// Handle action parameters. | |
$download_id = absint( $download_id ); | |
$license_id = absint( $options['license_id'] ); | |
$license_key = sanitize_text_field( $options['license_key'] ); | |
// We rely on a number of classes to accomplish this task. | |
if ( | |
! class_exists( 'EDD_Customer' ) | |
|| ! class_exists( 'EDD_Recurring_Subscriber' ) | |
|| ! function_exists( 'edd_software_licensing' ) | |
) { | |
return; | |
} | |
// Retrieve subscriptions for the subscriber. | |
$subscriber = new EDD_Recurring_Subscriber( get_current_user_id(), true ); | |
$subscriptions = $subscriber->get_subscriptions( $download_id ); | |
if ( empty( $subscriptions ) ) { | |
return; | |
} | |
$licence_being_renewed = edd_software_licensing()->get_license( $license_id ); | |
// Loop through the subscriptions to find the associated license keys. | |
foreach ( $subscriptions as $subscription ) { | |
// If this parent payment ID doesn't match the payment ID of the license being renewed, it's inapplicable. | |
if ( $subscription->parent_payment_id != $licence_being_renewed->payment_id ) { | |
continue; | |
} | |
// We have the subscription for the license being renewed. | |
// If the status is failing we need to prevent adding to cart. | |
if ( 'stripe' == $subscription->gateway && 'failing' == $subscription->status ) { | |
wp_safe_redirect( site_url( 'account/payment-information/' ) ); | |
die(); | |
} | |
} | |
}, 5, 2 ); |
The implementation is pretty straightforward. The hook is run before a product is added to the cart and in doing so we retrieve all of the subscriptions for the current Customer and if there is a failing Stripe subscription associated with the license being renewed, the customer is redirected away.
This works for me because I don’t have any manual renewal links emailed to customers so I’m able to (mostly) assume they’re logged in. Until that proves to be a problem I’m going to leave out the complexity of ensuring that the customer is logged in first.
I’m hoping this saves me a bit of time over the coming years, and you as well if the situation applies!