Signature
apply_filters( 'mam_iap_require_iap', string $has_iap );
| Parameter | Type | Description |
|---|---|---|
$has_iap |
string | The plugin’s resolved value, either 'yes' or 'no'. Reflects the Require IAP after login? general setting (tsl-setting-iap_show_on_login). |
Returns: string — 'yes' or 'no'. You must return one of these two values.
Purpose
Provides a last-word override on whether the mobile app’s paywall is active for the current request. The plugin applies this filter twice in sequence on the way out — first to compute inapp_has_iap (does IAP exist at all?) and again to compute inapp_is_required (is purchase required to proceed?). Both keys are sent to the mobile client.
Common reasons to subscribe:
- Bypass the paywall for a specific user based on the
iap_not_requireduser meta (see Recipe: Bypass IAP for a specific user) - Bypass the paywall for a role (
editor,subscriber, custom MAM roles) - Disable the paywall during a promotional window (e.g., free week)
- Force-enable the paywall for QA testing on a sandbox build that would otherwise have
iap_off_in_sandboxset - Honor entitlements from a non-WooCommerce source (B2B license server, comp list, partner integrations)
When it runs
In mam_in_app_purchase_phone_manager::manage_phone_data() (includes/phone-manager.php), after the iap_co_packages array is built but before the admin-exemption short-circuit:
$data_array['inapp_has_iap'] = apply_filters( 'mam_iap_require_iap', $data_array['inapp_has_iap'] );
$data_array['inapp_is_required'] = apply_filters( 'mam_iap_require_iap', $data_array['inapp_has_iap'] );
Notice the filter is applied twice, with the already-filtered value of inapp_has_iap passed back in as the input for inapp_is_required. If you set inapp_has_iap to 'no', the second invocation receives 'no' as input — your callback is responsible for the same logic on both keys, or for distinguishing the two cases on its own.
The plugin does not pass any extra context (like which key is being decided). Both invocations look identical from inside the callback.
Default behavior
There is no default callback. The unsubscribed value is whatever the plugin already computed:
inapp_has_iap→'yes'iftsl-setting-iap_show_on_loginis'yes', otherwise'no'inapp_is_required→ starts as the post-filterinapp_has_iap, then gets filtered again
Administrators are exempted after this filter, in the next block of manage_phone_data():
if ( $mam_user && in_array( 'administrator', $mam_user->roles ) ) {
$data_array['inapp_is_required'] = 'no';
$data_array['inapp_has_iap'] = 'no';
}
This means a mam_iap_require_iap callback that returns 'yes' cannot force the paywall on for an administrator — admins are unconditionally exempt.
Examples
Honor the per-user iap_not_required flag
This is the canonical subscriber: read the user meta written by Recipe: Bypass IAP for a specific user and force 'no' when set.
add_filter( 'mam_iap_require_iap', 'my_app_honor_iap_exemption' );
function my_app_honor_iap_exemption( $has_iap ) {
do_action( 'mam_update_current_user' );
global $mam_user_id;
if ( $mam_user_id > 0 && get_user_meta( $mam_user_id, 'iap_not_required', true ) === 'yes' ) {
return 'no';
}
return $has_iap;
}
Exempt an entire role
add_filter( 'mam_iap_require_iap', 'my_app_exempt_editors' );
function my_app_exempt_editors( $has_iap ) {
do_action( 'mam_update_current_user' );
global $mam_user;
if ( $mam_user && in_array( 'editor', (array) $mam_user->roles, true ) ) {
return 'no';
}
return $has_iap;
}
Time-windowed promo (free week)
add_filter( 'mam_iap_require_iap', 'my_app_free_week' );
function my_app_free_week( $has_iap ) {
$start = strtotime( '2026-06-01 00:00:00' );
$end = strtotime( '2026-06-08 00:00:00' );
$now = time();
if ( $now >= $start && $now < $end ) {
return 'no';
}
return $has_iap;
}
Diverge inapp_has_iap and inapp_is_required
The filter receives no signal about which of the two keys it’s deciding. To make inapp_has_iap = 'yes' (so the app shows the upgrade UI) while inapp_is_required = 'no' (so content remains accessible), the cleanest approach is to gate on the input value itself, since the second invocation receives the post-filter inapp_has_iap:
add_filter( 'mam_iap_require_iap', 'my_app_show_but_dont_require' );
function my_app_show_but_dont_require( $value ) {
// Both invocations come through here. On the second one, $value already
// reflects the post-filter inapp_has_iap. We can't reliably distinguish
// them from inside the callback, so the simplest correct shape is:
// - Always allow access (return 'no' for inapp_is_required).
// - Leave inapp_has_iap untouched (return 'yes' if input is 'yes').
// Because the same callback runs both times, the only way to give them
// different answers is to inspect global state set up between the two
// apply_filters calls — which there is none of. In practice, you have
// to choose one strategy for both keys, or split the filter via priority
// and a request-scoped flag (see Gotchas).
return $value;
}
This is a known limitation of the hook’s design — see Gotchas.
Gotchas
- The filter runs twice in a row, with no context distinguishing the two keys. A naive callback that returns
'no'to bypass the paywall will set bothinapp_has_iapandinapp_is_requiredto'no', which is usually what you want. But if you want to set them differently, you can’t tell them apart from inside the callback. Workarounds: (a) accept that they’ll be the same; (b) use a request-scoped static counter inside the callback to know which invocation is which (fragile); (c) overrideinapp_is_requiredin a separate filter further down the pipeline if your stack provides one. - Admins are exempted after this filter. Returning
'yes'for an administrator user has no effect on the final payload — the admin-exemption block at the end ofmanage_phone_data()overrides both keys to'no'. Don’t rely on this filter to test the paywall as an admin; create a non-admin test user. - Always return a string. Returning
null,true,false, or omitting the return strips the chain. The mobile keys are typed as string'yes'/'no'— anything else can break the app’s deserialization. - No early exit. Returning
'no'does not skip later subscribers in the chain. Another callback at higher priority can flip your'no'back to'yes'. If your override needs to win, use a high priority (e.g.,999) and accept that it locks out everyone after you. mam_update_current_useris your responsibility. The plugin does call it earlier inmanage_phone_data(), so$mam_userand$mam_user_idare populated globally by the time this filter runs. But if you’re consulting them from a callback that might run in another context (a CLI script, a unit test), trigger the action yourself first.- This hook does not affect
iap_co_packages. Returning'no'hides the paywall but does not remove the product list from the payload. The list is still sent and a deployed app may still cache it. To suppress the products themselves, you’d need to filter the payload elsewhere.
Verification
This article was last verified against:
- Plugin:
mam-inapp-purchase-managerv2.0 - Source:
includes/phone-manager.php(manage_phone_data— the twoapply_filterscalls and the admin-exemption block)
Re-verify whenever the filter is applied a different number of times, the admin-exemption block is moved or removed, the inapp_has_iap / inapp_is_required keys are renamed, or context (a second filter argument identifying the key) is added.
Related articles
- Plugin overview: mam-inapp-purchase-manager
- Recipe: Configure IAP settings — sets the upstream
tsl-setting-iap_show_on_loginvalue - Recipe: Bypass IAP for a specific user — admin-side equivalent that depends on this filter
- Hook: mam_iap_active_subscriber_product
- Hook: mam_iap_purchase
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 |
