From b35253a91e50f3e4a25e42fa693e44c112a64ef6 Mon Sep 17 00:00:00 2001 From: David Stone Date: Mon, 27 Apr 2026 19:47:53 -0600 Subject: [PATCH 1/2] fix: block duplicate signup when customer has an active membership When a logged-in customer with an active subscription goes through the registration form as a new checkout, the gateway replaces the old subscription. Any coupons or custom pricing on the original subscription are lost because the new cart has no discount applied. Add a guard in process_order() that blocks 'new' checkouts when the logged-in user already has an active, trialing, or on-hold membership. The check fires before any customer, membership, or payment records are created so there are no orphaned records when the checkout is blocked. Filterable via wu_allow_duplicate_signup for sites that intentionally permit re-registration (e.g. multi-membership setups). --- inc/checkout/class-checkout.php | 72 +++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/inc/checkout/class-checkout.php b/inc/checkout/class-checkout.php index a78bc019..75cbc9e6 100644 --- a/inc/checkout/class-checkout.php +++ b/inc/checkout/class-checkout.php @@ -729,6 +729,78 @@ public function process_order() { */ $this->type = $cart->get_cart_type(); + /* + * Block duplicate signup for logged-in users with active memberships. + * + * When a customer with an active subscription goes through the + * registration form again as a "new" checkout (rather than + * upgrade/downgrade), the new subscription replaces the old one + * and any applied coupons or custom pricing are lost. + * + * This guard fires early — before any customer, membership, or + * payment records are created — so there are no orphaned records + * to clean up when the checkout is blocked. + * + * Filterable via `wu_allow_duplicate_signup` for sites that + * intentionally permit re-registration (e.g. multi-membership). + * + * @since 2.5.1 + */ + if ('new' === $this->type && is_user_logged_in()) { + $existing_customer = wu_get_current_customer(); + + if ($existing_customer) { + $active_memberships = wu_get_memberships([ + 'customer_id' => $existing_customer->get_id(), + 'status__in' => [ + Membership_Status::ACTIVE, + Membership_Status::TRIALING, + Membership_Status::ON_HOLD, + ], + 'number' => 1, + ]); + + if ( ! empty($active_memberships)) { + $existing_membership = reset($active_memberships); + + /** + * Filters whether to allow a duplicate signup when the + * customer already has an active membership. + * + * Return `true` to allow the checkout to proceed. The + * default is `false` (block the signup). + * + * @since 2.5.1 + * + * @param bool $allow Whether to allow the duplicate signup. + * @param \WP_Ultimo\Models\Membership $membership The existing active membership. + * @param \WP_Ultimo\Models\Customer $customer The current customer. + * @param \WP_Ultimo\Checkout\Cart $cart The cart being processed. + */ + $allow = apply_filters( + 'wu_allow_duplicate_signup', + false, + $existing_membership, + $existing_customer, + $cart + ); + + if ( ! $allow) { + $account_url = get_admin_url(wu_get_main_site_id(), 'admin.php?page=account'); + + return new \WP_Error( + 'duplicate_signup', + sprintf( + /* translators: %s is a link to the account page. */ + __('You already have an active subscription. To manage your existing subscription, visit your account page. If you need help, please contact support.', 'ultimate-multisite'), + esc_url($account_url) + ) + ); + } + } + } + } + /* * Gets the gateway object we want to use. * From ad000055ca9935d4ac6397c2e5c7056b52b332f8 Mon Sep 17 00:00:00 2001 From: David Stone Date: Tue, 28 Apr 2026 19:45:42 -0600 Subject: [PATCH 2/2] fix: use customer subsite for account URL in duplicate signup guard (#968) The duplicate signup guard was building the account page URL with wu_get_main_site_id(), which forces the main network domain. When domain mapping is active, this sends the customer to the wrong domain and can break auth cookies. Use the customer's own subsite (from the existing membership's sites) instead, matching the existing pattern in class-checkout-element.php:442. Falls back to main site only when the membership has no published sites yet. --- inc/checkout/class-checkout.php | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/inc/checkout/class-checkout.php b/inc/checkout/class-checkout.php index 75cbc9e6..ce3970e4 100644 --- a/inc/checkout/class-checkout.php +++ b/inc/checkout/class-checkout.php @@ -786,7 +786,19 @@ public function process_order() { ); if ( ! $allow) { - $account_url = get_admin_url(wu_get_main_site_id(), 'admin.php?page=account'); + /* + * Build the account URL on the customer's own subsite + * rather than the main network site. The "account" admin + * page lives in each subsite's wp-admin. Using + * wu_get_main_site_id() would force the main-site domain + * which breaks when domain mapping is active. + * + * Falls back to the main site only if the membership has + * no published sites yet (pending site scenario). + */ + $membership_sites = $existing_membership->get_sites(); + $account_blog_id = ! empty($membership_sites) ? $membership_sites[0]->get_id() : wu_get_main_site_id(); + $account_url = get_admin_url($account_blog_id, 'admin.php?page=account'); return new \WP_Error( 'duplicate_signup',