What you’ll build
By the end of this tutorial you will have a working MAM Suite sibling plugin that does the one thing every sibling plugin does: it hooks into mam-main’s phone-data pipeline and injects a key into the JSON payload that mobile apps consume. You’ll write about 60 lines of PHP, activate the plugin, and then watch your new key — my_first_key — appear live in the Previewer.
This is the smallest possible “real” extension. It teaches the pattern that every no-code feature plugin in the Suite (mam-contact-form, mam-special-offers, mam-geodirectory, mam-chat-manager) is built on:
- A plugin class that gates on entitlement before registering anything.
- A single
add_filter()againstmam_get_phone_data_before_send. - A
manage_phone_data( $data_array )method that merges one key and returns the array.
Once you’ve done it once, every other sibling plugin is a variation on this shape.
Why this matters. mam-main is the kernel of the Suite. It owns the mobile-API surface — a single AJAX endpoint (
local_app_get_phone_data) that returns a JSON payload describing every screen, button, and content element in the app. mam-main never imports a sibling plugin’s classes. Sibling plugins extend it exclusively through filters and actions. This tutorial walks you through the canonical extension surface end to end.
Prerequisites
Before you start you need:
- A MAM Suite development site — WordPress 6.3+, PHP 8.1+, with
mam-maininstalled and activated. If mam-main isn’t active, your plugin’s entitlement guard will (correctly) make it exit silently and nothing will happen. - Write access to
wp-content/plugins/— you’ll be creating a new plugin folder. - An entitlement for your new plugin slug. mam-main gates every sibling plugin through the
mam_plugin_entitlementfilter. On a development site you’ll need the slug you choose below to resolve to a non-blockedentitlement state. If you’re unsure how entitlements are provisioned on your dev site, check with the platform team before continuing — without an entitlement, the plugin loads but does nothing. - The Previewer available on your site — the in-browser app preview (
mam-tsl-previewer). This is what lets you see the phone-data JSON render without building a native binary. - Basic WordPress plugin familiarity — you should know what a plugin header is and where plugins live.
You do not need: a native build toolchain, an iOS/Android device, or any knowledge of the Suite’s other plugins.
Step 1 — Scaffold the plugin folder
Create a new folder in wp-content/plugins/. Pick a slug that’s unique across the Suite — for this tutorial we’ll use mam-first-plugin.
wp-content/plugins/mam-first-plugin/
└── mam-first-plugin.php
Open mam-first-plugin.php and add the standard MAM Suite plugin header. Every sibling plugin carries both a Version and a Beta-Version line (the Suite’s release tooling promotes the beta to the stable version on release day):
<?php
/*
Plugin Name: Mobile App Manager First Plugin
Description: My first MAM Suite sibling plugin — injects one key into the phone-data JSON.
Version: 26.22.0
Beta-Version: 26.22.0
Author: Tiny Screen Labs, LLC
License: TSL Standard
Requires at least: 6.3
Requires PHP: 8.1
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
The ABSPATH guard is standard WordPress hygiene — it stops the file from being executed directly.
Step 2 — Add the plugin class and the entitlement gate
Sibling plugins bootstrap on plugins_loaded so that mam-main is fully loaded first. The class constructor does one thing before anything else: it checks entitlement and bails if the plugin isn’t licensed.
Add this below the header:
// Bootstrap after all plugins are loaded so mam-main is available.
add_action( 'plugins_loaded', array( 'mam_first_plugin_manager', 'init' ) );
class mam_first_plugin_manager {
/** Plugin slug identifier — must match the entitlement record. */
public $plugin_slug = 'mam-first-plugin';
/** Static callback used on plugins_loaded. */
public static function init() {
$class = __CLASS__;
new $class();
}
public function __construct() {
// 1. mam-main must be present and have registered the entitlement filter.
if ( ! has_filter( 'mam_plugin_entitlement' ) ) {
return;
}
if ( empty( $this->plugin_slug ) ) {
return;
}
// 2. Resolve this plugin's entitlement.
$entitlement = apply_filters( 'mam_plugin_entitlement', null, $this->plugin_slug );
if ( empty( $entitlement ) || 'blocked' === $entitlement['state'] ) {
return;
}
// 3. Register for update info unless the entitlement is expired.
if ( 'expired' !== $entitlement['state'] ) {
do_action( 'mam_set_plugin_update_info', basename( __DIR__ ), __FILE__ );
}
// 4. Now — and only now — register hooks. (Step 3.)
}
}
What’s happening here, in order:
has_filter( 'mam_plugin_entitlement' )— if mam-main isn’t active, this filter was never registered, so the constructor returns immediately and the plugin does nothing. This is the “silently exit” behavior you’ll see described across the Suite’s plugin docs.apply_filters( 'mam_plugin_entitlement', null, $this->plugin_slug )— asks mam-main for this plugin’s entitlement. The result is an array with astatekey. Ablocked(or empty) state means stop here.mam_set_plugin_update_info— registers the plugin with the update manager so it can receive updates, as long as the entitlement hasn’t expired.- Everything else — every
add_filter/add_action— goes after this gate. Nothing the plugin does should be reachable by an unentitled site.
The gate is the contract. This three-check pattern (
has_filter→ resolve entitlement → bail onblocked/empty) is copied verbatim across sibling plugins. Don’t register hooks before it, and don’t skip it — it’s what keeps unlicensed installs inert.
Step 3 — Inject one key into the phone-data JSON
Now the payoff. mam-main builds the mobile JSON in a four-phase pipeline (phase_auth → phase_settings → phase_content → phase_finalize). During the content phase it fires the filter that ~70 sibling plugins subscribe to:
mam_get_phone_data_before_send
Each subscriber receives the in-progress payload as a plain array, merges its own keys, and returns it. That’s the entire content. Add the hook registration at the bottom of your constructor (where Step 2 left the // 4. comment):
// mam_get_phone_data_before_send — inject our key into the mobile JSON payload.
add_filter( 'mam_get_phone_data_before_send', array( $this, 'manage_phone_data' ) );
Then add the method to the class:
/**
* Inject a single key into the mobile JSON payload.
*
* @param array $data_array The in-progress phone-data payload.
* @return array The payload with our key added.
*/
public function manage_phone_data( $data_array ) {
$data_array['my_first_key'] = 'Hello from mam-first-plugin';
return $data_array;
}
Three rules govern this method, and they’re the same three every sibling plugin follows:
- Take the array, merge your keys, return the array. Never replace
$data_arraywholesale — you’d wipe out every other plugin’s contribution. - Always
return $data_array;. Forgetting the return strips the payload silently. This is the single most common mistake. - Keep it fast. This filter fires on every app data fetch. Don’t call external APIs or run heavy queries here without caching — production sites hit this hundreds of times.
For comparison, several shipping sibling plugins follow exactly this shape: they query their own custom post type, build an array of items, and set one or two keys on $data_array before returning. Yours just sets one string key — but it’s the same pattern.
A note on priority
add_filter( 'mam_get_phone_data_before_send', ... ) with no priority argument runs at the default priority 10, which is the “default cohort” most feature plugins use. You only need to think about priority if your data depends on another plugin’s keys already being present, or if you’re intentionally overriding something:
- 1 — base settings (runs first)
- 10 — default cohort (most plugins; use this)
- 99–100 — late enrichments that read earlier keys
- 1000 — mam-main injects
home_cats; must run after everyone below it - 1001+ / 9999 — use-case-specific total overrides
For your first key, the default 10 is correct. Leave the priority argument off.
Step 4 — Activate and verify in the Previewer
- Go to Plugins in wp-admin and activate “Mobile App Manager First Plugin.”
- Confirm there’s no fatal error and the plugin shows as active. If it activates but you suspect the entitlement gate stopped it, double-check that your
$plugin_slugmatches the entitlement record from your prerequisites. - Open the Previewer for your site. The Previewer renders the live phone-data payload — the same JSON the mobile apps receive — in your browser.
- Trigger a fresh app data load in the Previewer (reload / refresh the preview so the phone-data pipeline runs again).
Now confirm your key is present. The most direct check is the raw payload: the Previewer (or the local_app_get_phone_data AJAX response) should now contain
"my_first_key": "Hello from mam-first-plugin"
at the top level of the payload.
Verifying without the Previewer. If the Previewer isn’t set up yet, you can hit the same endpoint directly. The phone-data payload is produced by the
local_app_get_phone_dataAJAX action (also reachable via the publicmam_get_phone_data()function). Loading that response and searching it formy_first_keyconfirms the injection independently of any UI.
If the key is there — congratulations, you’ve shipped a sibling plugin. The entitlement gate let it run, the filter let it contribute, and the pipeline carried your key all the way to the app payload.
If your key isn’t showing up
- Plugin not active, or gate bailed. Confirm activation, then confirm the entitlement state isn’t
blocked/empty for your slug. - Forgot to return
$data_array. Re-check Step 3 — a missingreturnproduces an empty or broken payload, not just a missing key. - Stale cache. The phone-data pipeline can serve a cached payload (the cursor cache). Force a fresh load in the Previewer, or disable caching for the test if your dev site supports it.
- Hook registered before the gate. Make sure your
add_filteris inside the constructor, after the entitlement checks — not at file scope.
What’s next
You’ve built the spine of a sibling plugin. From here, the Suite’s other extension surfaces are the same idea — register against a mam-main filter, do one job, return:
- Add a notification type — hook
mam_notification_listto register an email/SMS/push notification with templates. - Add a tab-bar button — hook
mam_tab_manager(per-button enrichment) to surface your feature in the app’s navigation. - Add a content-type handler — for a full custom screen, add a content class and register it, then inject its button via
mam_get_phone_data_before_send. - Read app settings — use the
mam_app_settings_get_settingfilter to make your plugin configurable from App Setup instead of hard-coding values like the'Hello…'string above.
For a worked example of a sibling plugin that uses several of these surfaces together, read the mam-contact-form plugin reference and its hook articles — same entitlement gate, same manage_phone_data shape, just more of it.
