Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions adminpages/emailsettings.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@

pmpro_setOption("email_member_notification");

pmpro_setOption("churned_email_days", null, 'intval');

//assume success
$msg = true;
$msgt = "Your email settings have been updated.";
Expand All @@ -49,6 +51,8 @@

$email_member_notification = get_option( "pmpro_email_member_notification");

$churned_email_days = get_option("pmpro_churned_email_days", 30);

if(empty($from_email))
{
$parsed = parse_url(home_url());
Expand Down Expand Up @@ -186,6 +190,18 @@
<p class="description"><?php esc_html_e( 'Recommended: Leave unchecked. Members will still get an email confirmation from PMPro after checkout.', 'paid-memberships-pro' ); ?></p>
</td>
</tr>
<tr>
<th scope="row" valign="top">
<label for="churned_email_days"><?php esc_html_e("Churned Member Email Delay", "paid-memberships-pro" );?>:</label>
</th>
<td>
<input type="number" id="churned_email_days" name="churned_email_days" value="<?php echo esc_attr($churned_email_days); ?>" min="1" max="365" class="small-text" /> <?php esc_html_e("days", "paid-memberships-pro" );?>
<p class="description">
<?php esc_html_e( "Number of days after a membership expires before sending a win-back email to churned members.", "paid-memberships-pro" ); ?>
<?php printf( esc_html__( 'You can %scustomize or disable this email%s from the Email Templates page.', 'paid-memberships-pro' ), '<a href="' . esc_url( admin_url( 'admin.php?page=pmpro-emailtemplates' ) ) . '">', '</a>' ); ?>
</p>
</td>
</tr>
</tbody>
</table>
<p class="submit">
Expand Down
21 changes: 21 additions & 0 deletions classes/class.pmproemail.php
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,27 @@ function sendMembershipExpiredEmail( $user = NULL, $membership_id = NULL ) {
$email = new PMPro_Email_Template_Membership_Expired( $user, $membership_id );
return $email->send();
}

/**
* Send the member a churned email 30 days after their membership has expired.
*
* @param object $user The WordPress user object.
* @param int $membership_id The member's membership level ID.
* @return bool Whether the email was sent successfully.
*/
function sendMembershipChurnedEmail( $user = NULL, $membership_id = NULL ) {
global $current_user;
if( !$user ) {
$user = $current_user;
}
//Bail if still we don't have a user.
if( !$user ) {
return false;
}

$email = new PMPro_Email_Template_Membership_Churned( $user, $membership_id );
return $email->send();
}

/**
* Send the member an email when their membership has ended.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
<?php
class PMPro_Email_Template_Membership_Churned extends PMPro_Email_Template {

/**
* The user object of the user to send the email to.
*
* @var WP_User
*/
protected $user;

/**
* The membership level that expired.
*
* @var int
*/
protected $membership_level_id;

/**
* Constructor.
*
* @param WP_User $user The user object of the user to send the email to.
* @param int $membership_id The membership level id of the membership level that expired.
*/
public function __construct( WP_User $user, int $membership_level_id ) {
$this->user = $user;
$this->membership_level_id = $membership_level_id;
}

/**
* Get the email template slug.
*
* @return string The email template slug.
*/
public static function get_template_slug() {
return 'membership_churned';
}

/**
* Get the "nice name" of the email template.
*
* @return string The "nice name" of the email template.
*/
public static function get_template_name() {
return esc_html__( 'Membership Churned', 'paid-memberships-pro' );
}

/**
* Get "help text" to display to the admin when editing the email template.
*
* @return string The "help text" to display to the admin when editing the email template.
*/
public static function get_template_description() {
$days = intval(get_option("pmpro_churned_email_days", 30));
/* translators: %d: number of days after membership expiration */
return sprintf(esc_html__( 'This email is sent to former members %d days after their membership expires (churned members).', 'paid-memberships-pro' ), $days);
}

/**
* Get the default subject for the email.
*
* @return string The default subject for the email.
*/
public static function get_default_subject() {
return esc_html__( 'We miss you at !!sitename!! - Special offer inside', 'paid-memberships-pro' );
}

/**
* Get the default body content for the email.
*
* @return string The default body content for the email.
*/
public static function get_default_body() {
$days = intval(get_option("pmpro_churned_email_days", 30));

// Convert days to friendly text with flexible ranges
$time_period = '';
if ($days <= 7) {
$time_period = 'a week';
} elseif ($days <= 14) {
$time_period = 'two weeks';
} elseif ($days <= 31) {
$time_period = 'a month';
} elseif ($days <= 62) {
$time_period = 'two months';
} elseif ($days <= 93) {
$time_period = 'three months';
} elseif ($days <= 186) {
$time_period = 'six months';
} elseif ($days <= 366) {
$time_period = 'a year';
} else {
$time_period = 'some time';
}

return wp_kses_post( sprintf( __( '<p>Hi !!display_name!!,</p>

<p>We noticed it has been %s since your membership at !!sitename!! expired, and we would love to have you back.</p>

<p>We value your past membership and would like to offer you the opportunity to rejoin our community.</p>

<p>View our current membership offerings here: !!levels_url!!</p>

<p>Log in to manage your account here: !!login_url!!</p>

<p>Thank you for considering rejoining us!</p>', 'paid-memberships-pro' ), $time_period ) );
}

/**
* Get the email address to send the email to.
*
* @return string The email address to send the email to.
*/
public function get_recipient_email() {
return $this->user->user_email;
}

/**
* Get the name of the email recipient.
*
* @return string The name of the email recipient.
*/
public function get_recipient_name() {
return $this->user->display_name;
}


/**
* Get the email template variables for the email paired with a description of the variable.
*
* @return array The email template variables for the email (key => value pairs).
*/
public static function get_email_template_variables_with_description() {
return array(
'!!display_name!!' => esc_html__( 'The display name of the user.', 'paid-memberships-pro' ),
'!!user_login!!' => esc_html__( 'The username of the user.', 'paid-memberships-pro' ),
'!!user_email!!' => esc_html__( 'The email address of the user.', 'paid-memberships-pro' ),
'!!membership_id!!' => esc_html__( 'The ID of the membership level.', 'paid-memberships-pro' ),
'!!membership_level_name!!' => esc_html__( 'The name of the membership level.', 'paid-memberships-pro' ),
);
}

/**
* Get the email template variables for the email.
*
* @return array The email template variables for the email (key => value pairs).
*/
public function get_email_template_variables() {
global $wpdb;
// If we don't have a level ID, query the user's most recently expired level from the database.
if ( empty( $this->membership_id ) ) {
$membership_id = $wpdb->get_var(
$wpdb->prepare(
"SELECT membership_id FROM $wpdb->pmpro_memberships_users
WHERE user_id = %d
AND status = 'expired'
ORDER BY enddate DESC
LIMIT 1",
$this->user->ID
)
);

// If we still don't have a level ID, bail.
if ( empty( $membership_id ) ) {
$membership_id = 0;
}
}

// Get the membership level object.
$membership_level = pmpro_getLevel( $membership_id );

return array(
"subject" => $this->get_default_subject(),
"name" => $this->user->display_name,
"display_name" => $this->user->display_name,
"user_login" => $this->user->user_login,
"user_email" => $this->user->user_email,
"membership_id" => ( ! empty( $membership_level ) && ! empty( $membership_level->id ) ) ? $membership_level->id : 0,
"membership_level_name" => ( ! empty( $membership_level ) && ! empty( $membership_level->name ) ) ? $membership_level->name : '[' . esc_html( 'deleted', 'paid-memberships-pro' ) . ']',
);
}
}

/**
* Register the email template.
*
* @param array $email_templates The email templates (template slug => email template class name)
* @return array The modified email templates array.
*/
function pmpro_email_templates_membership_churned( $email_templates ) {
$email_templates['membership_churned'] = 'PMPro_Email_Template_Membership_Churned';
return $email_templates;
}
add_filter( 'pmpro_email_templates', 'pmpro_email_templates_membership_churned' );
4 changes: 4 additions & 0 deletions includes/crons.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ function pmpro_get_crons() {
'interval' => 'daily',
'timestamp' => strtotime( '10:30:00' ) - ( get_option( 'gmt_offset' ) * HOUR_IN_SECONDS ),
],
'pmpro_cron_churned_emails' => [
'interval' => 'daily',
'timestamp' => strtotime( '11:00:00' ) - ( get_option( 'gmt_offset' ) * HOUR_IN_SECONDS ),
],
'pmpro_license_check_key' => [
'interval' => 'monthly',
],
Expand Down
4 changes: 4 additions & 0 deletions includes/email.php
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,10 @@ function pmpro_email_templates_send_test() {
$send_email = 'sendInvoiceEmail';
$params = array($test_user, $test_order);
break;
case 'membership_churned';
$send_email = 'sendMembershipChurnedEmail';
$params = array($test_user, $test_order->membership_id );
break;
case 'membership_expired';
$send_email = 'sendMembershipExpiredEmail';
$params = array($test_user, $test_order->membership_id );
Expand Down
1 change: 1 addition & 0 deletions paid-memberships-pro.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
require_once( PMPRO_DIR . '/classes/email-templates/class-pmpro-email-template-billing-failure-admin.php' ); // billing failure email template
require_once( PMPRO_DIR . '/classes/email-templates/class-pmpro-email-template-cancel-on-next-payment-date.php' ); //cancel auto renewals email template
require_once( PMPRO_DIR . '/classes/email-templates/class-pmpro-email-template-cancel-on-next-payment-date-admin.php' ); //cancel auto renewals admin email template
require_once( PMPRO_DIR . '/classes/email-templates/class-pmpro-email-template-membership-churned.php' ); //churned member email template

require_once( PMPRO_DIR . '/includes/filters.php' ); // filters, hacks, etc, moved into the plugin
require_once( PMPRO_DIR . '/includes/reports.php' ); // load reports for admin (reports may also include tracking code, etc)
Expand Down
2 changes: 2 additions & 0 deletions scheduled/churnedemails.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<?php
die( 'You may no longer trigger this cron directly. Trigger the pmpro_cron_churned_emails hook via WP-Cron.' );
86 changes: 86 additions & 0 deletions scheduled/crons.php
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,92 @@ function pmpro_cron_recurring_payment_reminders() {
}
}

/**
* Churned Member Emails
* Sends win-back emails to members whose memberships expired a certain number of days ago.
*/
function pmpro_cron_churned_emails() {
global $wpdb;

// Don't let anything run if PMPro is paused
if ( pmpro_is_paused() ) {
return;
}

// Set default value to true (disabled) if the option doesn't exist yet
add_option( 'pmpro_email_membership_churned_disabled', true );

// Check if churned emails are disabled
if ( filter_var( get_option( 'pmpro_email_membership_churned_disabled' ), FILTER_VALIDATE_BOOLEAN ) ) {
// Feature is disabled, exit early
return;
}

// Clean up errors in the memberships_users table that could cause problems
pmpro_cleanup_memberships_users_table();

// The current date
$today = date( 'Y-m-d H:i:s', current_time( 'timestamp' ) );

// Get the configurable days setting (default 30)
$churned_days = intval(get_option("pmpro_churned_email_days", 30));

// The date N days ago based on the setting
$churned_date = date( 'Y-m-d H:i:s', strtotime( '-' . $churned_days . ' days', current_time( 'timestamp' ) ) );
$interval = date( 'Y-m-d 00:00:00', strtotime( $churned_date ) );

// Look for users whose membership expired N days ago and who haven't been sent a churned email yet
$sqlQuery = $wpdb->prepare(
"SELECT DISTINCT
mu.user_id,
mu.membership_id,
mu.startdate,
mu.enddate,
um.meta_value AS notice
FROM {$wpdb->pmpro_memberships_users} AS mu
LEFT JOIN {$wpdb->usermeta} AS um ON um.user_id = mu.user_id
AND um.meta_key = CONCAT( 'pmpro_churned_notice_', mu.membership_id )
WHERE ( um.meta_value IS NULL )
AND ( mu.status = 'expired' )
AND ( mu.enddate IS NOT NULL )
AND ( mu.enddate <> '0000-00-00 00:00:00' )
AND ( mu.enddate <= %s )
AND ( mu.membership_id <> 0 OR mu.membership_id <> NULL )
ORDER BY mu.enddate",
$interval,
);

if ( defined( 'PMPRO_CRON_LIMIT' ) ) {
$sqlQuery .= " LIMIT " . PMPRO_CRON_LIMIT;
}

$expired_users = $wpdb->get_results( $sqlQuery );

foreach( $expired_users as $user_data ) {
$send_email = apply_filters( 'pmpro_send_churned_email', true, $user_data->user_id );

if ( $send_email ) {
// Send the churned email
$pmproemail = new PMProEmail();
$user = get_userdata( $user_data->user_id );

if ( ! empty( $user ) ) {
$pmproemail->sendMembershipChurnedEmail( $user, $user_data->membership_id );

if ( WP_DEBUG ) {
error_log( sprintf( esc_html__( "Churned member email sent to %s.", 'paid-memberships-pro' ), $user->user_email ) );
}
}
}

// Delete any old user meta for this key to prevent duplicate user meta rows
delete_user_meta( $user_data->user_id, 'pmpro_churned_notice' );

// Update user meta so we don't email them again
update_user_meta( $user_data->user_id, 'pmpro_churned_notice_' . $user_data->membership_id, $today );
}
}

/**
* Delete old files in wp-content/uploads/pmpro-register-helper/tmp every day.
*/
Expand Down