feat(membership): Add reactivation flow for cancelled memberships#1
feat(membership): Add reactivation flow for cancelled memberships#1kenedytorcatt wants to merge 2 commits intomainfrom
Conversation
When a membership is cancelled, the user has no way to renew. The blocked page says "log in" but logging in creates a redirect loop and there is no checkout for reactivation. Changes: - Add reactivate() method and is_cancelled() helper to Membership - Add cart_type 'reactivation' that skips site/template steps - Improve lock_site() with checkout URL and "Renew" button - Add cancelled membership detection on wp_login → redirect to checkout - Skip site creation steps in checkout for reactivation carts - Add 6 new hooks for extensibility All changes are additive. No breaking changes. No DB changes. Tested locally with Playwright (Pierina user, membership Ultimate-Multisite#256 cancelled). Screenshots in docs/reactivation-flow/.
Reactivation carts were inheriting the 15-day free trial from the plan, charging $0 today instead of the full amount. This broke the reactivation flow because a cancelled user was getting a second free trial. The fix returns null from get_billing_start_date() when cart_type is 'reactivation'. This cascades through has_trial() -> WC gateway -> $days_to_start = 0, which prevents the WU WooCommerce addon from adding \_subscription_trial_length to the auto-created WC product. WC Subscriptions then renders the subscription without a trial banner. No addon changes needed. No WooCommerce changes needed. The addon already reads $cart->has_trial() from the core, so flipping the core decision propagates correctly down the stack. Bumped plugin version to 2.4.13-beta.21.
|
@davidcrga heads up — this PR is now live in production on kursopro.com running as What just happened
What you need to doJust review the 5 modified core files in this PR and, if the approach looks good to you, merge it into upstream. Then release as Key files to focus on:
All changes are additive and behind We're running our fork in production while we wait for your review. Once you accept and release upstream, we'll align back. Zero urgency on your side — everything is working for our users right now. Take your time to review. Just needed to get this live because we had cancelled users piling up with no way to self-serve reactivation. Thanks David 🙏 |
…ate-Multisite#669) * t508: test(admin): write unit tests for System_Info_Admin_Page * test(t508): address CodeRabbit review suggestions on System_Info_Admin_Page_Test - Issue #1 (Minor): snapshot HTTP_USER_AGENT in setUp and restore in tearDown instead of unconditionally unsetting it, preventing global state mutation - Issue Ultimate-Multisite#2 (Minor): wrap active_sitewide_plugins tests in try/finally to restore the original option value even when assertions fail - Issue Ultimate-Multisite#3 (Major): store filter callback references and use remove_filter() in try/finally blocks instead of remove_all_filters(), ensuring exception-safe cleanup that only removes the test's own callback - Issue Ultimate-Multisite#4 (Minor): replace hardcoded English strings ('seconds', 'Yes', 'No') with __() translation calls matching the production code, making assertions locale-independent
|
Superseded by Ultimate-Multisite#750 — the PR has been retargeted to upstream as David requested, rebased cleanly on current |
Critical Gap: No Reactivation Flow for Cancelled Memberships
Status: Fully validated in production (Pierina membership Ultimate-Multisite#256, plan KURSO INICIAL, 2026-04-04)
The Problem
When a user's membership is cancelled, their site is locked via
lock_site(). The blocked page says "log in" but:cart_typesupportsnew,retry,upgrade,downgradebut NOTreactivationreactivate()method on Membership model (onlyrenew()andcancel())check_pending_payments()ignores cancelled memberships — only checksstatus=pendinglock_site()showswp_die()with no actionable path — no checkout link, no renew buttonRedirect loop reproduced with real cancelled user (Pierina, membership Ultimate-Multisite#256):
Impact: Every user who cancels is PERMANENTLY locked out. Zero self-service recovery. Admin must manually reactivate. Lost revenue from users who want to resubscribe.
The Solution
Core changes (this PR): Add
reactivate()method,reactivationcart type, improvedlock_site(), cancelled membership detection on login, and trial skip for reactivation carts.Companion mu-plugins (KP side): A dedicated
/renovar/page with a virtual checkout form, site-suspended page with branded "Renew" button, and checkout CSS/JS fixes for the grid layout and notice deduplication.New flow:
What Changed in Core (5 files)
inc/models/class-membership.phpreactivate()+is_cancelled()inc/checkout/class-cart.phpcart_type = 'reactivation'+ trial skipinc/managers/class-site-manager.phplock_site()with checkout URL + HTML templatewp_die()was a dead end with no path to paymentinc/managers/class-payment-manager.phpwp_logininc/checkout/class-checkout.phpreactivationThe Trial Skip Fix (latest commit — critical)
Problem chain we traced end-to-end:
/renovar/→ click "Pagar y reactivar"kp-reactivation-checkoutmu-plugin POSTs to WU WC Gatewaycart_type='reactivation'(this PR, line 810 inclass-cart.php)$cart->has_trial()→ which internally callsget_billing_start_date()get_billing_start_date()was iterating all products and returning the smallest trial → reactivation carts inherited the 15-day trial from the planhas_trial() === true→ computed$days_to_start = 15ultimate-multisite-woocommerce/inc/gateways/class-woocommerce-gateway.php:736the addon added_subscription_trial_length = 15to the auto-created WC productTotal a Pagar Hoy: $0Fix (4 lines in
get_billing_start_date()):Why this is enough — no addon changes needed:
ultimate-multisite-woocommerce) reads$cart->has_trial()from corehas_trial()depends onget_billing_start_date()null→has_trial() = false→ addon skips the trial meta → WC Subscriptions shows no trialNew Hooks (6)
wu_membership_pre_reactivatewu_membership_post_reactivatewu_blocked_site_reactivation_urlwu_blocked_site_templatewu_cancelled_membership_redirect_urlwu_reactivation_skip_site_stepBackward Compatibility
cart_type='reactivation'branches use the new logic)cart_type === 'reactivation'— normal subscriptions still get their trialVersion Bump
Plugin version bumped to
2.4.13-beta.21(from2.4.13-beta.1).End-to-End Test Results (Playwright headless, 2026-04-04)
Real test user:
pierinaferreiro(membership Ultimate-Multisite#256, plan KURSO INICIAL $69/mes, status cancelled)/renovar/(no more loop)/renovar/page loads with correct plan + price + status/renovar/pagar/creates WC cart withcart_type='reactivation'reactivacion20(recurring_percent, 1 cycle): $69 → $55 first month, $69/mes from second monthDeployment Status
kursopro.comproduction ✅kenedytorcatt/ultimate-multisite@feature/reactivation-flow, running in production ✅For David — What You Need to Do
class-cart.php::get_billing_start_date()— this is the critical fix2.4.13-beta.21(or whatever version scheme you prefer)All of this is already live in production on kursopro.com and working correctly. We're running our fork while waiting for the upstream merge. Once you accept, we'll align back to upstream.
No action needed on the WooCommerce addon. No action needed on any database migration. Just merge and release.