Recipe: Build a Login-Required App

The problem

Some apps have no public content at all — every screen belongs to signed-in
members. A private household app, a staff-only tool, a members club. Building
one of these is not “build a normal app and require login”: a logged-out
device still receives a phone payload, still renders a home screen, and still
executes every content class. If you only build the member experience, a
logged-out user opens the app to an empty shell with no way to sign in,
and — worse — any content class that doesn’t check identity happily ships the
members’ private data to the logged-out payload.

A login-required app is a two-cohort app. Design both cohorts explicitly.

The two cohorts

Cohort Role the button reader sees What they should get
Not logged in '' → falls back to the mam-all row set A front door: a Login button, plus at most public teaser content
Signed-in member Their managed WP role (e.g. subscriber) The real app

Two facts drive everything below:

  1. Button sets do not inherit across roles. A signed-in subscriber reads
    ONLY the subscriber rows; a logged-out device reads ONLY the mam-all
    rows. Configuring one cohort does nothing for the other. (See Hook:
    mam_app_settings_get_buttons
    .)
  2. Content classes run for every cohort. The payload builder does not know
    your app is private. Any get_data_for_app() that doesn’t check
    mam_user_id() serves its rows to logged-out devices too.

Step 1 — Make the member role a managed app role

App Setup → User Roles: enable managing for the member role (this stores
mam-user-roles-to-manage-<role> = yes).

This is not optional, and failing it looks like a different bug entirely: the
role-check that runs during payload builds deletes the app token of any user
whose role is not managed
— users appear to be randomly logged out moments
after signing in. If a login-required app is “forgetting” its users, check
this setting first.

Also confirm App Setup’s new user role setting points at the same role, so
in-app signups land in the managed cohort.

Step 2 — Configure the login settings

Two settings groups govern the login experience, and a login-required app
must take a position on every one of them:

On the Login button (Login Settings tab):

Setting Key Login-required guidance
Require Login? tsl-require_login yes — the app routes logged-out users straight to the sign-in screen
Allow user to sign up with Email tsl-sign_up_with_email Deliberate choice. Open signup means any stranger with the app becomes a member and receives everything the member role can see. Private/invite-only apps: no. Community apps: yes.
Allow user to sign up with Apple and Google tsl-sign_up_with_google Same reasoning as email signup — it creates accounts, not just sign-ins.
Require email validation during account setup tsl-sign_up_with_email_validation yes whenever signup is open.

In General Settings:

Setting Key Guidance
New User Role tsl-setting-new_user_role Set it to the member role from Step 1. The runtime falls back to subscriber when unset, but set it explicitly so the stored config says what actually happens — and remember the role it names must itself be managed (Step 1) or its users’ tokens get wiped.

A sibling plugin that must not have its posture relaxed by an App Settings
edit can hard-set the flags in the payload instead (they are ordinary flat
keys): $data_array['require_login'] = 'yes'; etc. in
mam_get_phone_data_before_send. Use with care: the override wins
silently — the App Settings toggles still render and still save, they just
stop doing anything, which presents as “the Google/Apple buttons disappeared
and no setting brings them back.” Hard-set only what must never change, and
leave the rest to the admin toggles.

Step 3 — Give the logged-out cohort a Login button

The stock Login content class (type key Login, vc_type: login_button)
is the front door. In App Setup, with the role selector on the logged-out /
mam-all set, add a button of content type Login Button to the main grid
and/or the left menu. Left menu placement is the house convention for apps
that also show public content; for a fully private app, put it on the main
grid — it is effectively the whole logged-out home screen.

A sibling plugin that wants this guaranteed (no admin setup, survives an
App Settings wipe) self-heals it, healing each cohort differently:

add_filter( 'mam_app_settings_get_buttons', function ( $buttons, $role, $location ) {
    if ( 'main' !== $location ) {
        return $buttons;
    }
    if ( ! is_array( $buttons ) ) {
        $buttons = array();
    }

    $logged_out = ( '' === (string) $role || 'mam-all' === (string) $role );

    // Logged out: the front door. Signed in: the real app.
    $required = $logged_out
        ? array( 'Login' => 'Sign In' )
        : array( 'my_members_list' => 'Members', 'my_other_screen' => 'More' );

    $present = array();
    foreach ( $buttons as $button ) {
        if ( ! empty( $button['type'] ) ) {
            $present[ $button['type'] ] = true;
        }
    }
    $added = false;
    foreach ( $required as $type => $name ) {
        if ( isset( $present[ $type ] ) ) {
            continue;
        }
        $buttons[] = array( 'id' => $type . '-auto', 'name' => $name, 'icon' => '', 'type' => $type, 'source' => '' );
        $added     = true;
    }
    return $added ? array_values( $buttons ) : $buttons;
}, 20, 3 );

Step 4 — Auth-gate every private content class

The Login button controls what the logged-out home shows, not what the
payload contains. Every content class whose rows are member-private must
refuse to build them for an unresolved user:

public function get_data_for_app( $source = null, $unused = false, $title = '' ) {
    if ( (int) mam_user_id() <= 0 ) {
        return array();   // logged-out payload carries nothing private
    }
    // ... build rows ...
}

Do the same for any homescreen_stack elements, custom sections, or settings
keys that reference private data. The test is simple and worth running: fetch
the payload with a random (never-logged-in) pid and grep it for member
content. If anything private appears, a stranger with the app binary can read
it — no login screen will stop them, because the leak is in the JSON, not the
UI.

Write subactions authorize separately (mam_user_id() + capability checks in
each handler) — that part is the standard AJAX contract and is unchanged here.

Step 5 — Verify both cohorts

  1. Logged out: payload with an unknown pid → Login button present,
    private content absent, private lists empty.
  2. Signed in: payload with a member’s pid → member buttons present with
    data; sign-out returns the device to cohort 1.

Checklist

  • [ ] Member role managed (mam-user-roles-to-manage-<role> = yes) — else tokens get wiped
  • [ ] New user role setting = the member role
  • [ ] Logged-out (mam-all) set has a Login button (admin-placed or self-healed)
  • [ ] Member role set has the real buttons (remember: no inheritance between role sets)
  • [ ] Every private content class returns array() when mam_user_id() <= 0
  • [ ] Anonymous-pid payload grep shows zero private content

Metadata

Field Value
Article type Recipe
Plugin slug mam-main
Audience App builder / PHP developer
Related Content class: Login · Hook: mam_app_settings_get_buttons · Concepts: login and authentication
Was this article helpful?
Contents

    Need Support?

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