MapsWidget Location SDK

What It Does

The Beans.ai Maps Widget shows the resident or driver as a live marker on the property map, keeps the map pinned to them as they walk, measures distance to a unit or parking spot, and hands off to Apple or Google Maps for turn-by-turn. All of that needs the device's location.

When you embed the widget in a mobile app's WebView, the page asking for location triggers a second permission prompt — on top of the one your app has already been granted. It is confusing for the user, and on some platforms it cannot be answered at all:

Platform What happens without this SDK
Android WebView The page fires onGeolocationPermissionsShowPrompt. If your app does not implement it, location never arrives and the marker never appears. If it does, the user sees a second, web-looking prompt.
iOS WKWebView iOS 15 and later show a system prompt naming the web page's origin, and there is no delegate hook to pre-answer it. Earlier versions never resolve it at all.
The Location SDK removes that prompt by changing where the page gets location from: your app streams the position it already has permission for directly into the widget. One permission, asked by your app, in your own words.

No change is required to your map page. Keep rendering the widget exactly as documented in the MapsWidget API v1, with navOptions.userLocation = 'LIVE'.

Requirements

Item Requirement
Maps Widget version 1.0.4 or later (any provider: ESRI, Beans Canvas, Google, Mapbox). Also works with Beans.ai-hosted pages.
Page configuration navOptions.userLocation = 'LIVE'. Nothing else to change.
Android minSdk 21. Location permission declared and granted to your app. No Gradle dependencies required.
iOS iOS 12+, WKWebView, and NSLocationWhenInUseUsageDescription in your Info.plist. No CocoaPods/SPM dependencies required.

Downloads

Every file is a single, self-contained drop-in. There is no package to install and no build step.

File Where it runs Purpose
beans-native-location-1.0.0.js Web page Required. The page-side bridge. Your app injects it, or bundles it.
BeansLocationBridge.kt Android app The Android bridge plus the default platform location source.
BeansFusedLocationSource.kt Android app Optional. Uses Google Play Services fused location when you already ship it.
BeansLocationBridge.swift iOS app The iOS bridge.
test-native-location.html Web page Diagnostic page. Load it in your build to confirm the wiring before touching a real map.
We recommend bundling the JavaScript file with your app (Android assets/, iOS app bundle) rather than loading it from beans.ai: it then works offline, is unaffected by your page's content-security policy, and is in place before the page's own scripts run. Both SDKs support either.

How It Works

Your app owns the location permission and the location stream. The page-side bridge takes over the browser's geolocation entry point, so neither the widget nor any other script on the page can reach the prompt.

   your app
     BeansLocationBridge  ──── start / stop ────►  location provider
             │                                          │
             │  ◄──────────── device position ───────────┘
             ▼
   beans-native-location.js   (injected into the page)
     1. registers as the widget's location provider
     2. replaces navigator.geolocation      ← this is what removes the prompt
     3. relays positions into iframes
             │
             ▼
   Beans Maps Widget
     user marker · map pinning · distance · navigation hand-off
Two independent safeguards mean a partial integration still cannot produce a prompt: the widget is pointed at your app's stream, and the browser geolocation API is replaced.

Load order does not matter. If the bridge is installed after the page has already asked the browser for location, the widget's in-flight request is handed over to your app without dropping it.

Widget inside an iframe. Mobile platforms can only evaluate JavaScript in a page's main frame. The bridge handles this for you: injected into every frame, a framed copy of the widget subscribes to its parent and the parent relays each position down. No configuration needed.

Android

1. Manifest
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
If you minify, keep the JavaScript interface methods:
-keepclassmembers class * { @android.webkit.JavascriptInterface <methods>; }
2. Attach the bridge
Copy BeansLocationBridge.kt into your project and attach it before loading the page.
class MapActivity : Activity() {
  private lateinit var bridge: BeansLocationBridge

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val webView = WebView(this)
    setContentView(webView)

    bridge = BeansLocationBridge.attach(
      webView, this,
      BeansLocationBridge.Options(
        // copied into src/main/assets/ — omit to load it from beans.ai instead
        shimAsset = "beans-native-location-1.0.0.js",
        minIntervalMs = 1000,
        minDistanceM = 1f,
        debug = BuildConfig.DEBUG
      )
    )
    bridge.attachDefaultClients()

