Goal
Add a custom action button to a detail-screen tab bar in your MAM app — the row of icons that appears under a listing, an event, a reservation, or any other “detail” content — and make it appear only for the right users.
The classic example: an Edit Reservation button on a reservation detail page that opens a pre-populated Gravity Forms form, but only for the reservation’s owner. Everyone else sees the detail screen without it.
This is a three-part job, and the parts hand off to different people:
- You (developer) register the button definition so it becomes selectable in the admin UI.
- An admin turns it on for a tab-bar slot.
- You (developer) contribute the button’s runtime payload — icon, action, target, pre-fill values — gated by role.
Mam-main coordinates the handoff but does not gate anything by role. Authorization is your code’s responsibility.
Prerequisites
- A working MAM Suite install with
mam-mainactive, and a use-case plugin of your own (or a content class) where this code can live. - A detail screen already configured for the content type whose tab bar you want to extend.
- Comfort with WordPress filters and the MAM content-class pattern.
- If your button opens a form: Gravity Forms and
mam-gravity-forms-manageractive, and the target form built. - Adding a brand-new action type (something other than the actions the client already understands) requires the app team — the action token is interpreted by the mobile client, so an unknown string is a no-op. Reuse an existing action where you can.
| Field | Value |
|---|---|
| Applies to | mam-main 2.1.11+ |
| Reference implementation | mam-geodirectory (includes/mam_gd_tab_bar_buttons.php) |
| Estimated time | 1–2 hours |
Steps
1. Choose a slug and use it everywhere
The slug is the button id, and it round-trips through three places that must match exactly:
- the registered definition’s
id - the dynamic filter name
mam_main_add_tab_bar_item_{slug} - the
idon the array your filter returns (mam-main preserves it for you)
Convention is snake_case prefixed by your plugin or content type: edit_reservation, share_listing, edit_listing. A typo in any one of the three produces a silent “missing tabbar” debug entry and no button — there is no error, just an absent icon.
2. Register the button definition
Registering the definition is what makes the button selectable in the admin UI. There are two ways to do it; pick one.
Option A — via your content class. If your plugin already has a content class, implement tab_bar_manager_buttons( array $buttons ): array and append your entry. Mam-main calls this method when it builds the admin’s button list.
public function tab_bar_manager_buttons( $buttons ) {
$buttons[] = array(
'id' => 'edit_reservation',
'title' => 'Edit Reservation',
'type' => 'form',
);
return $buttons;
}
Option B — via the global filter. If you don’t have a content class (or want the button available regardless of content type), hook mam_app_settings_get_tab_bar_buttons:
add_filter( 'mam_app_settings_get_tab_bar_buttons', function ( $buttons ) {
$buttons[] = array(
'id' => 'edit_reservation',
'title' => 'Edit Reservation',
'type' => 'form',
);
return $buttons;
} );
Either way, the entry needs at least id, title, and type. After this, Edit Reservation appears as a selectable button in the admin tab-bar configuration.
3. Have an admin enable the button
The runtime filter in step 4 only fires when an admin has switched the button on. Until then, mam-main short-circuits and your callback never runs.
In Mobile App Manager → Navigation → [tab] → Tab Bar Buttons (the per-content-type tab-bar admin):
- Pick Edit Reservation for one of the tab-bar slots.
- Check Show Button so the setting’s
visibleflag becomes'on'.
If your callback never seems to run in QA, this checkbox is the first thing to verify.
4. Contribute the runtime payload
When the app loads a detail item, mam-main walks each enabled tab-bar setting and, if a filter exists for the slug, fires mam_main_add_tab_bar_item_{slug}. Your callback receives a partially built button and returns the finished one — or an empty array to hide it for this item / this user.
add_filter(
'mam_main_add_tab_bar_item_edit_reservation',
array( $this, 'edit_reservation_button' ),
10,
3
);
public function edit_reservation_button( $tabbar_button, $tab_key, $data_array ) {
// 1. Authorize — only the reservation owner gets this button.
$owner_id = (int) get_post_field( 'post_author', $data_array['id'] );
if ( $owner_id !== mam_user_id() ) {
return array(); // hide for everyone else
}
// 2. Resolve the target form.
$form_id = get_option( 'mam_my_plugin_edit_reservation_form_id' );
$form_index = apply_filters( 'mam_gf_get_form_from_cache', 0, $form_id );
if ( $form_index <= 0 ) {
return array(); // no form configured — hide
}
// 3. Build pre-population values from the current item.
$values = array(
'reservation_id' => $data_array['id'],
'reservation_date' => get_post_meta( $data_array['id'], 'booking_date', true ),
'reservation_time' => get_post_meta( $data_array['id'], 'booking_start_time', true ),
);
// 4. Populate and return the button.
$tabbar_button['icon'] = 'black_edit';
$tabbar_button['action'] = 'open_form';
$tabbar_button['source'] = $form_index;
$tabbar_button['values'] = $values;
return $tabbar_button;
}
Almost every tab-bar callback follows the same four moves — authorize, resolve the target, build the payload, return — and if any of the first three fails, it returns [].
The fields you set on the returned button:
| Key | Required | Description |
|---|---|---|
id |
yes (preserved) | The slug; mam-main keeps it from the input. |
title |
yes (preserved) | The admin-configured display title. |
icon |
yes | Icon asset name resolved against the app’s bundled set, e.g. black_edit, black_share, black_chat. |
action |
yes | What the app does on tap. See the action table below. |
source |
varies | Action-specific payload. For open_form, the form index; for share_listing, the post id. |
values |
optional | Pre-fill values passed to the opened form. |
Common actions you can reuse without app-team involvement:
| Action | source contains |
Used for |
|---|---|---|
open_form |
Form index (int) | Open one Gravity Forms form, pre-filled via values. |
open_form_array |
Array of form definitions | Open a list of forms (e.g. one per existing staff member, plus an “Add”). |
share_listing |
Post id | Trigger the native share sheet. |
open_phone |
Phone number | Tap-to-call. |
open_webbrowser |
URL | Open in webview / browser. |
5. (Optional) Hide a button you don’t own
If your goal is to remove a button registered by another plugin in certain contexts — say, hide the generic Share button from staff because they have an Edit button instead — don’t reach into an add filter you don’t own. Use the sibling filter, which runs before the per-slug dispatch:
add_filter( 'mam_main_skip_tab_bar_button', function ( $tab_bar, $data_array ) {
if ( ( $tab_bar['id'] ?? '' ) !== 'share_listing' ) {
return $tab_bar; // not our concern — keep
}
if ( $this->current_user_is_staff_for( $data_array['id'] ?? 0 ) ) {
return array(); // hide for staff
}
return $tab_bar;
}, 10, 2 );
Return $tab_bar to keep the button or [] to hide it — never false or null. Skip and add are independent gates; both can hide.
6. Verify
- The button appears as a selectable entry in the admin’s tab-bar configuration.
- With Show Button checked, the runtime filter fires (confirm with a debug log line).
- For users who shouldn’t see it, the button is absent (your filter returned
[]). - For users who should, the button renders with the configured icon and performs its action — and for a form button, opens pre-populated.
Gotchas
- The slug must round-trip exactly. The registered
id, the{slug}in the filter name, and the returnedidare the same string. A typo anywhere = silent “missing tabbar” debug entry, no button. - The filter only fires when the admin enabled the button. No Show Button, no
visible === 'on', no callback. Check this before debugging your PHP. - Return
[]to hide, not a half-filled array. Mam-main treats any non-empty array as “render this.” A button with an emptysourcewill render and then break in the app. - This is a hot path. Your callback runs once per item per app load — 50 items, 50 invocations. Cache role/context lookups on
$this; avoid HTTP calls and unbatched queries. - Authorization is your job. Mam-main does not gate by role; it just dispatches. The owner/manager/staff check lives in your callback.
$data_arrayis per-item, not the whole list. Don’t try to batch inside the filter; pre-compute anything shared (form indexes, package meta) in your constructor or a memoized helper.
Related
- Hook:
mam_main_add_tab_bar_item_{slug}— the runtime button filter in detail. - Hook:
mam_main_skip_tab_bar_button— hide a registered button per item / per user. - Hook:
mam_app_settings_get_tab_bar_buttons— register a button definition globally. - Reference:
mam_gd_tab_bar_buttonsclass — the canonical implementation covering nine distinct tab-bar items. - Recipe: Configure the tab bar
What’s next
Once your button works for one role, extend the authorization gate to the other roles your use case needs (manager, staff, admin), and consider whether any other plugin’s button should be suppressed when yours is present — that’s the mam_main_skip_tab_bar_button filter from step 5.
