Search documentation of mob

# Permissions

Copy Markdown View Source

Single source of truth for the OS-level permissions Mob exposes, the manifest / `Info.plist` entries each one requires, and the platform-specific gotchas that aren't covered by the runtime API alone.

If you're hitting "the dialog never appears" or "I called the NIF and nothing happened", this is the first place to look.

## TL;DR

- Call `Mob.Permissions.request(socket, :capability)` from your screen.

- The result arrives as `handle_info({:permission, :capability, :granted | :denied}, socket)`.

- iOS additionally needs the matching `NS*UsageDescription` key in `ios/Info.plist`. Without it, the dialog is silently suppressed and you get nothing — no event, no error.
- Android additionally needs the matching `uses-permission` line in `AndroidManifest.xml`. The `mob.new` template ships most of these already; if you added a feature after generating the project, double-check.

## The per-capability table

`Mob.Permissions` cap | iOS `Info.plist` key | Android `uses-permission` | Notes
--- | --- | --- | ---
`:camera` | `NSCameraUsageDescription` | `android.permission.CAMERA` | Required by `Mob.Camera`. `CameraPreview` also needs the plist key but does not call `Mob.Permissions.request/2` — request explicitly before mounting it.
`:microphone` | `NSMicrophoneUsageDescription` | `android.permission.RECORD_AUDIO` | Required by `Mob.Audio.start_recording/2` and by `Mob.Camera.capture_video/2`.
`:photo_library` | `NSPhotoLibraryUsageDescription` | API 33+: `READ_MEDIA_IMAGES` + `READ_MEDIA_VIDEO`. API ≤32: `READ_EXTERNAL_STORAGE`. | Required by `Mob.Photos.pick/2`.
`:location` | `NSLocationWhenInUseUsageDescription` | `ACCESS_FINE_LOCATION` (high accuracy) and/or `ACCESS_COARSE_LOCATION` (low accuracy). | See iOS notes below — the dialog timing is unusual.
`:notifications` | (none — handled by `UNUserNotificationCenter`) | API 33+: `android.permission.POST_NOTIFICATIONS` | iOS shows the dialog the first time `request/2` runs. Android API ≤32 doesn't need a permission at all (notifications are user-controllable in Settings).

Capabilities that need no runtime permission on either platform and do not appear in the table:

- `Mob.Haptic`, `Mob.Clipboard`, `Mob.Share`, `Mob.Files.pick/2`, `Mob.Toast`, `Mob.Alert`, `Mob.WebView`, `Mob.Motion`, `Mob.Biometric` (uses biometric prompt UI but does not require a permission grant), `Mob.Storage` (app-local paths only).

Capabilities that need an `Info.plist` or manifest entry without going through `Mob.Permissions.request/2`:

Operation | iOS `Info.plist` key | Android
--- | --- | ---
`Mob.Storage.save_to_photo_library/2` | `NSPhotoLibraryAddUsageDescription` | Same `READ_MEDIA_*` family as `:photo_library` on API 33+.
`Mob.Audio.play/2` (no permission) | none | none
`Mob.Camera.start_preview/2` (no permission for the preview; capture still needs `:camera`) | `NSCameraUsageDescription` | `CAMERA`

## What the `mob.new` template ships by default

If you generate a fresh project with `mix mob.new`, the template emits:

- `ios/Info.plist` — `NSCameraUsageDescription` and `NSMicrophoneUsageDescription`. Nothing else.
- `android/app/src/main/AndroidManifest.xml` — `CAMERA`, `RECORD_AUDIO`, `ACCESS_FINE_LOCATION`, `ACCESS_COARSE_LOCATION`, `READ_MEDIA_IMAGES`, `READ_MEDIA_VIDEO`, `READ_EXTERNAL_STORAGE` (API ≤32 only), `POST_NOTIFICATIONS`, `VIBRATE`, `FOREGROUND_SERVICE`, `INTERNET`, `RECEIVE_BOOT_COMPLETED`.

So out-of-the-box your project covers camera + microphone on both platforms, plus everything Android needs for the other capabilities. Anything iOS-side beyond camera + mic needs you to add the `Info.plist` key yourself before the first time you call that capability. The most common ones to add:

    <key>NSLocationWhenInUseUsageDescription</key>
    <string>MyApp shows your location to ...</string>

    <key>NSPhotoLibraryUsageDescription</key>
    <string>MyApp lets you pick photos from your library.</string>

    <key>NSPhotoLibraryAddUsageDescription</key>
    <string>MyApp saves captures to your photo library.</string>

If you ship without the key, iOS won't even log the missing-key error in any obvious place — the dialog just silently doesn't appear, and the underlying `request*Authorization` call no-ops. Symptom looks identical to "the user denied permission" except no `denied` event ever arrives.

## iOS-specific notes

### iOS location extras

Apple's `CLLocationManager` couples permission and updates more tightly than the other capabilities. Mob exposes both paths:

1. `Mob.Permissions.request(socket, :location)` calls `requestWhenInUseAuthorization` on a dedicated `CLLocationManager` and reports the user's actual choice as `{:permission, :location, :granted | :denied}` once the dialog is dismissed (or immediately if the permission was previously decided).

2. `Mob.Location.get_once/1` and `Mob.Location.start/2` also trigger the dialog if `request/2` wasn't called yet. The dialog is one-shot per app install — subsequent calls short-circuit with the cached authorization.

