The idea: “where is the user, and what should they see?”
Plenty of mobile apps only make sense in a place. A restaurant directory, an events app, a “stores near me” listing — each one has to answer the same question before it can show anything useful: where is the user right now, and how far out should we look?
MAM Suite answers that question with two pieces that work together:
mam-geofilters— the feature that gives the app a city picker, resolves an active location, and attaches a search radius to every content request.- The Enable Location content class in
mam-main— a gating screen that asks the user for GPS permission before any GPS-driven behavior runs.
Here’s the mental model worth keeping straight: these two pieces are separable. Geofilters works fine with no GPS at all — the user just picks a city from a list. GPS is an enhancement that adds a “use my current location” option on top of that list. Enable Location is the door you walk the user through to unlock that enhancement. Knowing which piece does what is the difference between a picker that shows up empty and one that shows the user’s real neighborhood right at the top.
Below, you’ll see how location gets resolved, how the active city and radius turn into filtered content, and how the Enable Location screen gates the GPS path.
Three ways the app figures out location
There are three possible sources for “where the user is,” and they’re tried in a specific order of preference. Once you know that order, almost everything surprising about the picker starts to make sense.
1. The city picker (always available)
The baseline is a hand-curated list of cities. mam-geofilters ships with a default list of US cities (San Jose, Oakland, New York, Los Angeles, Chicago, and so on), each carrying a display name and a latitude/longitude pair. This list is built server-side and sent to the app as the data source for the picker. The user taps a city, and that city’s coordinates become the active location.
This path needs no GPS permission and no third-party plugin. Even with location services turned off entirely, the user can still pick a city and get filtered content. That’s why it’s the foundation — everything else just prepends onto this list.
2. GPS — the device’s real coordinates (preferred when available)
When the device has granted location permission and the app has a GPS fix, the app sends the real coordinates along with the content request (in the request as actual_lat and actual_lon). When the server sees those coordinates — and as long as the request didn’t explicitly opt out of GPS — it prepends a “Current Location” entry to the top of the picker, positioned at the device’s exact coordinates.
So GPS doesn’t replace the city list; it adds a smarter first option. The user can still scroll down and choose a named city, but “Current Location” sits at the top as the most relevant default.
There’s one more setting worth knowing about here. A site-level option (gd-show-current-location-on-app) can suppress the Current Location entry; when it’s set to no, the server skips the prepend even if GPS coordinates were sent. Reach for this when you want a fixed-city experience and don’t want the app defaulting to wherever the phone happens to be.
3. IP-based fallback (only when GPS is off and GeoIP is installed)
When the app reports that GPS is unavailable — the request carries a no_gps=yes flag — and the optional GeoIP Detection plugin is installed, the server can still make a reasonable guess. It reads the request’s IP address, looks up an approximate city from that IP, and prepends that city to the picker. On this path the server also sets a location_name value so the app can show which city it inferred.
This is a genuine fallback, not a primary path. It only fires when GPS is explicitly off and GeoIP is present. If GeoIP isn’t installed, there’s no IP guess — the app simply shows the plain city list and the user picks manually.
The order, in one sentence
GPS coordinates win when present and not suppressed; otherwise, if GPS is off and GeoIP is available, an IP-derived city is offered; otherwise the user works from the curated city list alone. Every path ultimately produces an active location with a latitude and longitude — that’s the only thing the content filter actually consumes.
How the active city + radius filter content
Once there’s an active location, filtering is conceptually simple: active coordinates + radius = a circle, and the app asks for content inside that circle.
The radius
Alongside the active city, every content request carries a search radius in miles. The radius is resolved on the server in this order:
- A per-request override. If the request supplies a
radiusvalue, that wins. This lets the app offer the user a “search within X miles” control without an admin change. - The role’s configured radius. Otherwise the server reads the per-role app setting (
tsl-setting-geofilter_radius). Because it’s per-role, different audiences in the same app can search at different distances. - A default of 25 miles. If nothing is configured, the radius falls back to 25.
Whatever the source, the radius is capped at 3000 miles — effectively “the whole country” — so a misconfiguration can’t turn into an unbounded query.
Turning the circle into results
The active city’s coordinates and the resolved radius are sent on each content request. The backend uses them to return only geographically relevant results — listings, events, posts, or whatever the app’s content type is — that fall within the radius of the active point. When the user picks a different city, two companion flags tell the app to refresh the home view so the new location takes effect: one says “reload home on location change,” and a second asks for that reload to happen quietly, without a visible spinner.
Here’s the key insight: content filtering doesn’t care how the active location was chosen. GPS, IP, or a manual tap all collapse to the same thing — a lat/lon and a radius. That’s what keeps the system simple: the filter has exactly one job and one input shape.
How “Enable Location” gates the GPS path
The GPS path above only works if the user has granted the device location permission. That’s where the Enable Location content class in mam-main comes in. It is a small, settings-light screen (a nav button content type) whose entire job is to walk the user through granting GPS permission.
Why a separate screen exists
Both Apple and Google scrutinize why an app wants location, and they reject builds whose justification is vague. So MAM splits the request into two steps:
- Your custom rationale. The Enable Location screen shows a title and body you write — explaining, in your own words, why the app needs location (e.g., “so we can show events near you”). It can also navigate the user somewhere specific after permission is granted.
- The OS permission prompt. After your rationale, the operating system shows its own canonical permission dialog. The string in that dialog isn’t set on this screen — it comes from the app’s publish-time permission strings (for iOS,
ios_app_gps_message). Both should explain the same thing.
This is deliberate. The rationale screen sets expectations; the OS prompt is the one that actually grants the permission.
Why gating matters
The OS-level permission prompt is one-shot. If the user declines, the app can’t simply ask again — they’d have to dig into device Settings to turn it back on. So the order here matters more than it might seem: show a clear rationale first, then trigger the OS prompt, so the user understands the value before they’re asked. A vague or skipped rationale doesn’t just risk app-store rejection — it costs you the GPS path for every user who taps “Don’t Allow.”
If the user never grants permission, nothing breaks. The app falls back exactly as described above: the city picker still works, and if GPS is off the IP fallback (when GeoIP is installed) can still offer a guessed city. Enable Location unlocks the GPS enhancement; it isn’t a prerequisite for geofilters to function.
How the pieces connect
Putting it together, here’s the flow for a single content request:
- Is geofilters on for this user? A per-role decision (
tsl-use_geofiltersortsl-geofilters_show_as_icon) determines whether the picker is shown at all, and as an inline strip or a compact icon. If it’s off, none of the rest applies. - Build the city list. The server assembles the curated list of cities — the always-available baseline.
- Resolve the best active location. GPS coordinates (if granted via Enable Location and not suppressed) prepend a “Current Location” entry; otherwise, with GPS off and GeoIP installed, an IP-derived city is prepended; otherwise the user picks from the list.
- Attach the radius. A per-request override, the role’s configured radius, or the 25-mile default — capped at 3000.
- Filter and render. The active coordinates and radius go out with the content request; the backend returns what’s inside the circle; the app refreshes the home view whenever the active city changes.
That clean separation between steps is what makes geofilters predictable. Enable Location governs only step 3’s GPS branch. The radius logic in step 4 is independent of how the location was chosen. And the city list in step 2 is the floor that guarantees the feature works even with zero permissions and no optional plugins.
Where to go next
- To turn the feature on and set a default radius, see the admin recipe for enabling geofilters.
- To trim, edit, or replace the curated city list, see the recipes for managing the city list and per-neighborhood city lists.
- To override the on/off decision or the radius in code, see the hook references for
mam_force_geofiltersandmam_geofilter_radius. - To customize the permission rationale screen, see the Enable Location content class reference in the App Settings reference.
