Changelog
Every release, every fix, every time a version bump felt suspiciously significant.
What changed, when, and why you should (probably) care.
March 2026
v1.3.1
Released March 2026 — GitHub Release
Your memberships finally have a proper home. Plus a few admin upgrades that were quietly missing and a display bug that made one member look like twins.
My Memberships portal
Members can now view their plans, content access, drip progress, and membership history from the frontend — without bothering an admin or squinting at a shortcode that renders like it was designed in 2009.
- FluentCart account integration — a "Memberships" tab appears in the customer portal at
/account/memberships/, sitting alongside Purchase History and Downloads like it always should have - Standalone shortcode —
[fchub_my_memberships]now renders a proper Vue app instead of static HTML. Same data, dramatically less depressing - Plan dashboard cards — each plan shows expiry date, days remaining, content progress bar, and a collapsible content library with drip status
- Matches FluentCart's design — same fonts, badges, spacing, and general vibe. No rogue gradients, no surprise dark mode, no "designed by a different person" energy
Admin improvements
- Retention cohort heatmap — new tab in Reports showing month-by-month retention as a colour-coded matrix. Green means they stayed, red means they didn't. Maths has never been this judgemental
- Drip timeline drawer — click any plan tag in a member's profile to see the full drip schedule in a side panel. Unlock dates, notification status, the lot
Bug fix
- Members list no longer clones people — if a plan has multiple content rules (say, a category and a post), the member appeared once per rule in the admin list. Now grouped by user and plan, because one row per human per plan is the bare minimum of data hygiene
v1.3.0
Released March 2026 — GitHub Release
New feature: Membership Term. Plus a genuinely alarming number of bugs that were quietly lurking behind the curtain, some since v1.0.0.
Membership Term
Plans now have an absolute upper bound on how long a membership can last. Set it to 1 Year, 2 Years, 3 Years, a custom duration, or a specific date — works across all four duration types.
A lifetime plan can finally be "lifetime, but actually 3 years." A fixed_anchor school membership can bill monthly but auto-expire after 12 months. And fixed_days users can stop typing 730 and hoping they got the leap year right — just pick "2 Years" and move on.
- Presets and custom — 1y/2y/3y presets for the click-averse, custom value+unit (days, weeks, months, years), or a hard calendar deadline
- Stored per-grant — the term end date is calculated at grant creation and baked into
meta.membership_term_ends_at. Changing the plan's term later doesn't retroactively yank existing members - Renewal capping — every subscription renewal is capped at the term end. The final billing cycle may be shorter than usual
- Cron enforcement — the 5-minute validity check catches term-expired grants (including lifetime ones that would otherwise live forever) and marks them expired
- Feed-level override — integration feeds can override the plan's term. Different products, different term lengths, same plan
- New hook —
fchub_memberships/grant_term_expiredfires alongsidegrant_expiredwhen a grant reaches its term limit - Validation — sensible upper bounds (max 100 years, 36500 days, etc.) because nobody needs a 999999-year membership and
strtotimewas starting to have opinions about it
Bug fixes
This release includes fixes for 12 bugs across the grant lifecycle, content protection, trial conversion, and subscription watcher:
- Revocation now includes paused grants — previously,
revokePlan()only queried active grants. Paused anchor grants (overdue payment) were silently skipped and lived forever, which is impressive commitment from an unpaying member - Revocation hooks fire correctly — the
grant_revokedhook and notification emails no longer fire when nothing was actually revoked. Downstream automations (FluentCRM, webhooks) were getting false positives - Revocation by source respects status transitions —
revokeBySource()no longer sets already-expired grants back to "revoked," which was a state machine violation that would make any finite automata textbook weep - Source-based revocation now fires hooks and sends notifications — was silently revoking grants with no audit trail, no emails, and no webhook/FluentCRM events. Grants revoked by source were invisible to every downstream system
- Trial conversion handles all duration types — converting a trial to paid membership now correctly applies
fixed_anchor(with anchor day in grant meta) and membership term caps. Previously, lifetime-with-term trials converted to infinite access - Content protection cache invalidated on expiry — the 5-minute transient cache wasn't cleared when grants expired or hit their term limit. Users kept access for up to 5 minutes after expiry. Now both
grant_expiredandgrant_term_expiredtrigger cache invalidation - Excerpt filtering checks taxonomy access —
filterExcerpt()now checks taxonomy-based access, matchingfilterContent(). Users with access via a category grant no longer see their excerpts hidden in archive listings resume()checks term expiry — paused grants whose term expired during the pause were being resumed to active. Now checked and skipped- Renewal meta preserved — grant meta (including term end dates and anchor days) is now properly merged during renewals via
GrantCreationService. Previously silently dropped - Subscription watcher hooks match FluentCart — the watcher was listening for
fluent_cart/subscription_cancelled(double L) but FluentCart firesfluent_cart/subscription_canceled(single L). Also fixed missing/payments/prefix onsubscription_status_changed. Several subscription event handlers were simply never firing capExpiry()handles malformed dates — returns the proposed date instead of garbage when either input is unparseable- Validator rejects whitespace dates — a date field containing only spaces passed validation because
strtotime(' ')returns "now" on PHP 8.4. The kind of behaviour that's technically correct and spiritually bankrupt
v1.2.0
Released March 2026 — GitHub Release
New duration type: Fixed Billing Anchor. Because some businesses charge dues by a calendar date, and FluentCart's rolling billing had other ideas.
Fixed Billing Anchor
Added fixed_anchor as the 4th plan duration type. Set a billing anchor day (1-31), and the membership plugin takes over access timing — ignoring FluentCart's next_billing_date entirely. Students, gym members, or anyone on monthly dues can now have a consistent due date that doesn't drift when they pay late.
- Anchor day stored per-grant — immutable after creation, so changing the plan's anchor day doesn't retroactively shuffle every existing member's billing cycle
- Overdue grants pause, not expire — recoverable on late payment. The 5-minute cron detects overdue anchor grants and pauses them, triggering all the usual FluentCRM/webhook notifications
- Late payment resumes and snaps — when a late payment comes in, the grant resumes and the next expiry snaps to the following month's anchor. No date drift, ever
- Short month clamping — day 31 in February becomes the 28th (29th in leap years), restores to 31 when the next month allows it
- Full feed support —
anchor_billingvalidity mode available in integration feeds, and auto-linked when creating product feeds from anchor plans
Plan meta merge fix
Plan updates now merge incoming meta with existing values instead of wholesale replacement. Previously, saving a plan from the editor would silently nuke any meta keys that weren't part of the form. Harmless before this release (nothing else used plan meta), but would have been a delightful surprise later.
v1.1.0
Released 14 March 2026 — GitHub Release
Major refactor, plus the discovery that half the plugin's SQL was querying tables that don't exist. Rebuilt plugin structure, split admin components, reduced bundle size, expanded tests — and then fixed everything that was quietly broken underneath.
Refactored
Rebuilt the plugin's internal structure so the code is cleaner, smaller, and far less cursed to work on. Plans, members, reports, settings, and content tools are split into focused pieces instead of one enormous service. Admin bundle size cut dramatically. Proper local dev setup for tests and builds. Packaging and repo hygiene cleaned up.
FluentCart integration fixes
Every SQL query that touched FluentCart's product and integration feed data was pointing at the wrong tables. fct_order_integration_feeds doesn't exist — never has. FluentCart stores product integration feeds in fct_product_meta with object_type='product_integration'. Similarly, fct_products isn't a thing — products live in wp_posts with post type fluent-products, and pricing sits in fct_product_variations.
This broke:
- Linked products tab — showed nothing because the query silently returned empty
- Product search dialog — same story
- Link and unlink operations — inserted into and deleted from a void
- WP-CLI commands —
backfill,sync --feed, andsync --planall failed immediately - Subscription renewal — used
next_billing_atinstead of the actual column namenext_billing_date, so grants never got their expiry extended when a subscription renewed
All queries have been rewritten against the correct FluentCart schema.
Revenue reporting
Revenue amounts were displayed in cents because FluentCart stores total_amount as a BIGINT in the smallest currency unit. A 99 zł order showed as 9900.00 zł. Now properly converted. The currency symbol, position, and separators are pulled from FluentCart's store settings instead of hardcoding $.
FluentCRM integration
CheckoutUrlHelper::getLinkedProductId() queried the non-existent feeds table, breaking the {{membership.checkout_url}} and {{membership.upgrade_url}} smart codes in automation emails. Fixed to query fct_product_meta.
FluentCommunity badges
The adapter's grant() and revoke() calls were missing the $context parameter that carries plan_id. Without it, maybeAssignBadge() and maybeRevokeBadge() couldn't look up badge mappings. Badges are now correctly assigned on grant and removed on revocation.
Timezone and lifecycle fixes
- Grace period calculation used
gmdate()while queries usedcurrent_time('mysql')— grants expired at the wrong time depending on timezone offset. Standardised oncurrent_time(). - Trial expiration checks had the same timezone mismatch.
- Grant expiry maintenance fired hooks before the database update. If anything threw after the hook, the audit log said "expired" but the grant stayed active. DB update now runs first.
- Grace period expiry logged as generic "revoked" — now uses distinct
grace_period_revokedaudit type.
Admin UI
- Four Vue pages used Element Plus icons without importing them — icons rendered as empty boxes.
- Linking a product opened a confirmation dialog behind the link dialog, causing an overlay deadlock. Now uses a two-step flow inside a single dialog.
- Unlinking uses inline popconfirm instead of a stacked modal.
- Linked products tab shows all product variations (name, price, recurring/one-time) instead of just the first.
- Revenue chart ticks and stat widgets use the store's currency formatter.
v1.0.3
Released 6 March 2026 — GitHub Release
Fixed a fatal error when multiple FCHub plugins are active at the same time. The shared GitHubUpdater class used a class_exists guard that PHP's OPcache cheerfully ignored during early class binding — so the second plugin to load would redeclare the class and take the site down. Wrapped the class inside the conditional so OPcache actually respects it.
v1.0.2
Released 3 March 2026 — GitHub Release
Minimum PHP bumped to 8.1, minimum WordPress to 6.4. If you're running older versions — not our problem, but also sort of our problem now. No functional changes.
v1.0.1
Released 3 March 2026 — GitHub Release
Version bump. First release under the monorepo slash-tag convention. Nothing changed except the number, which felt important enough to tag anyway.
v1.0.0
Released 2 March 2026 — GitHub Release
The initial release. A complete membership system built on top of FluentCart — plans, content protection, drip scheduling, trials, emails, and more third-party integrations than your theme has shortcodes. Built for stores that want to sell access, not just products.
Plans and access control
Create membership plans with durations (lifetime, fixed days, or mirrors a subscription), optional trial periods, and grace periods for when payments inevitably get complicated. Plans support hierarchy — higher tiers can include everything lower tiers get without duplicating rules. Duplicate plans, schedule activation dates, and configure exactly what happens when a membership ends.
| Feature | What you get |
|---|---|
| Plan durations | Lifetime, fixed days, subscription mirror |
| Trial periods | Configurable per plan, auto-converts or auto-expires |
| Grace periods | For cancellations and failed renewals |
| Plan hierarchy | Higher tiers can include lower tier content |
| Custom restrictions | Per-plan messages and redirect URLs |
Content protection
Lock down content without writing a line of PHP. Protect posts, pages, custom post types, categories, tags, and custom taxonomies. Hide nav menu items from non-members. Block URL patterns with exact, prefix, or regex matching. Add teasers — excerpt, first N words, custom text — so visitors know what they're missing.
Archive filtering keeps protected posts out of search results and feeds. The REST API respects protection too, so headless setups don't accidentally leak members-only content.
Drip content
Unlock content on a schedule. Either X days after a member joins, or on a fixed calendar date. Members get email notifications when content unlocks. Admin views include a drip timeline and calendar so you can see what unlocks when without squinting at a spreadsheet.
Trials
Configurable trial periods per plan. When a payment is confirmed, the trial converts automatically. When it isn't, it expires automatically. Either way, the cron job handles it — you don't have to.
Email notifications
Eight transactional emails covering the full membership lifecycle: access granted, access revoked, access expiring, drip content unlocked, membership paused, membership resumed, trial converted, trial expiring. HTML templates with smart codes, async delivery, and individual enable/disable toggles per type.
Integrations
| Integration | What it does |
|---|---|
| FluentCart | Triggered by order events — grants and revokes access, mirrors subscription state, auto-creates WP users |
| FluentCRM | 15 automation triggers, 7 actions, 7 benchmarks, 25+ smart codes, contact profile section, segment filters |
| FluentCommunity | Auto-adds/removes members from spaces and groups, badge assignment and revocation |
| LearnDash | Course and lesson access through plan rules |
Analytics
Overview dashboard with active member count, churn rate, new and churned members this month. Charts for member growth over 30 days, 6 months, and 12 months. Plan distribution, churn analysis, revenue by plan, and content popularity. Numbers aggregate daily — no live query hammering your database.
Frontend
Four shortcodes and two Gutenberg blocks for displaying membership status, restricting inline content, showing drip progress, and listing a user's active memberships. A member account page with content library and access history. CSS included, fully overridable.
Developer tools
Full REST API for plans, members, content rules, drip, reports, settings, and import. WP-CLI commands for everything from bulk grants to debug output. Five webhook events with HMAC-SHA256 signatures and async delivery. CSV import with parsers for generic and PMPro formats.
The WP-CLI backfill and sync commands are useful when migrating from another membership plugin. The import tool handles generic CSV and PMPro exports.