3. If the user denies, two events flow:

   - `Mob.Permissions.request/2`'s caller hears `{:permission, :location, :denied}`.
   - `Mob.Location.get_once/1`/`start/2`'s caller hears `{:location, :error, :permission_denied}` (via the `locationManagerDidChangeAuthorization:` callback). This means a screen that skipped `request/2` and went straight to `get_once` still has a way to break out of the "waiting for fix…" state.

4. The `Allow Once` button on iOS counts as `:granted` for the current run of the app. The next launch will prompt again.

5. Authorization can change mid-session — the user pops out to Settings and revokes. The delegate fires `{:location, :error, :permission_denied}` when that happens; surface it in your screen if you care about long-running tracking sessions.

### Camera + microphone

These go through `AVFoundation`'s `requestAccessForMediaType`, which fires the dialog at `request/2` time. No additional gotchas — make sure the plist key is present, the dialog appears, you get a typed `{:permission, :camera | :microphone, ...}` event.

### Photo library

`PHPhotoLibrary.requestAuthorizationForAccessLevel:PHAccessLevelReadWrite` is what `:photo_library` invokes. iOS treats `PHAuthorizationStatusLimited` (the user picked "Selected Photos…") as `:granted` from your screen's perspective — the rest of `Mob.Photos` deals with the limited-access set transparently.

### Notifications

Uses `UNUserNotificationCenter requestAuthorizationWithOptions:`. Asks for alert, sound, and badge in one shot. The current implementation returns `:granted` if the user granted any of the three.

## Android-specific notes

### Foreground vs background location

Mob only requests foreground location (`ACCESS_FINE_LOCATION` / `ACCESS_COARSE_LOCATION`). If your app needs to keep tracking while backgrounded, you need to additionally declare `ACCESS_BACKGROUND_LOCATION` in the manifest and request it through a custom flow — `Mob.Permissions.request/2` doesn't surface that capability today.

### Notifications on Android ≤ 12

Pre-API-33, posting a notification does not require a runtime permission grant — the user controls it via Settings. The `{:permission, :notifications, :granted}` event will still fire from `request/2` so your screen code stays portable.

### Storage and photos

API 33+ replaced the single `READ_EXTERNAL_STORAGE` permission with per-media-type permissions (`READ_MEDIA_IMAGES`, `READ_MEDIA_VIDEO`). The `mob.new` template declares all of them so the photo picker works across API levels. Saving with `Mob.Storage.save_to_photo_library/2` uses `MediaStore`, which doesn't require a permission on API 29+ at all — the manifest declarations are only for the read path.

## Re-requesting after denial

Calling `Mob.Permissions.request/2` again after the user denied does not re-show the dialog on either platform — that's an OS restriction. The event still arrives (with `:denied`), so your screen can re-render an explanation. To actually re-prompt the user, they have to go through system Settings:

- iOS: Settings → MyApp → \<capability>
- Android: Settings → Apps → MyApp → Permissions → \<capability>

A common UX is: on `:denied`, show a "Permission needed — open Settings" CTA. `Mob.OpenUrl.open/2` with the appropriate scheme (`"app-settings:"` on iOS, `Intent.ACTION_APPLICATION_DETAILS_SETTINGS` on Android — surfaced via `Mob.System.open_app_settings/1` if your project has it; otherwise call the manifest-permitted scheme directly) will jump straight to the right settings page.

## Diagnosing a stuck request

Symptom: you called `Mob.Permissions.request/2` (or a capability function), no dialog appears, no `:permission`/`:error` event ever arrives.

Run through this in order:

1. iOS plist key present? Open `ios/Info.plist` (or the rendered bundle inside the `.app`) and confirm the `NS*UsageDescription` for the capability is there. The single most common cause.
2. Android manifest entry present? Open `android/app/src/main/AndroidManifest.xml`. If you added the feature post-`mob.new`, the entry may be missing.
3. Already denied at the OS level? iOS: Settings → MyApp → \<cap>. Android: Settings → Apps → MyApp → Permissions. A previously-denied permission won't re-prompt; `request/2` still fires the `:denied` event, so check your `handle_info({:permission, :cap, :denied}, _)` clause exists.
4. The screen process actually still alive? If your screen crashed before `handle_info/2` ran, the message is lost. Check `adb logcat` or the iOS device console for a crash earlier in the pipeline.
5. You're calling `request/2` from a non-screen process. `enif_send` targets the calling pid; if a Task or `spawn` ran the request, its inbox is where the event went. Always request from the screen GenServer.

## Cross-platform pattern

    def mount(_params, _session, socket) do
      # Cheap and idempotent on both platforms. Safe to call even if
      # you're not yet ready to use the capability — the response
      # informs whether the action button below should be enabled.
      socket = Mob.Permissions.request(socket, :location)
      {:ok, Mob.Socket.assign(socket, permission: :pending)}
    end

    def handle_info({:permission, :location, :granted}, socket) do
      {:noreply, Mob.Socket.assign(socket, permission: :granted)}
    end

    def handle_info({:permission, :location, :denied}, socket) do
      # Render a "needs permission — open Settings" CTA.
      {:noreply, Mob.Socket.assign(socket, permission: :denied)}
    end

← Previous Page Mobile Surface Matrix
Next Page → Native Extensions (NIFs, features)