Hook: mam_iap_active_subscriber_product

Signature

apply_filters( 'mam_iap_active_subscriber_product', string $default_product, int $user_id = 0 );
Parameter Type Description
$default_product string The product SKU (or other token) to return if no active subscription is found. Pass-through value the caller can fall back to.
$user_id int Optional user ID. Note: the plugin’s own callback ignores this argument and reads the global $mam_user_id instead.

Returns: string — the SKU of the user’s active subscription product, or $default_product if none is active.


Purpose

Answers “is this user actively subscribed, and to which product?” The plugin’s default callback walks the user’s WooCommerce orders looking for one with a future expiration_date meta and returns the SKU of the product on that order.

Subscribe to this filter to override the resolution — for example, to honor entitlements from a non-WooCommerce source (a SaaS license server, a CRM, a staff comp list), or to map several SKUs to a single canonical “tier” identifier the rest of your code can switch on.


When it runs

Wherever any plugin or theme code applies the filter. Within MAM Suite, the typical caller is mam-main (or a sibling plugin) deciding whether to gate a feature. The plugin itself does not apply this filter — it only registers a callback against it.

The plugin’s callback runs at priority 10 with 2 accepted args.


Default behavior

mam_in_app_purchase_manager::mam_iap_active_subscriber_product() does the following:

  1. Reads $mam_user_id from the global scope.
  2. Calls wc_get_orders( [ 'customer_id' => $mam_user_id, 'status' => 'any' ] ).
  3. Walks each order, reading the expiration_date post meta.
  4. On the first order whose expiration_date is in the future, returns the SKU of the first item’s product.
  5. If no qualifying order is found, returns $default_product unchanged.

Implications:

  • A user with multiple overlapping subscriptions returns whichever order WooCommerce hands back first. Order is not guaranteed.
  • Cancelled or refunded orders are still considered if their expiration_date is in the future, because the query uses status: 'any'.
  • A user with a wc-completed order but no expiration_date meta (e.g., a One Time purchase, or a duration string the plugin’s switch didn’t recognize — see Hook: mam_iap_purchase) is treated as not subscribed.

Example: bundle a SaaS license with IAP

You sell a non-IAP B2B license that should also unlock the app. Add a callback that returns the license SKU when a user has an active license, and otherwise yields to the default chain:

add_filter( 'mam_iap_active_subscriber_product', 'my_app_b2b_license', 5, 2 );

function my_app_b2b_license( $default_product, $user_id = 0 ) {
    $user_id = $user_id ?: ( $GLOBALS['mam_user_id'] ?? 0 );
    if ( $user_id <= 0 ) {
        return $default_product;
    }

    $license_expires = (int) get_user_meta( $user_id, 'b2b_license_expires', true );
    if ( $license_expires > time() ) {
        return 'b2b_premium';
    }

    return $default_product;
}

Register at priority 5 so your check runs before the plugin’s WooCommerce-order walk. Returning anything other than $default_product short-circuits — the plugin’s callback at priority 10 overwrites your value if you yield.

To run after the plugin and only override its $default_product fallback, register at priority > 10:

add_filter( 'mam_iap_active_subscriber_product', 'my_app_fallback_to_b2b', 20, 2 );

function my_app_fallback_to_b2b( $resolved, $user_id = 0 ) {
    if ( $resolved === '' || $resolved === null ) {
        // The plugin found no WooCommerce subscription. Try the B2B path.
        return ( my_app_user_has_b2b_license( $user_id ) ) ? 'b2b_premium' : $resolved;
    }
    return $resolved;
}

Example: collapse multiple SKUs to a single tier

If the rest of your code only cares whether a user has any premium access, normalize:

add_filter( 'mam_iap_active_subscriber_product', 'my_app_normalize_premium', 20 );

function my_app_normalize_premium( $sku ) {
    $premium_skus = [ 'premium_monthly', 'premium_yearly', 'premium_lifetime' ];
    return in_array( $sku, $premium_skus, true ) ? 'premium' : $sku;
}

Run at priority > 10 so this sees the plugin’s resolved value.


Gotchas

  • The $user_id arg is decorative on the default callback. The plugin reads $mam_user_id from globals regardless of what’s passed. If your callsite needs to ask about a specific other user, you’ll need to set $mam_user_id yourself before applying the filter, or write your own callback at higher priority.
  • status: 'any' includes refunded and cancelled orders. If expiration_date meta is in the future on a refunded order, the plugin will still return its SKU. If you refund subscriptions in WooCommerce, also clear or back-date the expiration_date meta — or filter out non-wc-completed orders in your own callback.
  • Order traversal stops at the first match. The first order in the list that has a future expiration_date wins, regardless of which one is “newest” or “longest-running.” If a user has overlapping subs, you may not get the answer you’d guess.
  • Empty defaults. If the caller passes '' (or null), the plugin’s callback returns the same empty value when no subscription is found. Make sure your downstream code handles “no subscription” as an empty string, not as a missing key.
  • Always return a string. Returning null, false, or omitting the return statement strips the chain. The default plugin callback assumes a string-typed contract.

Verification

This article was last verified against:

  • Plugin: mam-inapp-purchase-manager v2.0
  • Source: mam-inapp-purchase-manager.php (mam_iap_active_subscriber_product)
  • Order meta keys: expiration_date, iap_expiration_date, iap_product

Re-verify whenever the order query in mam_iap_active_subscriber_product() changes status filters, the expiration_date meta key is renamed, the early-exit-on-first-match behavior changes, or the default callback starts honoring the $user_id argument.


  • Plugin overview: mam-inapp-purchase-manager
  • Recipe: Create an IAP product
  • Hook: mam_iap_purchase — sets the expiration_date meta this hook reads
  • Hook: mam_iap_require_iap — overrides the on/off paywall flag, which is a different decision

Metadata

Field Value
Article type Hook Reference
Plugin slug mam-inapp-purchase-manager
Applies to plugin version 2.0+
Category Extending MAM Suite
Hook type filter
Audience PHP developer
Last verified 2026-05-01
Contents

    Need Support?

    Can’t find the answer you’re looking for? Don’t worry we’re here to help!