    webView.loadUrl("https://your-host/your-map-page.html")
  }

  override fun onResume()  { super.onResume();  bridge.resume() }
  override fun onPause()   { super.onPause();   bridge.pause() }
  override fun onDestroy() { bridge.detach();   super.onDestroy() }

  override fun onRequestPermissionsResult(
    requestCode: Int, permissions: Array<out String>, grantResults: IntArray
  ) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    bridge.onRequestPermissionsResult(requestCode, grantResults)
  }
}
attachDefaultClients() installs a WebViewClient that injects the page-side bridge on every navigation, and a WebChromeClient that answers any stray geolocation prompt. If you already set your own clients, wire these two hooks instead:
webView.webViewClient = object : MyWebViewClient() {
  // Inject from BOTH callbacks: the widget can ask for location before the
  // page's load event, and onPageFinished alone leaves a race.
  override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
    super.onPageStarted(view, url, favicon); bridge.injectShim()
  }
  override fun onPageFinished(view: WebView?, url: String?) {
    super.onPageFinished(view, url); bridge.injectShim()
  }
}

webView.webChromeClient = object : MyChromeClient() {
  override fun onGeolocationPermissionsShowPrompt(
    origin: String?, callback: GeolocationPermissions.Callback?
  ) = bridge.grantGeolocationPermission(origin, callback)
}
Options
Option Default Meaning
shimAsset null Filename of the page-side bridge in src/main/assets/. Recommended.
shimUrl beans.ai hosted Used when shimAsset is not set.
locationSource auto Where fixes come from. Auto-selects fused location if available, else the platform provider. Implement BeansLocationSource to reuse your app's existing location stack.
autoRequestPermission true Ask for location permission the first time the page needs it. Set false if you request it during your own onboarding, then call bridge.onPermissionsResolved(granted).
minIntervalMs 1000 Minimum time between fixes.
minDistanceM 1 Minimum movement between fixes, in metres.
highAccuracy true Prefer GPS / high-accuracy priority.
debug false Logcat tracing under the tag BeansLocationBridge.
Google Play Services (optional)
If your app already depends on Play Services location, also copy BeansFusedLocationSource.kt into the same package. It is detected and used automatically — no wiring. Fused positions blend GPS, wifi, cell and sensors, so a resident walking a property gets smoother movement and usable bearings at lower battery cost.
implementation("com.google.android.gms:play-services-location:21.0.1")  // 20.0.0+
Without it, the platform LocationManager is used: GPS and network together for a fast first fix indoors, with coarse fixes suppressed while a recent, more accurate one is in hand.
Other calls you may need
Call Use
bridge.push(location)
bridge.push(lat, lng)
Send a position yourself — a simulated location in tests, or a position your app already tracks.
bridge.pushError(code, message) Report a failure to the page. 1 = permission denied, 2 = position unavailable, 3 = timeout.
bridge.pause() / bridge.resume() Stop and restart location while keeping the page's subscription. Call these from your lifecycle so the GPS is not held open in the background.
bridge.sourceName Which provider is active, for logs and bug reports.

iOS

1. Info.plist
Required — without it iOS refuses to start location updates.
<key>NSLocationWhenInUseUsageDescription</key>
<string>Shows your position on the property map.</string>
2. Create the bridge
Copy BeansLocationBridge.swift into your target and construct it before loading the page — it installs the page-side bridge as a document-start script.
final class MapViewController: UIViewController {
  private var webView: WKWebView!
  private var bridge: BeansLocationBridge!

  override func viewDidLoad() {
    super.viewDidLoad()

    webView = WKWebView(frame: view.bounds, configuration: WKWebViewConfiguration())
    view.addSubview(webView)

    bridge = BeansLocationBridge(
      webView: webView,
      options: .init(
        // bundled with the app — omit to load it from beans.ai instead
        shimBundleResource: "beans-native-location-1.0.0.js",
        distanceFilter: 1,
        useHeading: true,
        debug: true
      )
    )

    webView.load(URLRequest(url: URL(string: "https://your-host/your-map-page.html")!))
  }

  override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated); bridge.resume()
  }
  override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated); bridge.pause()
  }
  deinit { bridge?.detach() }
}
Options
Option Default Meaning
shimBundleResource nil Filename of the page-side bridge in the app bundle. Recommended.
shimURL beans.ai hosted Used when shimBundleResource is not set.
autoRequestAuthorization true Call requestWhenInUseAuthorization() the first time the page needs location.
desiredAccuracy kCLLocationAccuracyBest Passed to CLLocationManager.
distanceFilter 1 Minimum movement between fixes, in metres.
useHeading false Also start the compass. Recommended: a device's course is only valid while moving, so without it the marker's direction indicator does not update while the user stands still.
debug false Console tracing under [BeansLocationBridge].
Note: a WKWebViewConfiguration carries the injected scripts, so create one bridge per web view and call detach() when you are done with it. push(latitude:longitude:), pushError(code:message:), pause() and resume() work as on Android.

Other Hosts

React Native, Flutter, Capacitor, Cordova or your own container all work: anything that can inject the page-side file and evaluate a line of JavaScript can drive the bridge.

The simplest integration is push-only — you never listen for requests from the page, you just send positions on your own schedule. The first position you send activates the bridge, and the prompt stays gone.

  1. Inject beans-native-location-1.0.0.js into the page (as early as your container allows).
  2. Whenever your app has a position, evaluate:
    window.BeansNativeLocation.push({
      lat: 37.4220,
      lng: -122.0841,
      accuracy: 6.5
    });
