Why this matters
A single MAM app can show very different things to different people. A member sees a member menu; a guest sees a guest menu; a vendor sees a vendor dashboard. You build these menus once per role in Mobile App Manager, and the app shows each signed-in user the set that matches their WordPress role. There is no per-user configuration — role is the unit of personalization.
Two related mechanisms make this happen, and it pays to keep them separate in your head — they share the word “clone”, which is exactly what trips people up:
- Role resolution — at request time MAM figures out which role the current user has, then serves that role’s buttons. This is automatic and invisible.
- Role cloning (admin preview) — an admin can temporarily “become” another role in their own app session, so they can see exactly what that role’s users see without creating a throwaway test account.
There’s also a third, separately-named feature, cloning role content (copying one role’s button set onto another role as a starting point). It shares the word but is a build-time convenience, not a preview. We’ll cover all three below and flag which is which, since mixing them up is the most common source of “why am I seeing the wrong menu” questions.
How a role becomes a menu
Every time the app asks the website for content, MAM has to answer one question first: who is this, and which menu do they get? It resolves the current user to exactly one role, then pulls the buttons configured for that role.
The rule behind that is deliberately simple. MAM walks the WordPress roles that have been enabled for the app (each enabled role has a setting in the form mam-user-roles-to-manage-{role} set to yes), and resolves the user to an enabled role they actually hold. If the user holds no enabled role — or no one is signed in — MAM falls back to a built-in default role, referred to internally as mam-all. That fallback is what powers the “logged-out” or “everyone” menu.
Once the role is known, the menu itself is a lookup. The left-menu content class asks for that role’s buttons through a filter:
$button_array = apply_filters( 'mam_app_settings_get_buttons', false, $role, 'left' );
That one line is doing more than it looks like. Three things are worth noticing:
- The role is a parameter. The same content class produces a member menu or a guest menu depending only on the resolved role. You are not maintaining separate code paths.
- The location is a parameter (
'left'here). Buttons are stored per role and per location, so the same role can have one set of buttons in the left menu and a different set on, say, a tab bar. - Buttons are stored, not computed. The configured buttons for a
(role, location)pair live in a database table (wp_mam_app_settings_buttons). The filter reads them back. This is why your menu edits in Mobile App Manager are what the app sees — there is no separate “publish” transform of the menu structure itself.
From there, the content class dresses up each stored button with its display defaults (icon, layout, font sizing) and hands the list to the app. The big idea to hold onto: the menu is just data keyed by role and location, and role resolution is nothing more than picking the right key.
Caching, and why preview has to bust it
The app doesn’t rebuild its whole content payload on every interaction — that would be wasteful. Instead, MAM uses a cursor to avoid expensive rebuilds: the server keeps a cursor value that changes when content changes, the app sends back the cursor it last saw, and if the two match, the server short-circuits with a tiny no_new_data response instead of rebuilding everything. (There’s also a per-role disable_caching setting that resets the cursor on every request, used during heavy editing.)
That caching is great for everyday use, but it’s exactly what role cloning has to work around. Picture an admin switching to preview another role while the app still holds a matching cursor — it would happily keep showing the cached payload, built for the admin’s role, not the one being previewed. The preview would quietly do nothing at all.
So preview sidesteps the cursor entirely: the clone request carries no cursor for the server to match against, which forces a full rebuild every time — you’ll see exactly how, next.
Role cloning: previewing the app as another role
Role cloning lets an admin temporarily adopt another user’s identity inside the app session, so the content build runs as if that user were signed in.
Conceptually it works like this:
- The admin picks a user to clone. The admin’s app fires an AJAX request (subaction
admin_clone) naming the target user. This path is admin-only: it requiresmanage_optionsand a valid nonce, so a regular app user cannot impersonate anyone. - MAM records the impersonation as user meta. The target user gets an
admin_is_cloningmeta value pointing back at the admin’s own user ID. Any previous clone target for that admin is cleared first, so an admin can only be cloning one user at a time. - The build runs and is returned immediately. The same handler runs the content build and returns the freshly built JSON inline in the AJAX response. Because the clone request never carries a
cursor, the cursor check has nothing to match and cannot short-circuit tono_new_data— the admin always gets a full, freshly built payload for the cloned role. This is the “bypassing cache” behavior in action: preview works correctly precisely because it skips the cursor optimization. - Later requests follow the clone. On subsequent content requests, MAM notices the
admin_is_cloninglink, swaps the active session over to the cloned user’s token, and flips anis_cloningflag on for the rest of that request. From there, role resolution (above) runs against the cloned user, so the admin sees that role’s menus, gated content, and screens.
The signal that tells the rest of MAM “this request is a preview, not a real session” is this flag:
MAM_Current_Request::is_cloning();
Any code that gates behavior on the user’s role should check is_cloning() first, because during a preview the “current user” is intentionally not the admin. That’s a deliberate design choice, not an accident — the request context class exists specifically to hold user_role and is_cloning as explicit, mutable state for the life of one request.
Exiting preview is the mirror image (subaction reset_clone): MAM clears the admin_is_cloning meta, restores the admin’s own token, sets is_cloning back to false, and returns a fresh build for the admin’s real role — again inline, again uncached, so the admin immediately drops back to their own view.
The third “clone”: copying a role’s content
There’s a separate, similarly-named feature that’s easy to confuse with preview — but the two have nothing to do with each other.
Cloning role content copies the button set from one role onto another as a starting point. In Mobile App Manager you choose a source role and a destination role and a location; MAM reads the source role’s buttons via the same mam_app_settings_get_buttons filter, deletes whatever the destination role currently has for that location, and writes fresh copies (with new IDs) for the destination role. It’s a one-time, destructive copy that an admin runs deliberately while building — not a live view of another role.
Use it when two roles should start from the same menu and then diverge. It is the build-time analogue of preview: preview lets you see another role’s content, content-cloning lets you seed a role’s content from another.
| Feature | What it does | When it runs | Reversible? |
|---|---|---|---|
| Role resolution | Picks the current user’s role, serves that role’s buttons | Every content request | n/a (automatic) |
| Role cloning (preview) | Admin temporarily views the app as another role, uncached | On demand, in the admin’s session | Yes — exit preview restores the admin |
| Cloning role content | Copies one role’s button set onto another role | One-time, while building in Mobile App Manager | No — overwrites the destination |
How this connects to the rest of MAM
- User roles subsystem. Role resolution, the clone/reset handlers, and the
admin_is_cloningmeta all live in the user-roles subsystem of MAM Main. That subsystem also owns login, registration, and the per-role mobile JSON shaping that the menus ride on. - Per-role app settings. “Show this to that role” extends well beyond menus. The same
(role, ...)keying drives per-role settings (viamam_app_settings_get_setting), which is how things like role-specific caching, gated screens, and conditional buttons are decided. Menus are the most visible case of a general pattern: content and settings in MAM are resolved per role. - The Settings Menu content class. The left-menu / settings drawer is one concrete consumer of role-resolved buttons. Sibling plugins append their own entries to it via filter, and those entries are then subject to the same role gating.
Things that trip people up
- “I changed a menu but the app shows the old one.” That’s the cursor cache, not a bug — normal app sessions wait for the cursor to change. An admin previewing via clone always gets a fresh build, which is exactly why preview is the fastest way to confirm an edit landed.
- “Why is the admin seeing the member menu?” Because they’re cloning a member. Check
is_cloning()/ theadmin_is_cloningmeta before assuming role resolution is broken. - “Cloning broke my role’s buttons.” That’s almost certainly content cloning (the destructive copy), not preview — preview never writes to a role’s button set.
- No enabled role = the default menu. A user whose roles are all disabled for the app falls back to
mam-all. If a real user is unexpectedly seeing the guest menu, check whether their role hasmam-user-roles-to-manage-{role}enabled.
Related
- Content class: Settings Menu — the drawer that renders role-resolved left-menu buttons.
- Concept: Per-role app settings — the broader pattern that menu resolution is one instance of.
- Plugin: MAM Main — user roles — login, registration, role assignment, and the request-context that holds
is_cloning.
