MR-9: Add membership expiry warning banner for members#7
MR-9: Add membership expiry warning banner for members#7
Conversation
WalkthroughThis PR introduces an expiry banner feature for the Memberful WordPress plugin. The implementation includes a dismissable global banner displayed to logged-in users with memberships expiring within a configurable threshold (default 7 days) or already expired, along with admin settings to enable/disable the feature and configure the day threshold. Changes
Sequence DiagramsequenceDiagram
participant Browser as User's Browser
participant WP as WordPress
participant Footer as wp_footer Hook
participant Banner as expiry_banner.php
participant Meta as User Meta
participant View as View Template
participant Session as SessionStorage
Browser->>WP: Load frontend page
WP->>Footer: Trigger wp_footer action
Footer->>Banner: memberful_wp_render_expiry_banner()
Banner->>Banner: Check if logged in & enabled
alt Not logged in or disabled
Banner->>WP: Early return
else Proceed
Banner->>Meta: Fetch user memberful_subscription meta
Meta-->>Banner: Subscription array
Banner->>Banner: memberful_wp_get_soonest_expiring_subscription()
Banner->>Banner: Filter by threshold (default 7 days)
Banner-->>Banner: Return soonest expiry or null
alt No expiring subscription found
Banner->>WP: Return
else Subscription found
Banner->>Banner: memberful_wp_expiry_banner_message()
Banner-->>Banner: Build localised message
Banner->>View: Render expiry-banner template
View->>View: Output HTML with message & dismiss button
View-->>Browser: Display banner
end
end
Browser->>Session: Check sessionStorage for dismissal flag
Session-->>Browser: Flag status
alt Dismissed previously this session
Browser->>View: Hide banner via JavaScript
else Not dismissed
Browser->>View: Show banner
View->>Browser: User sees banner
Browser->>View: User clicks dismiss button
View->>Session: Store dismissal flag in sessionStorage
View->>Browser: Hide banner
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (3)
wordpress/wp-content/plugins/memberful-wp/src/options.php (1)
27-28: Minor style inconsistency:falsevsFALSE.The existing options in this array use uppercase
FALSE/TRUE(e.g., lines 20, 26), while these new entries use lowercasefalse. Functionally identical in PHP, but worth aligning for consistency.🔧 Suggested diff
- 'memberful_expiry_banner_enabled' => false, + 'memberful_expiry_banner_enabled' => FALSE,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wordpress/wp-content/plugins/memberful-wp/src/options.php` around lines 27 - 28, The new option entries 'memberful_expiry_banner_enabled' and 'memberful_expiry_banner_days' use lowercase boolean literal false; update 'memberful_expiry_banner_enabled' to use the uppercase PHP boolean literal FALSE to match the surrounding options style (e.g., other entries using TRUE/FALSE) so the array remains stylistically consistent.wordpress/wp-content/plugins/memberful-wp/views/expiry-banner.php (1)
19-21:asyncanddeferattributes are no-ops on inline scripts.These attributes only have effect on
<script>elements with asrcattribute. On inline scripts they are silently ignored by browsers. Removing them avoids confusion.🔧 Suggested diff
-<script async defer> +<script>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wordpress/wp-content/plugins/memberful-wp/views/expiry-banner.php` around lines 19 - 21, The inline script in expiry-banner.php is using async and defer on a script tag that has no src (they're no-ops); remove the async and defer attributes from the <script> element wrapping the IIFE that references memberful-expiry-banner and the sessionStorage key "memberful_expiry_banner_dismissed" so the tag is a plain inline <script> and behavior remains identical (keep the IIFE, event listener on .memberful-expiry-banner__dismiss, and sessionStorage logic unchanged).wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php (1)
10-17: Skipping admins — consider documenting the rationale or using a filter.Users with
manage_optionsare unconditionally excluded (line 15). If an admin also has a Memberful membership (e.g., for testing), they will never see the banner. This is likely intentional, but it's not overridable. Consider making this filterable for edge cases, or at least adding a brief inline comment explaining why admins are excluded.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php` around lines 10 - 17, The code unconditionally skips users with current_user_can('manage_options') inside memberful_wp_render_expiry_banner which prevents admins from ever seeing the expiry banner; update the logic to be overridable by introducing a filter (e.g. call apply_filters('memberful_wp_show_expiry_to_admins', false) or similar) and use its result to decide whether to return, or if you prefer minimal change add a clear inline comment above the current_user_can check explaining why admins are excluded and that it is intentional; make sure the filter name and the current_user_can('manage_options') check are referenced so callers can opt into showing the banner to admins.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php`:
- Around line 129-155: The current loop over $subscriptions incorrectly picks
the earliest expires_at inside the threshold even if that subscription is
long-ago expired; change the selection to prefer a non-expired subscription:
inside the foreach tracking $expires_at (from
memberful_wp_parse_expiry_timestamp) compute $is_expired = ($expires_at - $now)
< 0 and then maintain two candidates—$best_active (non-expired, choose the one
with smallest expires_at) and $best_expired (expired, choose the one with
largest expires_at = most recently expired). Update $best_active when a
non-expired subscription has an earlier expires_at than the current
$best_active; update $best_expired when an expired subscription has a later
expires_at than the current $best_expired. After the loop set $soonest to
$best_active if present, otherwise to $best_expired, preserving existing
calculation of days_remaining and is_expired using $now and DAY_IN_SECONDS.
In `@wordpress/wp-content/plugins/memberful-wp/views/expiry-banner.php`:
- Around line 16-18: The fixed .memberful-expiry-banner overlaps page content
because it uses position:fixed without offsetting the document flow; update the
display logic that renders .memberful-expiry-banner to also add/remove a
matching top offset to the page (e.g., set document.body.style.paddingTop =
banner.offsetHeight + 'px' when the banner is shown and clear it on dismiss) or
insert a spacer element of height equal to banner.offsetHeight immediately
before the main content; ensure this adjustment is applied after layout (or on
window resize) and is removed when the .memberful-expiry-banner is dismissed so
focus targets and skip-links are not hidden.
---
Nitpick comments:
In `@wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php`:
- Around line 10-17: The code unconditionally skips users with
current_user_can('manage_options') inside memberful_wp_render_expiry_banner
which prevents admins from ever seeing the expiry banner; update the logic to be
overridable by introducing a filter (e.g. call
apply_filters('memberful_wp_show_expiry_to_admins', false) or similar) and use
its result to decide whether to return, or if you prefer minimal change add a
clear inline comment above the current_user_can check explaining why admins are
excluded and that it is intentional; make sure the filter name and the
current_user_can('manage_options') check are referenced so callers can opt into
showing the banner to admins.
In `@wordpress/wp-content/plugins/memberful-wp/src/options.php`:
- Around line 27-28: The new option entries 'memberful_expiry_banner_enabled'
and 'memberful_expiry_banner_days' use lowercase boolean literal false; update
'memberful_expiry_banner_enabled' to use the uppercase PHP boolean literal FALSE
to match the surrounding options style (e.g., other entries using TRUE/FALSE) so
the array remains stylistically consistent.
In `@wordpress/wp-content/plugins/memberful-wp/views/expiry-banner.php`:
- Around line 19-21: The inline script in expiry-banner.php is using async and
defer on a script tag that has no src (they're no-ops); remove the async and
defer attributes from the <script> element wrapping the IIFE that references
memberful-expiry-banner and the sessionStorage key
"memberful_expiry_banner_dismissed" so the tag is a plain inline <script> and
behavior remains identical (keep the IIFE, event listener on
.memberful-expiry-banner__dismiss, and sessionStorage logic unchanged).
| 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, | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cd /tmp && find . -type f -name "*.php" 2>/dev/null | head -1 && echo "---" && rg -n 'memberful_subscription' --type=php -C 3 | head -100Repository: TheCodeCompany/memberful-wp
Length of output: 240
🏁 Script executed:
# Check the actual file and understand the subscription data structure
find . -type f -name "expiry_banner.php" -o -name "*.php" -path "*/memberful*" | head -20Repository: TheCodeCompany/memberful-wp
Length of output: 1427
🏁 Script executed:
# Look at the expiry_banner.php file to see the full context
cat -n wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php | head -160Repository: TheCodeCompany/memberful-wp
Length of output: 5766
🏁 Script executed:
# Search for where subscriptions are stored/retrieved to understand data structure
rg -n 'memberful_subscription|get_user_meta.*memberful' --type=php -B 2 -A 5 | head -150Repository: TheCodeCompany/memberful-wp
Length of output: 15291
🏁 Script executed:
# Find where memberful_subscription is updated to understand the data structure
rg -n 'update_user_meta.*memberful_subscription|memberful_subscription.*update' --type=php -B 2 -A 5Repository: TheCodeCompany/memberful-wp
Length of output: 53
🏁 Script executed:
# Search for any webhook handlers or sync functions that update subscription data
rg -n 'webhook|sync.*subscription' --type=php -i | head -50Repository: TheCodeCompany/memberful-wp
Length of output: 3877
🏁 Script executed:
# Look for authenticator.php which likely handles user sync from Memberful API
cat -n wordpress/wp-content/plugins/memberful-wp/src/authenticator.php | head -200Repository: TheCodeCompany/memberful-wp
Length of output: 7362
🏁 Script executed:
# Find the Memberful_Wp_User_Subscriptions class
find . -name "*.php" -type f | xargs grep -l "class Memberful_Wp_User_Subscriptions"Repository: TheCodeCompany/memberful-wp
Length of output: 141
🏁 Script executed:
# Look at the user subscriptions file
cat -n wordpress/wp-content/plugins/memberful-wp/src/user/subscriptions.phpRepository: TheCodeCompany/memberful-wp
Length of output: 1192
🏁 Script executed:
# Also check syncing.php to see how subscriptions are synced
cat -n wordpress/wp-content/plugins/memberful-wp/src/syncing.php | head -100Repository: TheCodeCompany/memberful-wp
Length of output: 3702
🏁 Script executed:
# Check the parent class to understand how subscriptions are stored
cat -n wordpress/wp-content/plugins/memberful-wp/src/user/entity.phpRepository: TheCodeCompany/memberful-wp
Length of output: 1797
Correct the subscription selection logic to avoid prioritising old-expired subscriptions over active ones.
When a user has multiple subscriptions, the banner should display based on the soonest active subscription within the threshold. Currently, if a user has both an old-expired subscription (e.g., from 2 years ago) and an active subscription expiring soon, the banner will incorrectly show "Your membership has expired" for the old one.
The subscription data stored in user meta includes no 'active' or 'status' field—only expires_at. When the Memberful API syncs subscriptions, old expired ones persist in user meta. The current loop selects the subscription with the earliest expires_at timestamp within the threshold, regardless of age.
Implement the proposed fix: prioritise non-expired subscriptions within the threshold (soonest to expire), and only fall back to the most recently expired subscription if no active subscription exists.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@wordpress/wp-content/plugins/memberful-wp/src/expiry_banner.php` around lines
129 - 155, The current loop over $subscriptions incorrectly picks the earliest
expires_at inside the threshold even if that subscription is long-ago expired;
change the selection to prefer a non-expired subscription: inside the foreach
tracking $expires_at (from memberful_wp_parse_expiry_timestamp) compute
$is_expired = ($expires_at - $now) < 0 and then maintain two
candidates—$best_active (non-expired, choose the one with smallest expires_at)
and $best_expired (expired, choose the one with largest expires_at = most
recently expired). Update $best_active when a non-expired subscription has an
earlier expires_at than the current $best_active; update $best_expired when an
expired subscription has a later expires_at than the current $best_expired.
After the loop set $soonest to $best_active if present, otherwise to
$best_expired, preserving existing calculation of days_remaining and is_expired
using $now and DAY_IN_SECONDS.
| <style> | ||
| .memberful-expiry-banner{position:fixed;top:var(--wp-admin--admin-bar--height,0);left:0;right:0;z-index:var(--memberful-expiry-banner-z-index,9999);display:flex;align-items:center;justify-content:center;gap:1em;padding:.5em 1em;margin:0;font:inherit;font-size:.875rem;line-height:1.4;background:var(--memberful-expiry-banner-background,#fef3cd);color:var(--memberful-expiry-banner-colour,#664d03);border-bottom:1px solid var(--memberful-expiry-banner-border-colour,#e0c882)}.memberful-expiry-banner a{color:inherit;text-decoration:underline}.memberful-expiry-banner__message{margin:0}.memberful-expiry-banner__dismiss{min-width:2rem;min-height:2rem;margin:0;padding:0;border:0;background:0 0;color:inherit;cursor:pointer;font:inherit;font-size:1.25rem;line-height:1} | ||
| </style> |
There was a problem hiding this comment.
Fixed-position banner overlaps page content without offsetting body padding.
The banner uses position:fixed;top:… which means it will sit on top of page content. There is no corresponding body or content-area offset, so the first ~40px of page content will be hidden behind the banner. Consider adding a spacer element or adjusting body padding-top via the inline styles/script to prevent content from being obscured.
This is particularly relevant for accessibility — keyboard-focus or skip-link targets near the top of the page may be visually hidden behind the banner.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@wordpress/wp-content/plugins/memberful-wp/views/expiry-banner.php` around
lines 16 - 18, The fixed .memberful-expiry-banner overlaps page content because
it uses position:fixed without offsetting the document flow; update the display
logic that renders .memberful-expiry-banner to also add/remove a matching top
offset to the page (e.g., set document.body.style.paddingTop =
banner.offsetHeight + 'px' when the banner is shown and clear it on dismiss) or
insert a spacer element of height equal to banner.offsetHeight immediately
before the main content; ensure this adjustment is applied after layout (or on
window resize) and is removed when the .memberful-expiry-banner is dismissed so
focus targets and skip-links are not hidden.
Summary
This PR introduces a new membership expiry banner that helps members avoid interruptions by warning them before access lapses.
When enabled, logged-in members will see a clear banner if their membership has expired, expires today, or is due to expire soon. The warning window is configurable in plugin settings, so site owners can choose how early to remind members.
What's included
1-90days, default7).role/aria-live) and extension hooks for custom implementations.Scope and behaviour
Test plan
7) and save.Summary by CodeRabbit
Release Notes