+
diff --git a/wordpress/wp-content/plugins/memberful-wp/views/options.php b/wordpress/wp-content/plugins/memberful-wp/views/options.php
index 25772c92..66716573 100755
--- a/wordpress/wp-content/plugins/memberful-wp/views/options.php
+++ b/wordpress/wp-content/plugins/memberful-wp/views/options.php
@@ -58,6 +58,18 @@
Show protected content in site search. ⚠️ Enabling this option will allow non-members to see protected content in WordPress search results.
+
+
+
+
+
+
+
From 4eb48a4f982d1103dd3f08207942d24db36e7b9d Mon Sep 17 00:00:00 2001
From: Zachary Hickson
Date: Thu, 19 Feb 2026 11:32:41 +1100
Subject: [PATCH 02/13] Minify the expiry banner
---
.../memberful-wp/views/expiry-banner.php | 27 ++-----------------
1 file changed, 2 insertions(+), 25 deletions(-)
diff --git a/wordpress/wp-content/plugins/memberful-wp/views/expiry-banner.php b/wordpress/wp-content/plugins/memberful-wp/views/expiry-banner.php
index 58d301a1..d5caf329 100644
--- a/wordpress/wp-content/plugins/memberful-wp/views/expiry-banner.php
+++ b/wordpress/wp-content/plugins/memberful-wp/views/expiry-banner.php
@@ -18,29 +18,6 @@
style="background:transparent;border:0;color:inherit;cursor:pointer;font:inherit;font-size:1rem;line-height:1;padding:0;"
>x
-
From 713d541177f424f37153f32d786864e2debb18b5 Mon Sep 17 00:00:00 2001
From: Zachary Hickson
Date: Thu, 19 Feb 2026 12:21:20 +1100
Subject: [PATCH 03/13] Sure about styling and accessibility
---
.../memberful-wp/src/expiry_banner.php | 28 ++++++++++++++++++-
.../memberful-wp/views/expiry-banner.php | 22 +++++++--------
2 files changed, 37 insertions(+), 13 deletions(-)
diff --git a/wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php b/wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php
index fe028dc2..e89e2b8a 100644
--- a/wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php
+++ b/wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php
@@ -50,11 +50,37 @@ function memberful_wp_render_expiry_banner() {
*/
$message = apply_filters( 'memberful_expiry_banner_message', $message, $expiry_data );
+ $is_expired = ! empty( $expiry_data['is_expired'] );
+ $aria_role = $is_expired ? 'alert' : 'status';
+ $aria_live = $is_expired ? 'assertive' : 'polite';
+
+ /**
+ * Filters the ARIA role used for the expiry banner live region.
+ *
+ * @param string $aria_role The computed ARIA role.
+ * @param array $expiry_data The computed expiry data for the current user.
+ *
+ * @return string The ARIA role for the banner.
+ */
+ $aria_role = (string) apply_filters( 'memberful_expiry_banner_aria_role', $aria_role, $expiry_data );
+
+ /**
+ * Filters the ARIA live mode used for the expiry banner.
+ *
+ * @param string $aria_live The computed ARIA live value.
+ * @param array $expiry_data The computed expiry data for the current user.
+ *
+ * @return string The ARIA live value for the banner.
+ */
+ $aria_live = (string) apply_filters( 'memberful_expiry_banner_aria_live', $aria_live, $expiry_data );
+
ob_start();
memberful_wp_render(
'expiry-banner',
array(
- 'message' => $message
+ 'message' => $message,
+ 'aria_role' => $aria_role,
+ 'aria_live' => $aria_live,
)
);
$html = ob_get_clean();
diff --git a/wordpress/wp-content/plugins/memberful-wp/views/expiry-banner.php b/wordpress/wp-content/plugins/memberful-wp/views/expiry-banner.php
index d5caf329..30621659 100644
--- a/wordpress/wp-content/plugins/memberful-wp/views/expiry-banner.php
+++ b/wordpress/wp-content/plugins/memberful-wp/views/expiry-banner.php
@@ -6,18 +6,16 @@
*/
?>
-
-
-
+
+
+
+
From df08a9d0e86112c2e536a2671d3d9467fc1da981 Mon Sep 17 00:00:00 2001
From: Zachary Hickson
Date: Wed, 25 Feb 2026 15:09:55 +1100
Subject: [PATCH 04/13] Adjust positioning of the expiry banner to push content
down under admin bar (if present)
---
.../memberful-wp/src/expiry_banner.php | 24 ++++++++++++++++++-
.../memberful-wp/views/expiry-banner.php | 2 +-
2 files changed, 24 insertions(+), 2 deletions(-)
diff --git a/wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php b/wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php
index e89e2b8a..cc7e8720 100644
--- a/wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php
+++ b/wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php
@@ -1,6 +1,20 @@
From b37277325503d440df8e7fd0128454c4bef7476c Mon Sep 17 00:00:00 2001
From: Zachary Hickson
Date: Wed, 25 Feb 2026 15:10:52 +1100
Subject: [PATCH 05/13] Allow expiry banner to show for admins
---
.../wp-content/plugins/memberful-wp/src/expiry_banner.php | 4 ----
1 file changed, 4 deletions(-)
diff --git a/wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php b/wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php
index cc7e8720..d64c320c 100644
--- a/wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php
+++ b/wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php
@@ -32,10 +32,6 @@ function memberful_wp_render_expiry_banner() {
return;
}
- if ( current_user_can( 'manage_options' ) ) {
- return;
- }
-
$enabled = (bool) get_option( 'memberful_expiry_banner_enabled', false );
/**
From 940d1f11cc12047729f66e48b78bcd7cfb6f06f0 Mon Sep 17 00:00:00 2001
From: Zachary Hickson
Date: Wed, 4 Mar 2026 12:53:28 +1100
Subject: [PATCH 06/13] Add mixed messaging for expiration of users with
multiple subscriptions
---
.../memberful-wp/src/expiry_banner.php | 78 +++++++++++++++++++
1 file changed, 78 insertions(+)
diff --git a/wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php b/wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php
index d64c320c..23a4b819 100644
--- a/wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php
+++ b/wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php
@@ -143,22 +143,29 @@ function memberful_wp_get_soonest_expiring_subscription( $user_id ) {
$now = time();
$threshold_timestamp = $now + ( $days_threshold * DAY_IN_SECONDS );
$soonest = null;
+ $expiring_subscriptions_count = 0;
+ $active_subscriptions_count = 0;
foreach ( $subscriptions as $subscription ) {
if ( empty( $subscription['expires_at'] ) ) {
+ ++$active_subscriptions_count;
continue;
}
$expires_at = memberful_wp_parse_expiry_timestamp( $subscription['expires_at'] );
if ( empty( $expires_at ) ) {
+ ++$active_subscriptions_count;
continue;
}
if ( $expires_at > $threshold_timestamp ) {
+ ++$active_subscriptions_count;
continue;
}
+ ++$expiring_subscriptions_count;
+
if ( null === $soonest || $expires_at < $soonest['expires_at'] ) {
$seconds_remaining = $expires_at - $now;
$is_expired = $seconds_remaining < 0;
@@ -172,6 +179,13 @@ function memberful_wp_get_soonest_expiring_subscription( $user_id ) {
}
}
+ if ( null === $soonest ) {
+ return null;
+ }
+
+ $soonest['expiring_subscriptions_count'] = $expiring_subscriptions_count;
+ $soonest['active_subscriptions_count'] = $active_subscriptions_count;
+
return $soonest;
}
@@ -189,8 +203,28 @@ function memberful_wp_expiry_banner_message( array $expiry_data, $account_url )
esc_url( $account_url ),
esc_html__( 'Update your membership', 'memberful' )
);
+ $expiring_subscriptions_count = max( 1, (int) ( $expiry_data['expiring_subscriptions_count'] ?? 1 ) );
+ $active_subscriptions_count = max( 0, (int) ( $expiry_data['active_subscriptions_count'] ?? 0 ) );
+ $is_mixed_subscriptions = $active_subscriptions_count > 0;
+ $has_multiple_expiring_subscriptions = $expiring_subscriptions_count > 1;
if ( ! empty( $expiry_data['is_expired'] ) ) {
+ if ( $is_mixed_subscriptions && $has_multiple_expiring_subscriptions ) {
+ return wp_sprintf(
+ /* translators: %s is the update membership link. */
+ __( 'You have multiple subscriptions that have expired. %s.', 'memberful' ),
+ $link
+ );
+ }
+
+ if ( $is_mixed_subscriptions ) {
+ return wp_sprintf(
+ /* translators: %s is the update membership link. */
+ __( 'You have a subscription that has expired. %s.', 'memberful' ),
+ $link
+ );
+ }
+
return wp_sprintf(
/* translators: %s is the update membership link. */
__( 'Your membership has expired. %s.', 'memberful' ),
@@ -199,6 +233,22 @@ function memberful_wp_expiry_banner_message( array $expiry_data, $account_url )
}
if ( (int) $expiry_data['days_remaining'] <= 0 ) {
+ if ( $is_mixed_subscriptions && $has_multiple_expiring_subscriptions ) {
+ return wp_sprintf(
+ /* translators: %s is the update membership link. */
+ __( 'You have multiple subscriptions expiring today. %s.', 'memberful' ),
+ $link
+ );
+ }
+
+ if ( $is_mixed_subscriptions ) {
+ return wp_sprintf(
+ /* translators: %s is the update membership link. */
+ __( 'You have a subscription expiring today. %s.', 'memberful' ),
+ $link
+ );
+ }
+
return wp_sprintf(
/* translators: %s is the update membership link. */
__( 'Your membership expires today. %s.', 'memberful' ),
@@ -206,6 +256,34 @@ function memberful_wp_expiry_banner_message( array $expiry_data, $account_url )
);
}
+ if ( $is_mixed_subscriptions && $has_multiple_expiring_subscriptions ) {
+ return wp_sprintf(
+ /* translators: 1: Number of days remaining. 2: Update membership link. */
+ _n(
+ 'You have multiple subscriptions expiring in %1$d day. %2$s.',
+ 'You have multiple subscriptions expiring in %1$d days. %2$s.',
+ (int) $expiry_data['days_remaining'],
+ 'memberful'
+ ),
+ (int) $expiry_data['days_remaining'],
+ $link
+ );
+ }
+
+ if ( $is_mixed_subscriptions ) {
+ return wp_sprintf(
+ /* translators: 1: Number of days remaining. 2: Update membership link. */
+ _n(
+ 'You have a subscription expiring in %1$d day. %2$s.',
+ 'You have a subscription expiring in %1$d days. %2$s.',
+ (int) $expiry_data['days_remaining'],
+ 'memberful'
+ ),
+ (int) $expiry_data['days_remaining'],
+ $link
+ );
+ }
+
return wp_sprintf(
/* translators: 1: Number of days remaining. 2: Update membership link. */
_n(
From cb1e9273db5aa32ad30ede762411f73a03d170a2 Mon Sep 17 00:00:00 2001
From: Zachary Hickson
Date: Wed, 18 Mar 2026 13:05:35 +1100
Subject: [PATCH 07/13] Wording changes making text more concise
---
.../memberful-wp/src/expiry_banner.php | 44 +++++++++----------
1 file changed, 22 insertions(+), 22 deletions(-)
diff --git a/wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php b/wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php
index 23a4b819..404b971b 100644
--- a/wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php
+++ b/wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php
@@ -201,7 +201,7 @@ function memberful_wp_expiry_banner_message( array $expiry_data, $account_url )
$link = wp_sprintf(
'%s',
esc_url( $account_url ),
- esc_html__( 'Update your membership', 'memberful' )
+ esc_html__( 'Renew now', 'memberful' )
);
$expiring_subscriptions_count = max( 1, (int) ( $expiry_data['expiring_subscriptions_count'] ?? 1 ) );
$active_subscriptions_count = max( 0, (int) ( $expiry_data['active_subscriptions_count'] ?? 0 ) );
@@ -211,23 +211,23 @@ function memberful_wp_expiry_banner_message( array $expiry_data, $account_url )
if ( ! empty( $expiry_data['is_expired'] ) ) {
if ( $is_mixed_subscriptions && $has_multiple_expiring_subscriptions ) {
return wp_sprintf(
- /* translators: %s is the update membership link. */
- __( 'You have multiple subscriptions that have expired. %s.', 'memberful' ),
+ /* translators: %s is the renewal link. */
+ __( 'Some of your subscriptions have expired. %s.', 'memberful' ),
$link
);
}
if ( $is_mixed_subscriptions ) {
return wp_sprintf(
- /* translators: %s is the update membership link. */
- __( 'You have a subscription that has expired. %s.', 'memberful' ),
+ /* translators: %s is the renewal link. */
+ __( 'One of your subscriptions has expired. %s.', 'memberful' ),
$link
);
}
return wp_sprintf(
- /* translators: %s is the update membership link. */
- __( 'Your membership has expired. %s.', 'memberful' ),
+ /* translators: %s is the renewal link. */
+ __( 'Your subscription has expired. %s.', 'memberful' ),
$link
);
}
@@ -235,33 +235,33 @@ function memberful_wp_expiry_banner_message( array $expiry_data, $account_url )
if ( (int) $expiry_data['days_remaining'] <= 0 ) {
if ( $is_mixed_subscriptions && $has_multiple_expiring_subscriptions ) {
return wp_sprintf(
- /* translators: %s is the update membership link. */
- __( 'You have multiple subscriptions expiring today. %s.', 'memberful' ),
+ /* translators: %s is the renewal link. */
+ __( 'Some of your subscriptions expire today. %s.', 'memberful' ),
$link
);
}
if ( $is_mixed_subscriptions ) {
return wp_sprintf(
- /* translators: %s is the update membership link. */
- __( 'You have a subscription expiring today. %s.', 'memberful' ),
+ /* translators: %s is the renewal link. */
+ __( 'One of your subscriptions expires today. %s.', 'memberful' ),
$link
);
}
return wp_sprintf(
- /* translators: %s is the update membership link. */
- __( 'Your membership expires today. %s.', 'memberful' ),
+ /* translators: %s is the renewal link. */
+ __( 'Your subscription expires today. %s.', 'memberful' ),
$link
);
}
if ( $is_mixed_subscriptions && $has_multiple_expiring_subscriptions ) {
return wp_sprintf(
- /* translators: 1: Number of days remaining. 2: Update membership link. */
+ /* translators: 1: Number of days remaining. 2: Renewal link. */
_n(
- 'You have multiple subscriptions expiring in %1$d day. %2$s.',
- 'You have multiple subscriptions expiring in %1$d days. %2$s.',
+ 'Some of your subscriptions expire in %1$d day. %2$s.',
+ 'Some of your subscriptions expire in %1$d days. %2$s.',
(int) $expiry_data['days_remaining'],
'memberful'
),
@@ -272,10 +272,10 @@ function memberful_wp_expiry_banner_message( array $expiry_data, $account_url )
if ( $is_mixed_subscriptions ) {
return wp_sprintf(
- /* translators: 1: Number of days remaining. 2: Update membership link. */
+ /* translators: 1: Number of days remaining. 2: Renewal link. */
_n(
- 'You have a subscription expiring in %1$d day. %2$s.',
- 'You have a subscription expiring in %1$d days. %2$s.',
+ 'One of your subscriptions expires in %1$d day. %2$s.',
+ 'One of your subscriptions expires in %1$d days. %2$s.',
(int) $expiry_data['days_remaining'],
'memberful'
),
@@ -285,10 +285,10 @@ function memberful_wp_expiry_banner_message( array $expiry_data, $account_url )
}
return wp_sprintf(
- /* translators: 1: Number of days remaining. 2: Update membership link. */
+ /* translators: 1: Number of days remaining. 2: Renewal link. */
_n(
- 'Your membership expires in %1$d day. %2$s.',
- 'Your membership expires in %1$d days. %2$s.',
+ 'Your subscription expires in %1$d day. %2$s.',
+ 'Your subscription expires in %1$d days. %2$s.',
(int) $expiry_data['days_remaining'],
'memberful'
),
From 0f456a875c8d8778e75f8f7108848daac6318242 Mon Sep 17 00:00:00 2001
From: Zachary Hickson
Date: Wed, 15 Apr 2026 15:38:24 +1000
Subject: [PATCH 08/13] Disable expiry banner when autorenew is on
---
.../memberful-wp/src/expiry_banner.php | 25 +++++++++++++++++--
1 file changed, 23 insertions(+), 2 deletions(-)
diff --git a/wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php b/wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php
index 404b971b..b38077ef 100644
--- a/wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php
+++ b/wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php
@@ -164,11 +164,17 @@ function memberful_wp_get_soonest_expiring_subscription( $user_id ) {
continue;
}
+ $seconds_remaining = $expires_at - $now;
+ $is_expired = $seconds_remaining < 0;
+
+ if ( ! $is_expired && memberful_wp_subscription_has_autorenew_enabled( $subscription ) ) {
+ ++$active_subscriptions_count;
+ continue;
+ }
+
++$expiring_subscriptions_count;
if ( null === $soonest || $expires_at < $soonest['expires_at'] ) {
- $seconds_remaining = $expires_at - $now;
- $is_expired = $seconds_remaining < 0;
$days_remaining = $is_expired ? 0 : (int) ceil( $seconds_remaining / DAY_IN_SECONDS );
$soonest = array(
@@ -317,3 +323,18 @@ function memberful_wp_parse_expiry_timestamp( $expires_at ) {
return (int) $parsed_time;
}
+
+/**
+ * Checks whether subscription auto-renew is enabled.
+ *
+ * @param array $subscription Subscription data from user meta.
+ *
+ * @return bool
+ */
+function memberful_wp_subscription_has_autorenew_enabled( array $subscription ) {
+ if ( ! array_key_exists( 'autorenew', $subscription ) ) {
+ return false;
+ }
+
+ return wp_validate_boolean( $subscription['autorenew'] );
+}
From 949fa6206eb5d74f9498a5491633b22410a3f0d2 Mon Sep 17 00:00:00 2001
From: Zachary Hickson
Date: Thu, 16 Apr 2026 16:48:52 +1000
Subject: [PATCH 09/13] Fix edge cases with banner messaging logic
---
.../memberful-wp/src/expiry_banner.php | 60 ++++++++++++++++---
.../plugins/memberful-wp/src/options.php | 2 +-
.../memberful-wp/views/expiry-banner.php | 2 +-
3 files changed, 54 insertions(+), 10 deletions(-)
diff --git a/wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php b/wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php
index b38077ef..7fa71f86 100644
--- a/wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php
+++ b/wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php
@@ -174,7 +174,21 @@ function memberful_wp_get_soonest_expiring_subscription( $user_id ) {
++$expiring_subscriptions_count;
- if ( null === $soonest || $expires_at < $soonest['expires_at'] ) {
+ $should_replace_soonest = false;
+
+ if ( null === $soonest ) {
+ $should_replace_soonest = true;
+ } else {
+ $soonest_is_expired = ! empty( $soonest['is_expired'] );
+
+ if ( $soonest_is_expired && ! $is_expired ) {
+ $should_replace_soonest = true;
+ } elseif ( $soonest_is_expired === $is_expired && $expires_at < $soonest['expires_at'] ) {
+ $should_replace_soonest = true;
+ }
+ }
+
+ if ( $should_replace_soonest ) {
$days_remaining = $is_expired ? 0 : (int) ceil( $seconds_remaining / DAY_IN_SECONDS );
$soonest = array(
@@ -215,10 +229,18 @@ function memberful_wp_expiry_banner_message( array $expiry_data, $account_url )
$has_multiple_expiring_subscriptions = $expiring_subscriptions_count > 1;
if ( ! empty( $expiry_data['is_expired'] ) ) {
- if ( $is_mixed_subscriptions && $has_multiple_expiring_subscriptions ) {
+ if ( $has_multiple_expiring_subscriptions ) {
+ if ( $is_mixed_subscriptions ) {
+ return wp_sprintf(
+ /* translators: %s is the renewal link. */
+ __( 'Some of your subscriptions have expired. %s.', 'memberful' ),
+ $link
+ );
+ }
+
return wp_sprintf(
/* translators: %s is the renewal link. */
- __( 'Some of your subscriptions have expired. %s.', 'memberful' ),
+ __( 'Your subscriptions have expired. %s.', 'memberful' ),
$link
);
}
@@ -239,10 +261,18 @@ function memberful_wp_expiry_banner_message( array $expiry_data, $account_url )
}
if ( (int) $expiry_data['days_remaining'] <= 0 ) {
- if ( $is_mixed_subscriptions && $has_multiple_expiring_subscriptions ) {
+ if ( $has_multiple_expiring_subscriptions ) {
+ if ( $is_mixed_subscriptions ) {
+ return wp_sprintf(
+ /* translators: %s is the renewal link. */
+ __( 'Some of your subscriptions expire today. %s.', 'memberful' ),
+ $link
+ );
+ }
+
return wp_sprintf(
/* translators: %s is the renewal link. */
- __( 'Some of your subscriptions expire today. %s.', 'memberful' ),
+ __( 'Your subscriptions expire today. %s.', 'memberful' ),
$link
);
}
@@ -262,12 +292,26 @@ function memberful_wp_expiry_banner_message( array $expiry_data, $account_url )
);
}
- if ( $is_mixed_subscriptions && $has_multiple_expiring_subscriptions ) {
+ if ( $has_multiple_expiring_subscriptions ) {
+ if ( $is_mixed_subscriptions ) {
+ return wp_sprintf(
+ /* translators: 1: Number of days remaining. 2: Renewal link. */
+ _n(
+ 'Some of your subscriptions expire in %1$d day. %2$s.',
+ 'Some of your subscriptions expire in %1$d days. %2$s.',
+ (int) $expiry_data['days_remaining'],
+ 'memberful'
+ ),
+ (int) $expiry_data['days_remaining'],
+ $link
+ );
+ }
+
return wp_sprintf(
/* translators: 1: Number of days remaining. 2: Renewal link. */
_n(
- 'Some of your subscriptions expire in %1$d day. %2$s.',
- 'Some of your subscriptions expire in %1$d days. %2$s.',
+ 'Your subscriptions expire in %1$d day. %2$s.',
+ 'Your subscriptions expire in %1$d days. %2$s.',
(int) $expiry_data['days_remaining'],
'memberful'
),
diff --git a/wordpress/wp-content/plugins/memberful-wp/src/options.php b/wordpress/wp-content/plugins/memberful-wp/src/options.php
index 23dd01f7..8171d53f 100755
--- a/wordpress/wp-content/plugins/memberful-wp/src/options.php
+++ b/wordpress/wp-content/plugins/memberful-wp/src/options.php
@@ -24,7 +24,7 @@ function memberful_wp_all_options() {
'memberful_filter_account_menu_items' => TRUE,
'memberful_auto_sync_display_names' => FALSE,
'memberful_show_protected_content_in_search' => FALSE,
- 'memberful_expiry_banner_enabled' => false,
+ 'memberful_expiry_banner_enabled' => FALSE,
'memberful_expiry_banner_days' => 7,
'memberful_use_global_marketing' => FALSE,
'memberful_use_global_snippets' => TRUE,
diff --git a/wordpress/wp-content/plugins/memberful-wp/views/expiry-banner.php b/wordpress/wp-content/plugins/memberful-wp/views/expiry-banner.php
index 562ca42f..f09cec70 100644
--- a/wordpress/wp-content/plugins/memberful-wp/views/expiry-banner.php
+++ b/wordpress/wp-content/plugins/memberful-wp/views/expiry-banner.php
@@ -16,6 +16,6 @@
-
From 4c3a6613978c179c60ee9b2d58612984f156af6b Mon Sep 17 00:00:00 2001
From: Zachary Hickson
Date: Fri, 17 Apr 2026 09:47:57 +1000
Subject: [PATCH 10/13] Add same calender-day calculation logic for same day
wording
---
.../memberful-wp/src/expiry_banner.php | 35 ++++++++++++++++++-
1 file changed, 34 insertions(+), 1 deletion(-)
diff --git a/wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php b/wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php
index 7fa71f86..ee9852e2 100644
--- a/wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php
+++ b/wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php
@@ -189,7 +189,7 @@ function memberful_wp_get_soonest_expiring_subscription( $user_id ) {
}
if ( $should_replace_soonest ) {
- $days_remaining = $is_expired ? 0 : (int) ceil( $seconds_remaining / DAY_IN_SECONDS );
+ $days_remaining = memberful_wp_get_subscription_days_remaining( $expires_at, $now );
$soonest = array(
'expires_at' => $expires_at,
@@ -368,6 +368,39 @@ function memberful_wp_parse_expiry_timestamp( $expires_at ) {
return (int) $parsed_time;
}
+/**
+ * Calculates remaining subscription days in the site timezone.
+ *
+ * @param int $expires_at Subscription expiry timestamp.
+ * @param int $now Current timestamp.
+ *
+ * @return int
+ */
+function memberful_wp_get_subscription_days_remaining( $expires_at, $now ) {
+ if ( $expires_at <= $now ) {
+ return 0;
+ }
+
+ $timezone = wp_timezone();
+ $current_date = wp_date( 'Y-m-d', $now, $timezone );
+ $expiry_date = wp_date( 'Y-m-d', $expires_at, $timezone );
+
+ if ( $current_date === $expiry_date ) {
+ return 0;
+ }
+
+ $current_date_object = date_create_immutable_from_format( '!Y-m-d', $current_date, $timezone );
+ $expiry_date_object = date_create_immutable_from_format( '!Y-m-d', $expiry_date, $timezone );
+
+ if ( false === $current_date_object || false === $expiry_date_object ) {
+ return (int) ceil( ( $expires_at - $now ) / DAY_IN_SECONDS );
+ }
+
+ $days_remaining = (int) $current_date_object->diff( $expiry_date_object )->format( '%a' );
+
+ return max( 1, $days_remaining );
+}
+
/**
* Checks whether subscription auto-renew is enabled.
*
From 8fa437f5e1123d48851f8d4a5e211ca188bcc8d6 Mon Sep 17 00:00:00 2001
From: Zachary Hickson
Date: Fri, 17 Apr 2026 10:04:49 +1000
Subject: [PATCH 11/13] Unminify Expiry Banner JS, move to standard enqueue
pattern
---
.../memberful-wp/js/src/expiry-banner.js | 71 +++++++++++++++++++
.../memberful-wp/src/expiry_banner.php | 32 +++++++++
.../memberful-wp/views/expiry-banner.php | 7 +-
.../plugins/memberful-wp/webpack.config.js | 1 +
4 files changed, 106 insertions(+), 5 deletions(-)
create mode 100644 wordpress/wp-content/plugins/memberful-wp/js/src/expiry-banner.js
diff --git a/wordpress/wp-content/plugins/memberful-wp/js/src/expiry-banner.js b/wordpress/wp-content/plugins/memberful-wp/js/src/expiry-banner.js
new file mode 100644
index 00000000..15c88f4b
--- /dev/null
+++ b/wordpress/wp-content/plugins/memberful-wp/js/src/expiry-banner.js
@@ -0,0 +1,71 @@
+(() => {
+ const dismissStorageKey = "memberful_expiry_banner_dismissed";
+ const hiddenClass = "memberful-expiry-banner--js-hidden";
+ const visibleClass = "memberful-expiry-banner-visible";
+ const banner = document.getElementById("memberful-expiry-banner");
+
+ if (!banner) {
+ return;
+ }
+
+ let bumpStyle = document.getElementById("memberful-expiry-banner-bump-style");
+
+ if (!bumpStyle) {
+ bumpStyle = document.createElement("style");
+ bumpStyle.id = "memberful-expiry-banner-bump-style";
+ document.head.appendChild(bumpStyle);
+ }
+
+ const refreshOffset = () => {
+ const height = banner.classList.contains(hiddenClass) ? 0 : banner.offsetHeight;
+
+ if (document.body) {
+ document.body.classList.toggle(visibleClass, height > 0);
+ }
+
+ bumpStyle.textContent = `@media screen { html { margin-top: calc(var(--wp-admin--admin-bar--height, 0px) + ${height}px) !important; } }`;
+ };
+
+ const isDismissed = () => {
+ if (!window.sessionStorage) {
+ return false;
+ }
+
+ try {
+ return window.sessionStorage.getItem(dismissStorageKey) === "1";
+ } catch (error) {
+ return false;
+ }
+ };
+
+ const markDismissed = () => {
+ if (!window.sessionStorage) {
+ return;
+ }
+
+ try {
+ window.sessionStorage.setItem(dismissStorageKey, "1");
+ } catch (error) {
+ // Ignore session storage write failures.
+ }
+ };
+
+ if (isDismissed()) {
+ banner.classList.add(hiddenClass);
+ } else {
+ banner.classList.remove(hiddenClass);
+ const dismissButton = banner.querySelector(".memberful-expiry-banner__dismiss");
+
+ if (dismissButton) {
+ dismissButton.addEventListener("click", () => {
+ markDismissed();
+ banner.classList.add(hiddenClass);
+ refreshOffset();
+ });
+ }
+ }
+
+ window.addEventListener("resize", refreshOffset);
+ window.addEventListener("orientationchange", refreshOffset);
+ refreshOffset();
+})();
diff --git a/wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php b/wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php
index ee9852e2..ba868e09 100644
--- a/wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php
+++ b/wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php
@@ -91,6 +91,7 @@ function memberful_wp_render_expiry_banner() {
$aria_live = (string) apply_filters( 'memberful_expiry_banner_aria_live', $aria_live, $expiry_data );
$has_rendered = true;
+ memberful_wp_enqueue_expiry_banner_script();
ob_start();
memberful_wp_render(
@@ -114,6 +115,37 @@ function memberful_wp_render_expiry_banner() {
echo apply_filters( 'memberful_expiry_banner_html', $html, $expiry_data );
}
+/**
+ * Enqueues the expiry banner script.
+ *
+ * @return void
+ */
+function memberful_wp_enqueue_expiry_banner_script() {
+ static $is_enqueued = false;
+
+ if ( $is_enqueued ) {
+ return;
+ }
+
+ $script_asset_path = MEMBERFUL_DIR . '/js/build/expiry-banner.asset.php';
+ $script_asset_info = file_exists( $script_asset_path )
+ ? include $script_asset_path
+ : array(
+ 'dependencies' => array(),
+ 'version' => MEMBERFUL_VERSION,
+ );
+
+ wp_enqueue_script(
+ 'memberful-expiry-banner',
+ plugins_url( 'js/build/expiry-banner.js', MEMBERFUL_PLUGIN_FILE ),
+ $script_asset_info['dependencies'],
+ $script_asset_info['version'],
+ true
+ );
+
+ $is_enqueued = true;
+}
+
/**
* Returns soonest subscription expiry data for a user within threshold.
*
diff --git a/wordpress/wp-content/plugins/memberful-wp/views/expiry-banner.php b/wordpress/wp-content/plugins/memberful-wp/views/expiry-banner.php
index f09cec70..af4b594d 100644
--- a/wordpress/wp-content/plugins/memberful-wp/views/expiry-banner.php
+++ b/wordpress/wp-content/plugins/memberful-wp/views/expiry-banner.php
@@ -6,7 +6,7 @@
*/
?>
-
+
-
diff --git a/wordpress/wp-content/plugins/memberful-wp/webpack.config.js b/wordpress/wp-content/plugins/memberful-wp/webpack.config.js
index b32e4222..738e9e7b 100644
--- a/wordpress/wp-content/plugins/memberful-wp/webpack.config.js
+++ b/wordpress/wp-content/plugins/memberful-wp/webpack.config.js
@@ -5,5 +5,6 @@ module.exports = {
entry: {
...defaultConfig.entry,
"editor-scripts": "./js/src/editor-scripts.js",
+ "expiry-banner": "./js/src/expiry-banner.js",
},
};
From 312e60850d901ffd162b68512e9aa82b643d41f2 Mon Sep 17 00:00:00 2001
From: Zachary Hickson
Date: Fri, 17 Apr 2026 10:31:02 +1000
Subject: [PATCH 12/13] Minor refactor of filters in expiry banner rendering
---
.../memberful-wp/src/expiry_banner.php | 32 +++++++++----------
1 file changed, 16 insertions(+), 16 deletions(-)
diff --git a/wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php b/wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php
index ba868e09..86867b4d 100644
--- a/wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php
+++ b/wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php
@@ -67,28 +67,28 @@ function memberful_wp_render_expiry_banner() {
$message = apply_filters( 'memberful_expiry_banner_message', $message, $expiry_data );
$is_expired = ! empty( $expiry_data['is_expired'] );
- $aria_role = $is_expired ? 'alert' : 'status';
- $aria_live = $is_expired ? 'assertive' : 'polite';
+ $aria_attributes = array(
+ 'role' => $is_expired ? 'alert' : 'status',
+ 'live' => $is_expired ? 'assertive' : 'polite',
+ );
/**
- * Filters the ARIA role used for the expiry banner live region.
+ * Filters the ARIA attributes used for the expiry banner live region.
*
- * @param string $aria_role The computed ARIA role.
- * @param array $expiry_data The computed expiry data for the current user.
+ * @param array $aria_attributes {
+ * ARIA attributes for the expiry banner.
*
- * @return string The ARIA role for the banner.
- */
- $aria_role = (string) apply_filters( 'memberful_expiry_banner_aria_role', $aria_role, $expiry_data );
-
- /**
- * Filters the ARIA live mode used for the expiry banner.
- *
- * @param string $aria_live The computed ARIA live value.
- * @param array $expiry_data The computed expiry data for the current user.
+ * @type string $role The ARIA role value.
+ * @type string $live The ARIA live mode value.
+ * }
+ * @param array $expiry_data The computed expiry data for the current user.
*
- * @return string The ARIA live value for the banner.
+ * @return array The filtered ARIA attribute values.
*/
- $aria_live = (string) apply_filters( 'memberful_expiry_banner_aria_live', $aria_live, $expiry_data );
+ $aria_attributes = apply_filters( 'memberful_expiry_banner_aria_attributes', $aria_attributes, $expiry_data );
+ $aria_attributes = is_array( $aria_attributes ) ? $aria_attributes : array();
+ $aria_role = isset( $aria_attributes['role'] ) ? (string) $aria_attributes['role'] : 'status';
+ $aria_live = isset( $aria_attributes['live'] ) ? (string) $aria_attributes['live'] : 'polite';
$has_rendered = true;
memberful_wp_enqueue_expiry_banner_script();
From 4c08d5515c6e3c25bfea21c774c7b01b17358590 Mon Sep 17 00:00:00 2001
From: Zachary Hickson
Date: Thu, 23 Apr 2026 09:10:22 +1000
Subject: [PATCH 13/13] Fix edge case by tracking expired_subscriptions_count
---
.../memberful-wp/src/expiry_banner.php | 59 +++++++++++++++++++
1 file changed, 59 insertions(+)
diff --git a/wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php b/wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php
index 86867b4d..0debef36 100644
--- a/wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php
+++ b/wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php
@@ -176,6 +176,7 @@ function memberful_wp_get_soonest_expiring_subscription( $user_id ) {
$threshold_timestamp = $now + ( $days_threshold * DAY_IN_SECONDS );
$soonest = null;
$expiring_subscriptions_count = 0;
+ $expired_subscriptions_count = 0;
$active_subscriptions_count = 0;
foreach ( $subscriptions as $subscription ) {
@@ -205,6 +206,9 @@ function memberful_wp_get_soonest_expiring_subscription( $user_id ) {
}
++$expiring_subscriptions_count;
+ if ( $is_expired ) {
+ ++$expired_subscriptions_count;
+ }
$should_replace_soonest = false;
@@ -236,6 +240,7 @@ function memberful_wp_get_soonest_expiring_subscription( $user_id ) {
}
$soonest['expiring_subscriptions_count'] = $expiring_subscriptions_count;
+ $soonest['expired_subscriptions_count'] = $expired_subscriptions_count;
$soonest['active_subscriptions_count'] = $active_subscriptions_count;
return $soonest;
@@ -256,9 +261,12 @@ function memberful_wp_expiry_banner_message( array $expiry_data, $account_url )
esc_html__( 'Renew now', 'memberful' )
);
$expiring_subscriptions_count = max( 1, (int) ( $expiry_data['expiring_subscriptions_count'] ?? 1 ) );
+ $expired_subscriptions_count = max( 0, (int) ( $expiry_data['expired_subscriptions_count'] ?? 0 ) );
+ $upcoming_subscriptions_count = max( 0, $expiring_subscriptions_count - $expired_subscriptions_count );
$active_subscriptions_count = max( 0, (int) ( $expiry_data['active_subscriptions_count'] ?? 0 ) );
$is_mixed_subscriptions = $active_subscriptions_count > 0;
$has_multiple_expiring_subscriptions = $expiring_subscriptions_count > 1;
+ $has_expired_subscriptions = $expired_subscriptions_count > 0;
if ( ! empty( $expiry_data['is_expired'] ) ) {
if ( $has_multiple_expiring_subscriptions ) {
@@ -293,6 +301,20 @@ function memberful_wp_expiry_banner_message( array $expiry_data, $account_url )
}
if ( (int) $expiry_data['days_remaining'] <= 0 ) {
+ if ( $has_expired_subscriptions ) {
+ return wp_sprintf(
+ /* translators: 1: Number of subscriptions expiring today. 2: Renewal link. */
+ _n(
+ 'Some of your subscriptions have expired; another expires today. %2$s.',
+ 'Some of your subscriptions have expired; %1$d others expire today. %2$s.',
+ $upcoming_subscriptions_count,
+ 'memberful'
+ ),
+ $upcoming_subscriptions_count,
+ $link
+ );
+ }
+
if ( $has_multiple_expiring_subscriptions ) {
if ( $is_mixed_subscriptions ) {
return wp_sprintf(
@@ -324,6 +346,43 @@ function memberful_wp_expiry_banner_message( array $expiry_data, $account_url )
);
}
+ if ( $has_expired_subscriptions ) {
+ if ( $upcoming_subscriptions_count <= 0 ) {
+ return wp_sprintf(
+ /* translators: %s is the renewal link. */
+ __( 'Some of your subscriptions have expired. %s.', 'memberful' ),
+ $link
+ );
+ }
+
+ if ( 1 === $upcoming_subscriptions_count ) {
+ return wp_sprintf(
+ /* translators: 1: Number of days remaining. 2: Renewal link. */
+ _n(
+ 'Some of your subscriptions have expired; another expires in %1$d day. %2$s.',
+ 'Some of your subscriptions have expired; another expires in %1$d days. %2$s.',
+ (int) $expiry_data['days_remaining'],
+ 'memberful'
+ ),
+ (int) $expiry_data['days_remaining'],
+ $link
+ );
+ }
+
+ return wp_sprintf(
+ /* translators: 1: Number of subscriptions still expiring. 2: Number of days remaining. 3: Renewal link. */
+ _n(
+ 'Some of your subscriptions have expired; %1$d others expire in %2$d day. %3$s.',
+ 'Some of your subscriptions have expired; %1$d others expire in %2$d days. %3$s.',
+ (int) $expiry_data['days_remaining'],
+ 'memberful'
+ ),
+ $upcoming_subscriptions_count,
+ (int) $expiry_data['days_remaining'],
+ $link
+ );
+ }
+
if ( $has_multiple_expiring_subscriptions ) {
if ( $is_mixed_subscriptions ) {
return wp_sprintf(