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:
- Button sets do not inherit across roles. A signed-in
subscriberreads
ONLY thesubscriberrows; a logged-out device reads ONLY themam-all
rows. Configuring one cohort does nothing for the other. (See Hook:
mam_app_settings_get_buttons.) - Content classes run for every cohort. The payload builder does not know
your app is private. Anyget_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
- Logged out: payload with an unknown
pid→ Login button present,
private content absent, private lists empty. - 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()whenmam_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 |
