A button type appears in the builder but renders nothing on device

Goal

You have a button type that shows up correctly in the Mobile App Manager builder — admins can add it to a tab bar or left menu, configure it, and save it — but on a real device the button either renders blank, shows nothing, or silently falls back to placeholder content. This article walks you through diagnosing the most common cause: a gap in the duck-typed content-class interface, where the class either omits get_data_for_app() or returns a payload the device-side JSON pipeline can’t use.

This is a developer task. It assumes you can read PHP and reach the mam-main source on the affected site.


Background: why the builder and the device disagree

Every button a customer can place in the no-code builder is backed by a content class — one file in mam-main/includes/content-classes/. There are roughly 21 of them today (Login, Map, Web URL, Phone Call, WP Post Category, and so on). Each file does two things:

  1. Registers itself in the global $local_app_content[] array with content_type, class, and vc_type metadata.
  2. Defines a PHP class that implements a duck-typed interface — a set of methods that are expected by convention, not enforced by a formal interface declaration.

The builder UI and the device payload are produced by different methods on that class:

  • The builder uses app_settings_categories(), app_settings(), get_blank_content_html(), and get_content_type_form(). If those work, the button appears and is configurable in the admin.
  • The device uses get_data_for_app($source, ...). This is the method that produces the mobile-app payload — the data the app actually receives.

Because there is no formal interface enforcing all methods, a class can ship with a perfectly working admin side and a broken or missing device side. It will “break only at the moment the method is called” — which is during JSON assembly for the phone, not during admin save. That is exactly the failure you’re chasing.


Prerequisites

  • Filesystem or repo access to mam-main on the affected install.
  • The content_type label of the offending button as it appears in the builder (for example, “Web URL”, “Phone Call”). You’ll use this to find the registration.
  • Ability to read the JSON the device receives — either via the app-connect endpoint response, or by logging inside the build pipeline. (You do not need a physical device; the payload is the source of truth.)

Steps

1. Map the builder label to its content class

Open mam-main/includes/content-classes/ and find the file whose registration matches the builder label. The registration block at the top of each file looks like this:

$local_app_content['Web_URL'] = array(
    'content_type' => 'Web URL',   // user-visible label in the builder
    'class'        => 'local_app_web_url',
    'vc_type'      => 'url',
);

The content_type value is the label admins see. The class value is the PHP class that must implement the interface. The vc_type value matters more than it looks — keep it in mind for Step 4.

Confirm the file is actually loaded: it must be in the require_once chain in content-class-manager.php. A file that exists on disk but was never added to that loader will not register at all (and usually the button wouldn’t appear in the builder either — but a partial merge can leave you in a confusing in-between state).

2. Check that get_data_for_app() exists on the class

In the class body, confirm there is a method named exactly:

public function get_data_for_app( $source ) {
    // ...
}

If it is missing entirely, you have found the problem. Per the content-class contract: a content class that omits get_data_for_app will appear in the admin UI but render nothing on the device. Because the interface is not enforced, nothing warns you at save time — the omission only surfaces here.

If the method exists, continue — the shape of what it returns is the next suspect.

3. Trace how the pipeline calls the method

The device payload is assembled in mam-main/includes/app-connect/main-json-manager.php, in mam_build_nav_item(). The relevant call is branched on vc_type, and this is where a wrong shape silently fails:

$content_obj = new $content_class_name();

if ( 'html' === $this_button['vc_type'] ) {
    // HTML buttons: the method receives the button array and must RETURN the whole button
    $this_button = $content_obj->get_data_for_app( $value['source'], $this_button );
} else {
    // Everything else: the method's return value is placed under ['data']
    $this_button['data'] = $content_obj->get_data_for_app( $value['source'], false, $title );
    if ( 'list' === $this_button['vc_type'] ) {
        $this_button['use_subcategories'] = 'yes';
    }
}

Two things to notice:

  • For vc_type === 'html', the return value replaces the entire button array. If your get_data_for_app() returns just a fragment (or null), you wipe out title, icon, and everything else. The button effectively disappears.
  • For every other vc_type, the return value is dropped into ['data']. If it returns nothing useful, the next block kicks in.

