Goal
Add one or more custom fields to a MAM app’s user profile form, make those fields persist when the user saves their profile (or registers), and surface the saved values back to the app through the phone-data payload.
This is the standard three-part pattern for extending the app’s identity surface from a sibling plugin or a site-specific snippet, without touching mam-main core files:
- Inject the field into the profile form via
mam_add_fields_to_user_profile. - Persist the submitted value via the per-field save filter
mam_user_roles_save_addl_user_profile_field_{meta_key}. - Read it back to the app via a
mam_get_phone_data_before_sendsubscriber.
Prerequisites
- MAM Suite with
mam-mainactive. The hooks below are defined in mam-main’s user-roles form manager. - A place to put PHP that loads on the website (a sibling plugin, a must-use plugin, or a site-specific helper plugin). Do not edit mam-main core files.
- A
usermetakey you own. Prefix it with your plugin slug to avoid collisions with WordPress core meta and with other plugins. - Familiarity with WordPress
update_user_meta()/get_user_meta().
Steps
1. Inject the field into the profile form
Subscribe to mam_add_fields_to_user_profile. The filter is applied by the user-roles form manager when it assembles the profile form’s field list. Each entry you add is merged into the form alongside the built-in fields (first name, last name, email, and so on).
In the live source the form manager reads two keys off each entry — a display title and a meta_key — and uses the meta_key both as the form field’s identifier and as the usermeta key it writes to:
add_filter( 'mam_add_fields_to_user_profile', function ( array $fields ): array {
$fields[] = array(
'title' => 'Loyalty card ID', // shown as the field label
'meta_key' => 'my_plugin_loyalty_id', // form key AND usermeta key
// Only `title` and `meta_key` are read by mam-main's consumers of this
// filter. Any other keys you add (type, required, placeholder, etc.)
// are ignored by the profile-form builder in the current source.
);
return $fields;
} );
Things to know about this filter, from the source:
- It is applied with the in-progress field array as its only argument —
apply_filters( 'mam_add_fields_to_user_profile', array() ). There is no$user_id, so don’t register with a two-argument form expecting one; derive the current user fromget_current_user_id()instead, and never assume an existing user during signup. - The
meta_keyis the field’s identity. It is what the next step’s dynamic save filter name is built from, so pick it deliberately and keep it stable.
2. Persist the submitted value
When the profile form is saved, the form manager walks the submitted fields. For any field whose key has a registered save filter, it fires:
apply_filters( 'mam_user_roles_save_addl_user_profile_field_' . $meta_key, $value );
The dynamic suffix is your field’s meta_key from step 1. Two behaviors are load-bearing and easy to get wrong:
- You must persist the value yourself. The form manager only calls
update_user_meta()automatically for fields that have no save filter registered. The moment you register a save filter for a key, mam-main hands the whole save off to you — it does not write the meta and does not use your return value. If your callback doesn’t callupdate_user_meta(), the value is silently dropped. - The filter receives the raw
$valueonly. The source fires it asapply_filters( 'mam_user_roles_save_addl_user_profile_field_' . $meta_key, $value )— no$user_idsecond argument. Get the user id from the current request withget_current_user_id().
add_filter( 'mam_user_roles_save_addl_user_profile_field_my_plugin_loyalty_id', function ( $value ) {
$user_id = get_current_user_id();
if ( ! $user_id ) {
return $value;
}
// Normalize / validate.
$clean = preg_replace( '/[^A-Z0-9]/i', '', (string) $value );
// YOU must persist — returning a value is not enough.
update_user_meta( $user_id, 'my_plugin_loyalty_id', $clean );
return $clean;
} );
Decide explicitly what an empty submission means: an empty $value will still reach your callback, so choose whether empty deletes the meta or preserves the prior value.
This per-field filter has no structured error channel — returning a value can’t surface a validation message to the user. For cross-field validation or to veto a save with a message, use the whole-payload pre-save filter (mam_user_roles_modify_user_profile_values_before_save) instead. See the related hook references.
3. Run anything that must happen once, at registration
If your field needs a one-time action when the account is first created (seed a default, sync to an external system, queue a welcome flow), hook mam_user_roles_after_create_user. It fires only for users created through mam-main’s registration flow — not for users added in wp-admin. In the source it is fired as apply_filters( 'mam_user_roles_after_create_user', array(), $user_id ): the first argument is an unused pass-through array (the filtered value), and the second argument is the new user id. No submitted registration data is passed — derive whatever you need from $user_id and get_user_meta():
add_filter( 'mam_user_roles_after_create_user', function ( $result, int $user_id ) {
// Seed a default if the field wasn't part of signup.
if ( '' === (string) get_user_meta( $user_id, 'my_plugin_loyalty_id', true ) ) {
update_user_meta( $user_id, 'my_plugin_loyalty_id', '' );
}
return $result;
}, 10, 2 );
Return the first argument ($result) unchanged — it’s the filtered value, and returning it keeps you a well-behaved filter even though every call site currently ignores the return. Don’t re-send the bundled welcome notification from here; the registration flow already dispatches it.
4. Read the value back into the app
The field now persists, but the app won’t see it until you put it in the phone-data payload. Subscribe to mam_get_phone_data_before_send — the primary mobile-API extension point — and inject your value:
add_filter( 'mam_get_phone_data_before_send', function ( array $data_array ): array {
$user_id = get_current_user_id();
if ( $user_id ) {
$data_array['my_plugin_loyalty_id'] =
get_user_meta( $user_id, 'my_plugin_loyalty_id', true );
}
return $data_array;
}, 10 );
Notes for this subscriber:
- This filter is on the hot path — it runs on every phone-data request. Keep the callback cheap; a
get_user_meta()read is fine, an HTTP call is not. - Use priority 10 (the default content-provider cohort). Don’t register at priority 1000, which is reserved for mam-main’s
home_catsinjection. - The key you add (
my_plugin_loyalty_idhere) appears at the top level of the JSON response. Once a deployed app reads it, that key is effectively a frozen contract — renaming it breaks shipped apps. - Whether your value lands at the top level or nested under the existing
userblock is your choice; match whatever the app side expects to read.
Verify
- Open the app’s profile screen (or the in-app profile-completion form). Your field should render with the
titleyou supplied. - Enter a value and save. Confirm it persisted: check
wp_usermetafor yourmeta_key, orget_user_meta( $user_id, 'my_plugin_loyalty_id', true ). - Trigger a fresh phone-data fetch from the app (or call the phone-data endpoint) and confirm your key is present in the JSON with the saved value.
- Register a brand-new user through the app’s signup flow and confirm any
mam_user_roles_after_create_userlogic ran exactly once.
Common pitfalls
- Field renders but never saves. You registered the field (step 1) but not the save filter (step 2) — or your save filter is registered under a name that doesn’t exactly match
mam_user_roles_save_addl_user_profile_field_{meta_key}. A mismatched suffix silently never fires. - Save filter fires but nothing persists. You returned a value but didn’t call
update_user_meta(). Registering the save filter opts you out of mam-main’s automatic write. - Value saves but the app never shows it. You skipped step 4. Persistence and the phone-data payload are separate concerns.
- Assuming
$user_idfrom the filter. Neither the inject filter nor the per-field save filter passes a$user_id. Useget_current_user_id(). - Reading existing meta during signup. At signup the user may not exist yet; guard reads accordingly.
Related
- Hook:
mam_add_fields_to_user_profile— inject step, full field schema. - Hook:
mam_user_roles_save_addl_user_profile_field_{key}— per-field save, persistence rules. - Hook:
mam_user_roles_after_create_user— post-registration lifecycle. - Hook:
mam_user_roles_modify_user_profile_values_before_save— whole-payload validation and veto (use this for user-facing validation errors). - Hook:
mam_get_phone_data_before_send— the mobile-API payload extension point, priority conventions, and the frozen-contract rules.
What’s next
- Add per-field validation with a user-facing error by moving validation into
mam_user_roles_modify_user_profile_values_before_save. - If your field should be cache-aware, follow the cursor pattern in the phone-data subscriber reference so the field only re-sends when its underlying value changes.
