From 1d61e495e04dee0234eb7e0ddd7ce66529cb7b7c Mon Sep 17 00:00:00 2001 From: Zachary Hickson Date: Wed, 11 Feb 2026 13:45:59 +1100 Subject: [PATCH 01/13] Initial expiry banner feature commit --- .../plugins/memberful-wp/memberful-wp.php | 1 + .../plugins/memberful-wp/src/admin.php | 8 +- .../memberful-wp/src/expiry_banner.php | 197 ++++++++++++++++++ .../plugins/memberful-wp/src/options.php | 2 + .../memberful-wp/views/expiry-banner.php | 46 ++++ .../plugins/memberful-wp/views/options.php | 12 ++ 6 files changed, 265 insertions(+), 1 deletion(-) create mode 100644 wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php create mode 100644 wordpress/wp-content/plugins/memberful-wp/views/expiry-banner.php diff --git a/wordpress/wp-content/plugins/memberful-wp/memberful-wp.php b/wordpress/wp-content/plugins/memberful-wp/memberful-wp.php index eeb0aadc..bf450384 100755 --- a/wordpress/wp-content/plugins/memberful-wp/memberful-wp.php +++ b/wordpress/wp-content/plugins/memberful-wp/memberful-wp.php @@ -50,6 +50,7 @@ require_once MEMBERFUL_DIR . '/src/search_filter.php'; require_once MEMBERFUL_DIR . '/src/entities.php'; require_once MEMBERFUL_DIR . '/src/embed.php'; +require_once MEMBERFUL_DIR . '/src/expiry_banner.php'; require_once MEMBERFUL_DIR . '/src/api.php'; require_once MEMBERFUL_DIR . '/src/roles.php'; require_once MEMBERFUL_DIR . '/src/syncing.php'; diff --git a/wordpress/wp-content/plugins/memberful-wp/src/admin.php b/wordpress/wp-content/plugins/memberful-wp/src/admin.php index 724e2e7e..5f70aa4e 100755 --- a/wordpress/wp-content/plugins/memberful-wp/src/admin.php +++ b/wordpress/wp-content/plugins/memberful-wp/src/admin.php @@ -273,6 +273,8 @@ function memberful_wp_options() { update_option( 'memberful_filter_account_menu_items', isset( $_POST['memberful_filter_account_menu_items'] )); update_option( 'memberful_auto_sync_display_names', isset( $_POST['memberful_auto_sync_display_names'] ) ); update_option( 'memberful_show_protected_content_in_search', isset( $_POST['memberful_show_protected_content_in_search'] ) ); + update_option( 'memberful_expiry_banner_enabled', isset( $_POST['memberful_expiry_banner_enabled'] ) ); + update_option( 'memberful_expiry_banner_days', min( 90, max( 1, (int) ( $_POST['memberful_expiry_banner_days'] ?? 7 ) ) ) ); return wp_redirect( admin_url( 'options-general.php?page=memberful_options' ) ); } @@ -310,6 +312,8 @@ function memberful_wp_options() { $filter_account_menu_items = get_option( 'memberful_filter_account_menu_items' ); $auto_sync_display_names = get_option( 'memberful_auto_sync_display_names' ); $show_protected_content_in_search = get_option( 'memberful_show_protected_content_in_search' ); + $expiry_banner_enabled = get_option( 'memberful_expiry_banner_enabled' ); + $expiry_banner_days = get_option( 'memberful_expiry_banner_days', 7 ); memberful_wp_render ( 'options', @@ -322,7 +326,9 @@ function memberful_wp_options() { 'block_dashboard_access' => $block_dashboard_access, 'filter_account_menu_items' => $filter_account_menu_items, 'auto_sync_display_names' => $auto_sync_display_names, - 'show_protected_content_in_search' => $show_protected_content_in_search + 'show_protected_content_in_search' => $show_protected_content_in_search, + 'expiry_banner_enabled' => $expiry_banner_enabled, + 'expiry_banner_days' => $expiry_banner_days ) ); } diff --git a/wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php b/wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php new file mode 100644 index 00000000..fe028dc2 --- /dev/null +++ b/wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php @@ -0,0 +1,197 @@ + $message + ) + ); + $html = ob_get_clean(); + + /** + * Filters the full expiry banner HTML output. + * + * @param string $html The full banner HTML output. + * @param array $expiry_data The computed expiry data for the current user. + * + * @return string The filtered banner HTML output. + */ + echo apply_filters( 'memberful_expiry_banner_html', $html, $expiry_data ); +} + +/** + * Returns soonest subscription expiry data for a user within threshold. + * + * @param int $user_id User ID. + * + * @return array|null + */ +function memberful_wp_get_soonest_expiring_subscription( $user_id ) { + $subscriptions = get_user_meta( $user_id, 'memberful_subscription', true ); + + if ( empty( $subscriptions ) || ! is_array( $subscriptions ) ) { + return null; + } + + $days_threshold = min( 90, max( 1, (int) get_option( 'memberful_expiry_banner_days', 7 ) ) ); + + /** + * Filters the number of days before expiry that triggers the banner. + * + * @param int $days_threshold The configured day threshold. + * + * @return int The filtered day threshold. + */ + $days_threshold = (int) apply_filters( 'memberful_expiry_banner_days_threshold', $days_threshold ); + $days_threshold = min( 90, max( 1, $days_threshold ) ); + + $now = time(); + $threshold_timestamp = $now + ( $days_threshold * DAY_IN_SECONDS ); + $soonest = null; + + foreach ( $subscriptions as $subscription ) { + if ( empty( $subscription['expires_at'] ) ) { + continue; + } + + $expires_at = memberful_wp_parse_expiry_timestamp( $subscription['expires_at'] ); + + if ( empty( $expires_at ) ) { + continue; + } + + if ( $expires_at > $threshold_timestamp ) { + continue; + } + + 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( + 'expires_at' => $expires_at, + 'days_remaining' => $days_remaining, + 'is_expired' => $is_expired, + ); + } + } + + return $soonest; +} + +/** + * Builds the user-facing banner message. + * + * @param array $expiry_data Expiry information array. + * @param string $account_url Memberful account URL. + * + * @return string + */ +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' ) + ); + + if ( ! empty( $expiry_data['is_expired'] ) ) { + return wp_sprintf( + /* translators: %s is the update membership link. */ + __( 'Your membership has expired. %s.', 'memberful' ), + $link + ); + } + + if ( (int) $expiry_data['days_remaining'] <= 0 ) { + return wp_sprintf( + /* translators: %s is the update membership link. */ + __( 'Your membership expires today. %s.', 'memberful' ), + $link + ); + } + + return wp_sprintf( + /* translators: 1: Number of days remaining. 2: Update membership link. */ + _n( + 'Your membership expires in %1$d day. %2$s.', + 'Your membership expires in %1$d days. %2$s.', + (int) $expiry_data['days_remaining'], + 'memberful' + ), + (int) $expiry_data['days_remaining'], + $link + ); +} + +/** + * Converts an expiry value into a Unix timestamp. + * + * @param mixed $expires_at Expiry value from user meta. + * + * @return int + */ +function memberful_wp_parse_expiry_timestamp( $expires_at ) { + if ( is_numeric( $expires_at ) ) { + return (int) $expires_at; + } + + $parsed_time = strtotime( (string) $expires_at ); + + if ( false === $parsed_time ) { + return 0; + } + + return (int) $parsed_time; +} diff --git a/wordpress/wp-content/plugins/memberful-wp/src/options.php b/wordpress/wp-content/plugins/memberful-wp/src/options.php index 36394faa..23dd01f7 100755 --- a/wordpress/wp-content/plugins/memberful-wp/src/options.php +++ b/wordpress/wp-content/plugins/memberful-wp/src/options.php @@ -24,6 +24,8 @@ 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_days' => 7, 'memberful_use_global_marketing' => FALSE, 'memberful_use_global_snippets' => TRUE, 'memberful_global_marketing_override' => 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 new file mode 100644 index 00000000..58d301a1 --- /dev/null +++ b/wordpress/wp-content/plugins/memberful-wp/views/expiry-banner.php @@ -0,0 +1,46 @@ + + + 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 @@ */ ?> -