To also let the page turn location on and off — worth doing for battery, since the widget only needs it while a map is on screen — expose one of the transports in the Wire Protocol section below. React Native's window.ReactNativeWebView is detected automatically; your app receives {type:'beans-location', action:'start'|'stop'|'once'} messages.

Location Payload

The object you hand to push(). Only latitude and longitude are required; everything else improves the experience where available.

Field Type Required? Notes
lat Double Yes Degrees. latitude is also accepted.
lng Double Yes Degrees. longitude is also accepted.
accuracy Double No Horizontal accuracy in metres.
heading Double No Degrees clockwise from true north. Drives the direction indicator on the marker.
speed Double No Metres per second.
altitude
altitudeAccuracy
Double No Metres. Used by multi-floor properties.
timestamp Long No Milliseconds since the Unix epoch.
A JSON string of the same object is also accepted, as is a standard browser GeolocationPosition.

JavaScript API

Available on the page as window.BeansNativeLocation once the file is injected.

Method Called by Purpose
push(position) Your app Deliver a position. See Location Payload.
pushError(code, message) Your app Report a failure. 1 = permission denied, 2 = position unavailable, 3 = timeout.
pushStatus(status) Your app 'ready', 'denied' or 'stopped'. Optional lifecycle signal.
configure(options) Page Tune the stream: highAccuracy, minIntervalMs, minDistanceM, eagerStart, debug.
isNative()
getTransport()
Page Whether a native host is present, and which channel it uses. Useful for building one page that runs both on the web and in your app.
getLastPosition()
getLastError()
isWatching()
Page Current state, for diagnostics.
uninstall() Page Hand geolocation back to the browser.

Wire Protocol

Only needed if you are writing your own host rather than using the Android or iOS file. The page-side bridge picks the first transport it finds, in this order:

Transport Detected by Call the page makes
iOS window.webkit.messageHandlers.beansLocation .postMessage({action, highAccuracy, minIntervalMs, minDistanceM})
Android window.BeansLocationAndroid .start(jsonConfig).stop().once(jsonConfig)
React Native window.ReactNativeWebView .postMessage(JSON.stringify({type:'beans-location', action, ...}))
Parent frame the page is in an iframe parent.postMessage({__beansLocation:true, action, ...}, '*')
action is 'start', 'stop' or 'once'. Positions travel back the other way through push(). If you implement no transport at all, push-only mode applies and the page simply consumes whatever you send.

Testing

Load the diagnostic page in your own build — before pointing it at a real map — and confirm the wiring end to end:

https://www.beans.ai/mapswidget/sdk/test-native-location.html

Row What you want to see
Shim loaded A version number. bootstrap stub only means the JavaScript file itself never loaded — usually a content-security policy blocking it, so bundle it with the app instead.
Transport android, ios or push-only. none means your injection is not landing.
navigator.geolocation bridged (no prompt).
Fixes received Climbing as you move, with a plausible position and accuracy.
Then load your real map page and confirm three things: no permission prompt from the page, the user marker appears, and it follows you.

Troubleshooting

Symptom Cause
Prompt still appears The page-side file is not injected (check Transport on the diagnostic page), or the page reached geolocation before injection. Inject at document start, or from both onPageStarted and onPageFinished.
Transport reads none inside your app Android: the JavaScript interface was not registered before the page loaded, or was renamed. iOS: the message handler is not named beansLocation.
No positions and no error The page never asked. The widget only asks when navOptions.userLocation = 'LIVE'. Also check that pause() was not called without a matching resume().
Positions arrive but no marker The position is far from the property. Confirm the fix on the diagnostic page first; if it is correct there, the bridge has done its job.
Marker direction is wrong when standing still A device reports no course while stationary. On iOS set useHeading: true; on Android this resolves as soon as the user moves.
Works on the main page, not in an embedded map The iframe case — make sure the file is injected into subframes as well as the main frame.
Battery drain Call pause() and resume() from your lifecycle, and raise minIntervalMs / minDistanceM if metre-level tracking is not needed.

Privacy

The SDK is a transport: it moves the position your app already holds into the page that is already displaying your map. It adds no storage, no logging to Beans.ai, and no background collection — it streams only while the map is asking, and stops when you call pause() or the map goes away.

Because your app now asks for location itself, the permission prompt your users see is yours: your wording, your timing, and a single request instead of two. What the widget then does with the position — drawing the user marker, pinning the map, measuring distance to a unit, handing off to Apple or Google Maps — is unchanged from the standard integration described in the MapsWidget API v1.

Note that a WebView JavaScript interface is reachable by every frame in the page. As with any WebView integration, load only pages you control or trust.