Right after that call, the pipeline has a fallback:

if ( ! $this_button['data'] ) {
    if ( 'list' === $this_button['vc_type'] || 'offDirectory' === $this_button['vc_type'] ) {
        $this_button['data']   = array( 'nodata' => 1 );
        $this_button['nodata'] = array( 'nodata' => 1 );
    } else {
        $this_button['data'] = ' ';
    }
}

This is why the symptom is usually “renders nothing” rather than a crash. An empty or falsy return gets papered over with a nodata marker or a single space — so the app receives a structurally valid but empty button.

4. Verify the method’s signature matches the branch it lands in

The signature your class needs depends on which branch in Step 3 it falls into, and that is decided by the vc_type you registered in Step 1:

  • vc_type is html — the method is called as get_data_for_app( $source, $this_button ). Your method must accept that second argument, enrich it, and return the full button array. The simple per-button classes (Web URL, Phone Call) instead use get_data_for_app( $source ) and are not HTML-typed — copying one of their signatures into an HTML-typed registration is a classic mismatch.
  • Any other vc_type — the method is called as get_data_for_app( $source, false, $title ). Your return value becomes the data payload. Returning a scalar where the app expects an array (or vice versa) is the typical wrong-shape failure here.

So: line up the vc_type in the registration against the method signature and return type. A mismatch between the two is the single most common cause once get_data_for_app() is present but the button still renders blank.

5. Inspect the actual payload

Capture the JSON the device receives for this button and look for the tells:

  • "data": " " (a single space) — your method returned something falsy and hit the non-list fallback.
  • "data": {"nodata": 1} or a sibling "nodata" key — falsy return on a list/offDirectory button.
  • Missing title/icon on an HTML button — your html-typed method returned a fragment instead of the whole button array.

Any of these confirms the gap is in get_data_for_app(), not in the builder, the icon, or the placement.

6. Fix the class and re-verify

  • If the method was missing, add it with the signature that matches your vc_type (Step 4), and return a payload shaped like a working sibling class of the same vc_type.
  • If the method returned the wrong shape, correct the return to match the branch. For HTML buttons, return the mutated $this_button; for data buttons, return the array the app expects under data.
  • Re-pull the device payload and confirm the nodata / single-space tells are gone and real content is present.

Note the frozen-name constraint while you’re in here: do not rename the content class if customers may have it saved in serialized button arrays (local-app-button-array). Renaming, say, local_app_login_button orphans every site’s saved button of that type. Fix the method; leave the class name alone unless you ship a migration.


If it still renders nothing

If get_data_for_app() exists, has the right signature for its vc_type, and returns a real array — but the button is still blank — widen the search beyond the content class:

  • Registration not loaded. Confirm the file is in the require_once chain in content-class-manager.php, and that any code reading $local_app_content runs after that manager has loaded (it’s a global populated at include time).
  • vc_type confusion. vc_type is a Visual Composer leftover that is mostly inert, but downstream code — including the branching above — still reads it. A button registered with an unexpected vc_type can route into the wrong branch.
  • Enrichment hook. Per-button data is further massaged by subscribers to mam_tab_manager (dozens of subscribers across the suite). A subscriber stripping or overwriting your data would produce the same empty-button symptom from outside the class.

  • content-classes — Class Interface (Duck-Typed) — the full list of the seven expected methods and what each returns. Source of record: mam-main/includes/content-classes/README.md.
  • Adding a new content class — the four-step recipe (create file, add to loader, register in $local_app_content, implement the interface) the README documents.
  • App-connect JSON pipeline — how mam_build_nav_item() assembles the per-button payload the device consumes.

What’s next

Once you’ve confirmed the fix in the payload, add a regression check: this class of bug is invisible until get_data_for_app() is actually called during a build, so the safest guard is a test that assembles the JSON for a button of this type and asserts data is neither the single-space fallback nor a nodata marker. A formal interface declaration for content classes is a known refactor target that would catch the missing-method variant at load time.

Was this article helpful?
Contents

    Need Support?

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