Show pending memberships more clearly on the account page - #3714
Show pending memberships more clearly on the account page#3714kimcoleman wants to merge 3 commits into
Conversation
Only treat a pending order as live if it is the user's most recent order for that level, and suppress it once the user has started a different level in the same single-selection group after the order was created (they chose another plan). Covers the card list, the card message, and the action link, since all three route through pmpro_get_pending_order_for_user_level(). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
flintfromthebasement
left a comment
There was a problem hiding this comment.
PR: #3714 — Show pending memberships more clearly on the account page
kimcoleman → dev | 2 files, +212 -3 lines
#3714
Summary
Clean, well-reasoned feature. The staleness logic in pmpro_get_pending_order_for_user_level() is solid — "most recent order for this level must be pending, and no sibling group level was started after it" — and the static cache is correctly keyed by user_id:level_id. The shortcode restructuring from if/else to two independent blocks is the right move. Two minor concerns below; nothing blocking.
Issues
Minor includes/functions.php ~line 5506 — $held_level->startdate >= $recent_order->timestamp compares two values with different timezone provenance. startdate comes from UNIX_TIMESTAMP(CONVERT_TZ(startdate, '+00:00', @@global.time_zone)) — the MySQL global timezone determines what UNIX_TIMESTAMP sees. $recent_order->timestamp is strtotime( $dbobj->timestamp ) in the MemberOrder constructor — PHP interprets the raw MySQL DATETIME using the PHP default timezone. If @@global.time_zone and date_default_timezone_set() differ (not uncommon on shared hosts), the comparison can be off by hours. A level started 30 seconds after a pending order was created could be incorrectly treated as not superseding it. Low probability on a well-configured install, but worth a docblock note: // Both timestamps are Unix; assumes MySQL @@global.time_zone matches the PHP default timezone.
Minor pmpro_membership_account_after_level_card_content and pmpro_member_action_links now also fire for pending-only level cards. Core handled this correctly — pmpro_display_member_account_level_message() got the pmpro_hasMembershipLevel() guard. But any third-party code hooked to these actions that assumes the user holds that level will silently output wrong content on pending-only cards. Call this out explicitly in the changelog as a hook behavior change so Add On / custom-code authors know to add their own guard.
Questions
If PMPro deactivates a membership after a failed recurring payment (level moves to cancelled, user drops out of $mylevels), the user gets a pending-only card via $pending_order_levels. pmpro_hasMembershipLevel() returns false, so the message reads "We are waiting for your payment to be completed. Your membership will be activated once the payment has been confirmed." That reads like a first-time purchase, not a lapsed renewal. Is this scenario reachable in your supported gateway configurations (i.e., does any gateway immediately cancel rather than holding the level active while retrying)? If yes, the copy distinction matters.
Looks Good
The two-condition staleness check is exactly right — stale-if-newer-order-exists catches the "paid again by another method" case, stale-if-group-sibling-started-after catches the "switched plans" case. The invoice link being suppressed when update-billing is already present avoids a confusing duplicate path. All output is properly escaped (esc_html, esc_url, esc_attr, wp_kses). Property access on MemberOrder objects via __get correctly surfaces status, timestamp, and membership_id despite their private declarations.
What
Surfaces memberships that are waiting on payment on the Membership Account page, so members can see the status of an order that hasn't completed yet (e.g. Pay by Check, async gateway confirmations, or a failed recurring payment).
Both surfaces reuse the existing
pmpro_membership_account_after_level_card_contentaction andpmpro_member_action_linksfilter, so the message and links route through a single helper,pmpro_get_pending_order_for_user_level().Not surfacing stale pending orders
A pending order is only treated as live when:
Known limitation
A pending order that's simply abandoned (e.g. a Pay by Check checkout where the member never pays and never buys anything else) will keep showing its card indefinitely. There's no data signal that distinguishes "abandoned" from "check is in the mail," so any age-out is a product/policy decision — flagging for discussion rather than guessing.
🤖 Generated with Claude Code