Author: Predrag

  • How to Redirect Unauthorized Users to a Custom Page for Protected File Downloads

    Summary

    When unauthorized users attempt to download a protected file in MemberPress, they are redirected to the login page by default. This behavior differs from how MemberPress handles protected pages and posts, where unauthorized content can be replaced with a custom message inline.

    This happens because file access is handled at the server level rather than through WordPress page rendering. MemberPress cannot override the file itself with a custom message, so a redirect is the only way to block access. If the global “Redirect unauthorized visitors to a specific URL” option is not enabled under MemberPress > Settings > Pages tab, the redirect defaults to the login page.

    This document explains the difference between page/post and file protection, and provides a custom code solution to redirect unauthorized file download attempts to a custom page without affecting the global unauthorized redirect setting.

    Troubleshooting

    Understanding Page/Post Protection vs. File Protection

    Pages/Posts Protection: MemberPress hooks into the the_content() function that WordPress uses to display page and post content. This allows MemberPress to override the content of the page and display a custom unauthorized message directly to unauthorized users. The message appears inline, and the visitor stays on the page URL without being redirected.

    File Protection: File access is handled separately from page content. Files are served outside of the normal WordPress page rendering process. When someone clicks a protected file link, there is no page template to render a message on. MemberPress blocks access at the file level and redirects the user instead. By default, this redirect goes to the login page.

    Note: This is expected behavior by design. File downloads cannot display inline unauthorized messages because there is no page content to replace — the redirect ensures the file stays secure.

    1) Unauthorized File Download Redirects to Login Page Instead of a Custom Page

    When unauthorized users click a protected file download link, they are redirected to the MemberPress login page. The global unauthorized redirect setting under MemberPress > Settings > Pages tab can change this behavior, but enabling it affects all protected content sitewide — not just file downloads.

    If you want to redirect only file download attempts to a custom page while leaving other protection rules unaffected, you can use a code snippet that hooks into the mepr_rule_redirect_unauthorized filter.

    How to Test/Fix:

    Add custom code that filters the unauthorized redirect URL specifically for protected file downloads. This can be implemented using either the WPCode plugin or a child theme functions.php file.

    Option 1: Using WPCode Plugin (Recommended)

    1. Navigate to Dashboard > Code Snippets > + Add Snippet.
    2. Click Add Your Custom Code (New Snippet).
    3. Enter a name for the snippet: “Custom Unauthorized File Download Redirect”.
    4. Set Code Type to PHP Snippet.
    5. Paste the code snippet provided below into the code editor.
    6. Set Location to Run Everywhere.
    7. Enable the Active toggle.
    8. Click Save Snippet.

    For more details, refer to our guide: How to Add Custom Code Snippets in WPCode.

    Option 2: Using Child Theme functions.php

    1. Navigate to Dashboard > Appearance > Theme File Editor.
    2. Select your child theme from the dropdown.
    3. Click functions.php in the right sidebar.
    4. Add the code snippet at the end of the file.
    5. Click Update File.

    Important: Always test code modifications on a staging site before implementing on production. Create a complete backup of your site before making changes to theme files.

    Code Snippet:

    /**
     * Redirect unauthorized users to a custom page
     * when they attempt to download a protected file.
     * Replace '/unauthorized/' with the slug of your custom page.
     */
    add_filter('mepr_rule_redirect_unauthorized', function($redirect_to, $uri) {
        return '/unauthorized/'; // Change this to your custom page slug
    }, 10, 2);

    Replace /unauthorized/ with the slug of the page you want unauthorized users to see. You can customize this page with any content you need — a login form, pricing information, registration links, or a custom message.

    2) Verifying the Redirect Works Correctly

    After implementing the code, verify that unauthorized file download attempts are redirected to your custom page.

    How to Test/Fix:

    1. Create a test page with the slug used in the code snippet (e.g., /unauthorized/).
    2. Add content to the page so you can visually confirm the redirect is working.
    3. Ensure you have a protected file set up via a MemberPress rule.
    4. Log out of your WordPress site (or use an incognito/private browser window).
    5. Try to access the protected file download URL directly.
    6. Confirm you are redirected to the custom page instead of the default login page.
    7. Log in as a user with the required membership and confirm the file downloads normally.

    Note: If the global “Redirect unauthorized visitors to a specific URL” option is enabled under MemberPress > Settings > Pages, it may override or conflict with this snippet for non-file content. This snippet is intended for use when the global redirect option is disabled, so that only file download attempts are affected.

    Public Facing Documentation / Additional References

    Public Facing Documentation

    Using the Unauthorized Redirect

    Add Unauthorized Redirection Exclusions

    Protecting Files with MemberPress

    How to Add Custom Code Snippets in WPCode

    Additional References

    WordPress Plugin Hooks: Filters (WordPress Developer Documentation)

    Child Themes (WordPress Developer Documentation)

    Meta Data for Heroic KB

  • How to Resend the Stripe Webhook for Payments in MemberPress

    Summary

    When payments processed through Stripe do not register in MemberPress, the cause is often a webhook delivery failure. In these cases, the payment exists in Stripe but the corresponding transaction is missing from MemberPress.

    This document explains how to resend Stripe payment webhooks from the Stripe Dashboard. Resending a webhook triggers MemberPress to process and record the missing transaction. This process should only be performed after the underlying webhook delivery issue has been resolved.

    Troubleshooting

    Common Causes of Missing Transactions

    Stripe sends webhook notifications to MemberPress when payments succeed. If the webhook cannot reach the site, MemberPress will not record the transaction. Common causes include:

    • The site was temporarily down or unreachable when Stripe sent the webhook;
    • A security plugin, firewall, or server rule blocked the incoming webhook request;
    • The Stripe webhook URL configured in MemberPress was incorrect or changed;
    • A server timeout occurred before MemberPress could finish processing the webhook;
    • Caching or CDN rules interfered with the webhook endpoint.

    Resolve the root cause of the webhook failure before resending any payments. If the original issue persists, resent webhooks will also fail.

    Prerequisites

    Before resending webhooks, confirm the following:

    1. The MemberPress Stripe webhook URL is correctly configured. Navigate to Dashboard > MemberPress > Settings > Payments tab and verify the webhook URL shown under the Stripe gateway settings.
    2. The webhook endpoint is reachable. Check the Stripe Dashboard > Developers > Webhooks section to confirm the endpoint status is active and recent deliveries are succeeding.
    3. The root cause of the original delivery failure has been identified and resolved.

    Resending a Stripe Payment Webhook

    Follow these steps to resend a payment webhook from the Stripe Dashboard to MemberPress.

    Locate the Payment in Stripe

    1. Log in to the Stripe Dashboard.
    2. Navigate to Stripe Dashboard > Payments at https://dashboard.stripe.com/payments.
    3. Locate the payment that needs to be resent.
    4. Click on the payment to open the payment detail view.

    Find the Payment Event

    1. Scroll down to the Events section within the payment detail view.
    2. Locate the event that reads similar to: “The payment pi_3SlD5aH46pO6dg1e1G5VHK3T for $17.00 has succeeded.” The pi_xxxxxxxxxx value and dollar amount will be unique to each transaction.
    3. Click on the payment_intent.succeeded event.

    Resend the Webhook

    1. A window will appear showing the webhooks registered for the event. The MemberPress webhook endpoint will be listed on the right side, identified by the site URL.
    2. Click the Resend button next to the site webhook endpoint.
    3. Stripe will resend the payment notification to MemberPress.
    4. MemberPress will then process and record the transaction.

    Repeat these steps for each payment that needs to be resent. This process must be completed one payment at a time. Stripe does not provide a bulk resend option.

    Verifying the Transaction in MemberPress

    After resending the webhook, confirm that MemberPress has recorded the transaction.

    1. Navigate to Dashboard > MemberPress > Transactions.
    2. Search for the transaction by the member’s name or email address.
    3. Verify that a new transaction entry exists with the correct amount, date, and status.
    4. If the transaction does not appear, check the Stripe Dashboard > Developers > Webhooks section for delivery errors on the resent event.

    Known Limitations

    • Stripe does not provide a way to replay historical events to new webhook endpoints. If the original webhook endpoint has been deleted and a new one created, past events cannot be resent to the new endpoint;
    • Payments can only be resent one at a time. There is no bulk resend feature in the Stripe Dashboard;
    • Resending a webhook for a payment that MemberPress has already recorded may create a duplicate transaction. Always verify that the transaction is missing in MemberPress before resending.

    Do not resend webhooks for payments that are already recorded in MemberPress. Doing so may result in duplicate transactions and incorrect member access records

    Public Facing Documentation / Additional References

    Public Facing Documentation

    Additional References

  • How to Add a vCard QR Code to a MemberPress ClubDirectory™ Profile

    Summary

    This document explains how to add a vCard QR code to a MemberPress ClubDirectory™ member profile page. When scanned, the QR code allows visitors to instantly save the profile member’s contact information — including name, email, phone, website, and address — directly to their device as a vCard contact.

    This solution requires installing the Effortless QR Code Generator plugin and adding a custom PHP shortcode via the WPCode plugin. Once in place, the shortcode is added to the active ClubSuite™ Profile template using a Shortcode block. The QR code automatically renders with the data of whichever member’s profile is currently being viewed.

    This document also covers common troubleshooting scenarios, including mismatched custom field slugs, the GD library requirement for server-side QR generation, and how to extend the shortcode to support multiple phone number fields.

    Troubleshooting

    Initial Setup

    Complete the following steps before adding the shortcode or troubleshooting any issues.

    1) Effortless QR Code Generator Plugin Not Installed

    The custom shortcode depends on the Effortless_QRCode_Native class provided by the Effortless QR Code Generator plugin. The QR code will not render if this plugin is missing or inactive.

    How to Test/Fix:

    1. Navigate to Dashboard > Plugins > Add New Plugin.
    2. Search for “Effortless QR Code Generator”.
    3. Click Install Now, then click Activate.

    No additional plugin configuration is required after activation. The plugin’s PHP API is available immediately for use in custom code.

    2) PHP GD Library Not Available on the Server

    The shortcode uses the server-side PNG generation method (Effortless_QRCode_Native::generate_png). This method requires the PHP GD library to be enabled on the server. If GD is not available, the QR code will not generate and the shortcode will return an empty output.

    How to Test/Fix:

    1. Ask the user to check whether their hosting environment has GD enabled. This is typically found under Dashboard > Tools > Site Health > Info > Server.
    2. If GD is not listed, the user should contact their hosting provider to enable it. GD is included in most managed WordPress hosting environments by default.
    3. As an alternative, direct the user to the client-side rendering mode offered by the Effortless QR Code Generator plugin’s built-in shortcode. Note that this alternative does not support vCard data encoding and is not suitable for this use case.

    Adding the Custom Shortcode via WPCode

    The following steps add the PHP shortcode that generates the vCard QR code. The recommended method is to use the WPCode plugin. Alternatively, the code can be added to the active child theme’s functions.php file.

    1. Navigate to Dashboard > Code Snippets > Add Snippet.
    2. Click Add Your Custom Code (New Snippet).
    3. Enter a descriptive title, such as “MemberPress vCard QR Code Shortcode”.
    4. Set the Code Type to PHP Snippet.
    5. Paste the following code into the code editor:
    /**
     * MemberPress ClubDirectory - vCard QR Code Shortcode
     * Displays a vCard QR code for the currently viewed ClubDirectory profile user.
     * Requires: Effortless QR Code Generator plugin (server-side/GD mode).
     * Usage: [memberpress_vcard_qr]
     * Attributes:
     *   size         - QR code image size in pixels (default: 150)
     *   show_download - Show a download button below the QR code: "true" or "false" (default: "true")
     *   phone_type   - vCard phone type label: CELL, HOME, or WORK (default: CELL)
     */
    add_shortcode('memberpress_vcard_qr', function($atts) {
      $atts = shortcode_atts([
        'size'          => 150,
        'show_download' => 'true',
        'phone_type'    => 'CELL'  // Options: CELL, HOME, WORK
      ], $atts);
    
      $show_download = filter_var($atts['show_download'], FILTER_VALIDATE_BOOLEAN);
      $phone_type    = strtoupper($atts['phone_type']);
    
      // Requires ClubDirectory unhashid utility
      if (!method_exists('memberpress\directory\lib\Utils', 'unhashid')) {
        return '';
      }
    
      $hashed_id = get_query_var('mpdir_user_profile');
      if (empty($hashed_id)) {
        return '';
      }
    
      $user_id = memberpress\directory\lib\Utils::unhashid($hashed_id);
      if (!$user_id) {
        return '';
      }
    
      $user_info = get_userdata($user_id);
      if (!$user_info) {
        return '';
      }
    
      // Retrieve user data
      $first_name = get_user_meta($user_id, 'first_name', true);
      $last_name  = get_user_meta($user_id, 'last_name', true);
      $full_name  = trim($first_name . ' ' . $last_name) ?: $user_info->display_name;
      $email      = $user_info->user_email;
      $website    = $user_info->user_url;
    
      // Phone field - update slug on line below if using a different custom field slug
      $phone      = get_user_meta($user_id, 'mepr_phone_number', true);
      $phone_clean = preg_replace('/[^0-9+]/', '', $phone);
    
      // MemberPress default address fields
      $address1 = get_user_meta($user_id, 'mepr-address-one', true);
      $address2 = get_user_meta($user_id, 'mepr-address-two', true);
      $city     = get_user_meta($user_id, 'mepr-address-city', true);
      $state    = get_user_meta($user_id, 'mepr-address-state', true);
      $zip      = get_user_meta($user_id, 'mepr-address-zip', true);
      $country  = get_user_meta($user_id, 'mepr-address-country', true);
      $street   = trim($address1 . ' ' . $address2);
    
      // Build vCard 3.0 string
      $vcard_data  = "BEGIN:VCARD\n";
      $vcard_data .= "VERSION:3.0\n";
      if (!empty($full_name))                          $vcard_data .= "FN:$full_name\n";
      if (!empty($first_name) || !empty($last_name))   $vcard_data .= "N:$last_name;$first_name;;;\n";
      if (!empty($email))                              $vcard_data .= "EMAIL:$email\n";
      if (!empty($phone_clean))                        $vcard_data .= "TEL;TYPE=$phone_type:$phone_clean\n";
      if (!empty($website))                            $vcard_data .= "URL:$website\n";
      if (!empty($street) || !empty($city) || !empty($state) || !empty($zip)) {
        $vcard_data .= "ADR:;;$street;$city;$state;$zip;$country\n";
      }
      $vcard_data .= "END:VCARD";
    
      // Generate QR code PNG via Effortless QR Code Generator
      if (!class_exists('Effortless_QRCode_Native')) {
        return '';
      }
    
      $result = Effortless_QRCode_Native::generate_png($vcard_data, intval($atts['size']));
      if (!$result || empty($result['url'])) {
        return '';
      }
    
      $filename = sanitize_title($full_name) . '-vcard.png';
    
      $html  = '<div class="memberpress-vcard-qr" style="text-align:center;">';
      $html .= '<img src="' . esc_url($result['url']) . '"'
             . ' alt="vCard QR code for ' . esc_attr($full_name) . '"'
             . ' width="' . intval($atts['size']) . '"'
             . ' height="' . intval($atts['size']) . '"'
             . ' style="max-width:100%; height:auto;">';
    
      if ($show_download) {
        $html .= '<div style="margin-top:12px;">';
        $html .= '<a href="' . esc_url($result['url']) . '"'
               . ' download="' . esc_attr($filename) . '"'
               . ' style="display:inline-block; padding:8px 16px; background-color:#0073aa;'
               . ' color:#ffffff; text-decoration:none; border-radius:4px; font-size:14px;">';
        $html .= 'Download QR Code';
        $html .= '</a>';
        $html .= '</div>';
      }
    
      $html .= '</div>';
      return $html;
    });
    1. Set the Insert Method to Auto Insert and the Location to Run Everywhere.
    2. Toggle the snippet status to Active.
    3. Click Save Snippet.

    Adding the Shortcode to the ClubSuite™ Profile Template

    1. Navigate to Dashboard > ClubSuite™ > Profiles.
    2. Hover over the currently active profile and click Edit.
    3. In the block editor, click the + icon to add a new block in the desired location on the profile layout.
    4. Search for and select the Shortcode block.
    5. Enter [memberpress_vcard_qr] in the shortcode field.
    6. Click Update to save the profile template.

    Once saved, the QR code will appear on each member’s profile page, populated with that member’s data. The result should resemble the following:

    Shortcode Attributes

    The [memberpress_vcard_qr] shortcode supports the following optional attributes:

    Attribute Default Description Example
    size 150 QR code size in pixels [memberpress_vcard_qr size="200"]
    show_download true Show or hide the download button [memberpress_vcard_qr show_download="false"]
    phone_type CELL Phone number type (CELL, HOME, WORK) [memberpress_vcard_qr phone_type="WORK"]
    • size — Sets the QR code image dimensions in pixels. Accepts any integer. Default: 150. Example: [memberpress_vcard_qr size="200"]
    • show_download — Controls whether a download button appears below the QR code. Accepts “true” or “false”. Default: “true”. Example: [memberpress_vcard_qr show_download="false"]
    • phone_type — Sets the vCard phone type label used when the QR code is scanned. Accepts CELLHOME, or WORK. Default: CELL. Example: [memberpress_vcard_qr phone_type="WORK"]

    Common Issues After Setup

    3) Phone Number Field Not Appearing in the QR Code

    The shortcode reads the phone number from a custom field with the meta key mepr_phone_number. This slug is generated automatically by MemberPress based on the field name. If the user created their phone field with a different name, the meta key will differ and the field will return empty.

    How to Test/Fix:

    1. Navigate to Dashboard > MemberPress > Settings > Fields tab.
    2. Locate the phone number field under Custom User Information Fields.
    3. Note the field slug displayed beneath the field name. The meta key used in the database will be “mepr_” followed by this slug (e.g., a slug of phone_number produces the meta key mepr_phone_number).
    4. In the WPCode snippet, locate line 41: $phone = get_user_meta($user_id, 'mepr_phone_number', true);
    5. Replace mepr_phone_number with the correct meta key for the site’s phone field.
    6. Click Save Snippet in WPCode.

    The MemberPress Fields documentation at Dashboard > MemberPress > Settings > Fields tab explains how custom field slugs are generated. The meta key stored in the database is always prefixed with mepr_

    4) QR Code Does Not Appear on the Profile Page

    If the shortcode renders no output, one of several conditions may be preventing the QR code from generating. The shortcode performs several checks and returns empty silently on failure.

    How to Test/Fix:

    1. Confirm the Effortless QR Code Generator plugin is installed and active under Dashboard > Plugins.
    2. Confirm the WPCode snippet is saved with status set to Active.
    3. Confirm the shortcode is being used on an actual ClubDirectory profile page. The shortcode reads the mpdir_user_profile query variable. It will not render on any other page type.
    4. Check that the PHP GD library is available on the server (see Issue 2 above).
    5. If all of the above conditions are met and the QR code still does not render, enable WordPress debug mode temporarily to check for PHP errors related to the snippet.

    5) Supporting Multiple Phone Number Fields in the QR Code

    By default, the shortcode encodes a single phone number. If the site collects separate mobile, work, and home phone numbers using distinct MemberPress custom fields, the vCard can be extended to include all three.

    How to Test/Fix:

    In the WPCode snippet, replace the single phone block (lines 41–43) with the following code. Update the meta key values to match the slugs of the site’s actual custom fields:

    // Multiple phone fields - update meta key slugs to match the site's custom field slugs
    $mobile = get_user_meta($user_id, 'mepr_phone_number', true);   // Update slug if needed
    $work   = get_user_meta($user_id, 'mepr_work_phone', true);     // Update slug if needed
    $home   = get_user_meta($user_id, 'mepr_home_phone', true);     // Update slug if needed
    
    if (!empty($mobile)) $vcard_data .= "TEL;TYPE=CELL:" . preg_replace('/[^0-9+]/', '', $mobile) . "\n";
    if (!empty($work))   $vcard_data .= "TEL;TYPE=WORK:" . preg_replace('/[^0-9+]/', '', $work) . "\n";
    if (!empty($home))   $vcard_data .= "TEL;TYPE=HOME:" . preg_replace('/[^0-9+]/', '', $home) . "\n";

    When using multiple phone fields, remove or comment out the original single $phone and $phone_clean variable declarations and the single TEL line in the vCard build section, to avoid duplicating the phone entry.

    Public Facing Documentation / Additional References

    Public Facing Documentation

    Developer Documentation

  • Enabling Order Bumps for the MemberPress Offline Payment Gateway

    Summary

    By default, the MemberPress Offline payment gateway does not support the Order Bumps add-on or multiple subscriptions. The Order Bumps tab is hidden on any membership that uses only the Offline gateway, and order bump selections made during checkout are silently ignored.

    This article provides a PHP code patch that extends the Offline gateway’s declared capabilities to include order-bumps and multiple-subscriptions, and adds the processing logic required to create a MeprOrder, link all relevant transactions and subscriptions, and handle each order bump product correctly during the offline signup flow.

    This solution is intended for sites that rely on the Offline gateway and need Order Bumps functionality without switching to an online payment processor. The patch is applied as a custom PHP snippet and does not modify any MemberPress core files.

    Troubleshooting

    Order Bumps Not Visible or Not Processing on Offline Gateway Checkouts

    1) Order Bumps Tab Is Hidden for Memberships Using the Offline Gateway

    MemberPress checks each payment gateway’s declared capabilities array before showing the Order Bumps tab in the membership editor. Because MeprArtificialGateway (the Offline gateway class) does not declare order-bumps or multiple-subscriptions in its capabilities, the tab remains hidden and order bumps cannot be configured.

    How to Test/Fix:

    The first function in the patch below hooks into init at priority 5 and appends the missing capabilities to every instance of MeprArtificialGateway found in the site’s configured payment methods. After the patch is active, navigate to Dashboard > MemberPress > Memberships, open any membership, scroll to Membership Options, and confirm the Order Bumps tab is now visible.

    2) Order Bump Selections Are Ignored During Offline Checkout

    Even after the Order Bumps tab becomes visible and bumps are configured, the Offline gateway’s signup routine does not contain the logic to read mepr_order_bumps POST data, create a MeprOrder, or process the additional products. As a result, only the primary membership transaction is recorded and order bump items are never created.

    How to Test/Fix:

    The second function in the patch hooks into mepr_signup at priority 99 and runs after the primary transaction has been stored. It reads the mepr_order_bumps POST array, validates the product IDs via MeprCheckoutCtrl::get_order_bump_products(), creates a MeprOrder to group the primary transaction with the bumps, links any existing subscription to the order, then iterates over each bump product and calls either process_payment() or process_create_subscription() depending on whether the product is a one-time or recurring membership. Errors for individual bump products are logged to the PHP error log and do not interrupt the main checkout.

    3) Order Bump Data Persists in POST and Causes Redirect Errors

    After the offline signup completes and MemberPress redirects the user to the Thank You page, any residual mepr_order_bumps values remaining in the $_POST superglobal can trigger unintended re-processing or errors during redirect handling.

    How to Test/Fix:

    The third function in the patch also hooks into mepr_signup at priority 99 and unsets $_POST['mepr_order_bumps'] after processing completes, ensuring the data does not carry over to subsequent hooks or the redirect routine.

    Adding the Code Patch

    Add all three functions together as a single snippet. The recommended method is to use the WPCode plugin. As an alternative, the code can be added to the functions.php file of a child theme.

     Important: This patch modifies runtime gateway capabilities and signup behavior. Test thoroughly in a staging environment before deploying to production. Always use a child theme if adding code to functions.php to prevent changes from being overwritten by theme updates.

    Adding the Patch via WPCode (Recommended)

    1. Navigate to Dashboard > Code Snippets > Add Snippet.
    2. Hover over “Add Your Custom Code (New Snippet)” and click Use Snippet.
    3. Enter a descriptive title, such as “MemberPress – Offline Gateway Order Bumps Patch”.
    4. Set the code type to PHP Snippet.
    5. Paste the full code below into the code editor.
    6. Under Insertion, set the location to Run Everywhere.
    7. Toggle the snippet to Active and click Save Snippet.

    Adding the Patch via Child Theme functions.php

    1. Connect to the site via FTP or navigate to Dashboard > Appearance > Theme File Editor.
    2. Open the functions.php file of the active child theme.
    3. Paste the full code at the end of the file.
    4. Save the file.

    The Code

    <?php
    /**
     * Enables order bumps for the offline payment gateway.
     */
    function mepr_offline_ob_patch_capabilities() {
      if ( ! class_exists( 'MeprOptions' ) || ! class_exists( 'MeprArtificialGateway' ) ) {
        return;
      }
      $mepr_options = MeprOptions::fetch();
      $pms          = $mepr_options->payment_methods( false );
      foreach ( $pms as $pm ) {
        if ( $pm instanceof MeprArtificialGateway ) {
          if ( ! in_array( 'order-bumps', $pm->capabilities, true ) ) {
            $pm->capabilities[] = 'order-bumps';
          }
          if ( ! in_array( 'multiple-subscriptions', $pm->capabilities, true ) ) {
            $pm->capabilities[] = 'multiple-subscriptions';
          }
        }
      }
    }
    add_action( 'init', 'mepr_offline_ob_patch_capabilities', 5 );
    
    function mepr_offline_ob_process_order_bumps( $main_txn ) {
      // Only act on offline gateway transactions.
      if ( ! class_exists( 'MeprArtificialGateway' ) || ! class_exists( 'MeprCheckoutCtrl' ) ) {
        return;
      }
      $mepr_options = MeprOptions::fetch();
      $pm           = $mepr_options->payment_method( $main_txn->gateway );
      if ( ! ( $pm instanceof MeprArtificialGateway ) ) {
        return;
      }
      // Collect order bump product IDs from the signup form POST data.
      $ob_product_ids = isset( $_POST['mepr_order_bumps'] ) && is_array( $_POST['mepr_order_bumps'] )
        ? array_filter( array_map( 'intval', $_POST['mepr_order_bumps'] ) )
        : [];
      if ( empty( $ob_product_ids ) ) {
        return;
      }
      try {
        $ob_products = MeprCheckoutCtrl::get_order_bump_products( $main_txn->product_id, $ob_product_ids );
      } catch ( Exception $e ) {
        return; // Invalid product — silently bail.
      }
      if ( empty( $ob_products ) ) {
        return;
      }
      // Create the MeprOrder to group main txn + bumps.
      $order                         = new MeprOrder();
      $order->user_id                = $main_txn->user_id;
      $order->primary_transaction_id = $main_txn->id;
      $order->gateway                = $pm->id;
      $order->store();
      // Link the main transaction to the order.
      $main_txn->order_id = $order->id;
      $main_txn->store();
      // Link the main subscription (if any) to the order.
      $main_sub = $main_txn->subscription();
      if ( $main_sub instanceof MeprSubscription ) {
        $main_sub->order_id = $order->id;
        $main_sub->store();
      }
      // Create a transaction (+ subscription) per order bump.
      foreach ( $ob_products as $product ) {
        try {
          list( $ob_txn, $ob_sub ) = MeprCheckoutCtrl::prepare_transaction(
            $product,
            $order->id,
            $main_txn->user_id,
            $pm->id
          );
          if ( $product->is_one_time_payment() ) {
            $pm->process_payment( $ob_txn );
          } else {
            $pm->process_create_subscription( $ob_txn );
          }
        } catch ( Exception $e ) {
          // Log but don't break the main checkout.
          error_log( '[MeprOfflineOrderBumps] Failed to process order bump product #' . $product->ID . ': ' . $e->getMessage() );
        }
      }
    }
    add_action( 'mepr_signup', 'mepr_offline_ob_process_order_bumps', 99 );
    
    function mepr_offline_ob_clear_obs_redirect( $main_txn ) {
      if ( ! class_exists( 'MeprArtificialGateway' ) ) {
        return;
      }
      $mepr_options = MeprOptions::fetch();
      $pm           = $mepr_options->payment_method( $main_txn->gateway );
      if ( $pm instanceof MeprArtificialGateway ) {
        unset( $_POST['mepr_order_bumps'] );
      }
    }
    add_action( 'mepr_signup', 'mepr_offline_ob_clear_obs_redirect', 99 );

    Note: The MeprArtificialGateway class is the internal PHP class MemberPress uses for all Offline gateway instances. The patch targets this class specifically and will not affect any online payment gateways configured on the same site.

    Verifying the Fix

    1. After adding the snippet, navigate to Dashboard > MemberPress > Memberships and open a membership that uses the Offline gateway.
    2. Scroll to Membership Options and confirm the Order Bumps tab is now visible.
    3. Add one or more memberships as order bumps and click Update.
    4. Visit the membership registration page while logged out and complete a test signup using the Offline gateway.
    5. After signup, navigate to Dashboard > MemberPress > Transactions and confirm that separate transactions have been created for the primary membership and each selected order bump.
    6. Confirm all transactions are linked to the same order under Dashboard > MemberPress > Orders.

    Known Limitations

    • This patch does not add required order bump support. Order bumps configured as Required (a feature introduced in Order Bumps 1.0.4) are not enforced at the gateway level for the Offline gateway and should be left as optional when using this patch;
    • Renewal transactions for recurring order bump memberships acquired through this patch must still be created manually, consistent with standard Offline gateway behavior for all recurring subscriptions;
    • MemberPress Coupons applied at checkout will affect the primary membership only. They will not be applied to order bump items, consistent with standard Order Bumps behavior across all gateways;
    • This patch is not compatible with the MemberPress Gifting add-on. Order bumps will not be available on gift membership registration forms;

    Public Facing Documentation / Additional References

    Public Facing Documentation

    Developer Documentation