Changelog
Every release, every fix, every time a version bump felt suspiciously significant.
What changed, when, and why you should (probably) care.
August 2026
v1.4.6 Latest
Released 13 August 2026 — GitHub Release
Two screens spent this release admitting they had been showing you database internals and calling it information.
A membership is one thing, not six rows
The member profile listed grant rows. A plan writes one row per access rule, so a single membership could appear as six cards, each with its own Pause button — and pausing one of them left the other five wide open. The profile now groups by plan, agreeing with the member list instead of contradicting it, and the rules survive inside the card as "what this plan unlocks", which is what they always were.
Pause and resume act on the whole membership. POST /admin/members/pause and
/resume keep their request shape and still accept a grant_id, but resolve it to
its membership first. Half-paused access is no longer a reachable state.
The header answers the question people actually arrive with — does this person have access, and is it going to renew — instead of offering three counters and letting you do the arithmetic. Grant history and activity merged into one timeline, which now includes revocations and pauses; they were previously read from fields that never existed, so they had never appeared at all.
Things that were quietly broken
Every membership date picker emits YYYY-MM-DD. The REST layer has only ever
accepted Y-m-d H:i:s. Extending, bulk-extending and granting with an expiry all
failed with "Invalid parameter: expires_at" the moment anyone touched the calendar.
A chosen day now becomes the last second of that day, so the member keeps all of it.
Access that had not started yet was filed under Ended, labelled Active in its row, and findable by no filter — the row status was an alphabetical minimum over a column, which is a fun way to decide anything. It is now called Scheduled, in the summary, the filter and the row alike.
Extending is written to the audit log, and +1 month / +1 year are measured from
the current expiry, so extending an unexpired membership can no longer shorten it.
Bulk export writes one row per membership rather than repeating a member once per
protected resource.
Finding the content you want to protect
Three screens asked which piece of content? with three separate implementations, and none of them could reliably say what you had picked.
The plan editor stored the id and threw the title away, then read the rule back to
you as Post #1234. Selecting a resource now records its name alongside its id,
always — searching again, removing an earlier rule, or reopening a saved plan can
no longer turn your choice back into a number.
The profile's own "Can they open it?" search was worse: it read two fields the
listing endpoint has never sent, so every result was named post #55. Its test
fixture had invented one of those fields, which is the tidy explanation for why a
green suite sat on top of a search that had never once displayed a title. Picking
an item and searching again also dropped the option behind the selection, leaving
the Check access button to do nothing at all. Both are fixed, and the panel now
lists protected content when you open it rather than waiting for two characters it
never asked for out loud.
Content search also only ever looked at published posts, so a draft, a scheduled post or a private page could not be protected — you cannot pick what the search refuses to admit exists. All five editable statuses are searchable, and each result carries its type and, when it is not simply published, its status. There is no character minimum any more: the plan editor wanted one, the protect wizard silently wanted two, and neither mentioned it. Search runs from the first keystroke, an empty box means "recent", and opening the dropdown shows a cached list rather than a spinner it does not need.
v1.4.5
Released 9 August 2026 — GitHub Release
A follow-up to 1.4.4, and an awkward one.
Updates work again
The plugin was prepared for a WordPress.org listing that has not happened yet, and the GitHub updater was removed ahead of it. That left a gap nobody wants to be in: not on WordPress.org, so no update channel there — and no self-updater either, so no update channel at all. Anyone who installed it stayed on the version they installed.
The updater is back, and it stays until the directory actually lists the plugin.
The include is guarded, so a package built without it means no automatic updates
rather than a fatal on activation, and the whole decision now hangs on one flag in
wporg/plugins.json that the builder and the release gates both read.
v1.4.4
Released 9 August 2026 — GitHub Release
The welcome email now names the things it welcomes you to. Previously it announced "you have immediate access to the following resources:" and then showed a single empty bullet, which is technically a list.
The Access Granted email lists actual resources
Reported by @bergy10 in #73, with the root cause already diagnosed — the grant path
handed the email its raw plan_rules database rows, while the email expected
{title, url} pairs. A rule row carries provider, resource_type and
resource_id and no title at all, so every resource resolved to an empty
anchor pointing at #.
A new PlanRulePresenter now sits between the two. Posts and custom post types
resolve to their title and permalink, taxonomy rules to the term name and its
archive, and wildcard rules to the same "All Categories" wording the plan editor
already uses. Rules that are not addressable content — deleted posts, drafts,
URL patterns, comment rules, CRM segmentation — are dropped rather than
rendered as a link to nowhere.
The drip list was broken in exactly the same way
Nobody reported this one, because everybody had already stopped reading the
email. Drip items expected an available_date that rule rows have never
carried, so "Coming Soon" listed a row of bare dashes. Delayed rules now read
"7 days after joining" and fixed-date rules use the site's own date format.
Sections that have nothing to say now say nothing
When no resource survives resolution, the list, the "Coming Soon" heading and
the lead-in sentence all disappear instead of framing an empty space. The
lead-in moved into a new {resources_intro} smart code so it can vanish with
the list.
If you have saved a custom Access Granted template, its lead-in sentence is
part of your own template text and will keep appearing above an empty list.
Add {resources_intro} where the sentence used to be, and delete the hardcoded
copy.
v1.4.3
Released 6 August 2026 — GitHub Release
The plan editor now remembers that connected products continue to exist after you open the plan, and can add a whole FluentCommunity Space Group without the traditional ritual of selecting every Space by hand. Object permanence and fewer repetitive clicks: truly lavish.
Add a FluentCommunity Space Group in one click
The plan editor can now add every community Space from a FluentCommunity Space
Group at once. Memberships expands the group into individual fc_space rules,
skips Spaces already present in the plan, and deliberately ignores course
Spaces.
The added rules start with immediate access but remain editable individually, so each Space can still use its own drip schedule. The group is expanded as a snapshot when selected; later changes to the FluentCommunity group do not silently rewrite existing plans. Access control has enough surprises already.
Plan management that tells the truth
- Linked products appear immediately — opening an existing plan now loads its FluentCart connections without requiring a ceremonial trip to another tab and back.
- Duplicate links are obvious before saving — products already connected to the plan are labelled and cannot be selected again.
- Link and unlink actions stay in sync — successful changes refresh the table, while failed requests keep the current data and explain what went wrong instead of inventing an empty state.
- Recovery is built in — linked products and plan members now show a clear retry action when loading fails.
- Delayed access previews behave during setup — new plans open the Drip Preview as soon as a scheduled content rule needs it, without exposing edit-only tools.
- Content rules have room to breathe — controls now keep balanced spacing from every card edge instead of crowding the bottom border like rent is due.
v1.4.2
Released 6 August 2026 — GitHub Release
Plan slugs have learned that the alphabet did not stop at z. A modest
achievement, admittedly, but one that matters when the plan is called
Klub Przyjaciół Psów rather than Gold Membership for the nine-hundredth
time.
International plan slugs
- Polish accents are transliterated properly —
Klub Przyjaciół Psównow becomesklub-przyjaciol-psow, rather than quietly losing half the letters. - WordPress is the authority — the plan editor previews the exact slug the server will save, so Czech, French, Turkish, Vietnamese and non-Latin scripts no longer get a separate JavaScript interpretation of reality.
- Native scripts remain usable — Greek, Cyrillic, Arabic, Hebrew, Hindi, Japanese and Chinese titles retain WordPress's percent-encoded slugs instead of collapsing to an empty field.
- Canonical forms agree — composed and combining-accent forms now produce the same saved identifier.
- Database limits are respected — long encoded slugs and their collision suffixes stay within the 100-byte column without slicing an encoded character in half. The database has suffered enough creative input for one lifetime.
- Empty slugs are rejected — punctuation-only titles and custom slugs fail with a useful validation message instead of persisting an unusable identifier.
July 2026
v1.4.0
Released July 2026 — GitHub Release
Eighteen Memberships commits landed in 72 hours. The result is not a longer feature list. It is a much better place to run the membership business.
The new workspace
The admin experience has been rebuilt around useful decisions:
- a dashboard for active members, upcoming expiries, readiness, provider health, trends, and recent activity;
- consistent navigation and layouts that behave on smaller screens;
- clearer plan, member, content, drip, report, integration, and settings workspaces;
- visible problems with direct routes to the next useful action.
Guided setup
Creating a plan now follows three steps: shape the offer, choose content access, and review the member experience. Content protection gets its own four-step wizard with friendlier browsing and search.
The advanced controls still exist. They simply stopped demanding attendance before you can protect one page.
Better member care
Member search, access summaries, bulk actions, profiles, timelines, activity, imports, exports, and Community context all received a serious usability pass. The workspace now helps answer who has access, why they have it, and what needs attention.
Notification Studio
Membership emails now have a visual editor, shared brand styling, realistic previews, test sends, and reusable member data. Each lifecycle moment can use a built-in email, a FluentCRM automation, or remain off.
Integrations that admit when something went wrong
FluentCRM and FluentCommunity health, pending work, failures, and drift are now visible. Recovery can be previewed and applied without overwriting access that still belongs to another valid source.
Supported FluentCommunity Pro badges, points, and levels appear only when the installed runtime can provide them. Decorative controls have finally been denied a promotion.
Webhooks and API connections
Settings now include practical connection guides, one-time credentials, independent webhook endpoints, required tests before activation, delivery history, automatic retries, manual retry, pause, and cancellation.
Existing integrations should review the new Webhooks & API guide before switching to 1.4.0. It explains the safer credential flow without expecting anyone to infer it from a code sample and a hopeful shrug.
Reliability without the bedtime story
Access ownership, subscription changes, grace periods, drip schedules, provider recovery, content protection, and external connections received broad correctness and recovery improvements. The release is backed by expanded PHP, JavaScript, browser, runtime, documentation, route, workflow, and package checks.
In short: less hunting, fewer silent failures, better member care, and an admin workspace that no longer treats every task like an escape room.
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 my problem, but also sort of my 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.