Device Capabilities

Copy Markdown View Source

All device APIs in Dala follow a consistent pattern: call the function from a callback (returning the socket unchanged), then handle the result in handle_info/2. APIs never block the screen process.

Permissions

Some capabilities require an OS permission before they can be used. Request permissions via Dala.Permissions.request/2. The result arrives asynchronously:

def mount(_params, _session, socket) do
  socket = Dala.Permissions.request(socket, :camera)
  {:ok, socket}
end

def handle_info({:permission, :camera, :granted}, socket) do
  {:noreply, Dala.Socket.assign(socket, :camera_ready, true)}
end

def handle_info({:permission, :camera, :denied}, socket) do
  {:noreply, Dala.Socket.assign(socket, :camera_ready, false)}
end

Capabilities that require permission: :camera, :microphone, :photo_library, :location, :notifications

No permission needed: haptics, clipboard, share sheet, file picker.

Haptic feedback

Dala.Haptic.trigger/2 fires synchronously (no handle_info needed) and returns the socket:

def handle_event("tap", %{"tag" => "purchase"}, socket) do
  socket = Dala.Haptic.trigger(socket, :success)
  {:noreply, socket}
end

Feedback types: :light, :medium, :heavy, :success, :error, :warning

iOS uses UIImpactFeedbackGenerator / UINotificationFeedbackGenerator. Android uses View.performHapticFeedback.

Clipboard

# Write to clipboard
def handle_event("tap", %{"tag" => "copy"}, socket) do
  socket = Dala.Clipboard.write(socket, socket.assigns.code)
  {:noreply, socket}
end

# Read from clipboard — result arrives in handle_info
def handle_event("tap", %{"tag" => "paste"}, socket) do
  socket = Dala.Clipboard.read(socket)
  {:noreply, socket}
end

def handle_info({:clipboard, :read, text}, socket) do
  {:noreply, Dala.Socket.assign(socket, :pasted_text, text)}
end

Share sheet

Opens the platform's native share sheet (iOS: UIActivityViewController, Android: ACTION_SEND):

def handle_event("tap", %{"tag" => "share"}, socket) do
  socket = Dala.Share.sheet(socket, text: "Check out this app!", url: "https://example.com")
  {:noreply, socket}
end

Options: :text, :url, :title

Camera

Requires :camera permission (and :microphone for video).

# Capture a photo
socket = Dala.Camera.capture_photo(socket)
socket = Dala.Camera.capture_photo(socket, quality: :medium)

# Record a video
socket = Dala.Camera.capture_video(socket)
socket = Dala.Camera.capture_video(socket, max_duration: 30)

# Results:
def handle_info({:camera, :photo, %{path: path, width: w, height: h}}, socket) do
  {:noreply, Dala.Socket.assign(socket, :photo_path, path)}
end

def handle_info({:camera, :video, %{path: path, duration: seconds}}, socket) do
  {:noreply, Dala.Socket.assign(socket, :video_path, path)}
end

def handle_info({:camera, :cancelled}, socket) do
  {:noreply, socket}
end

path is a local temp file. Copy it to a permanent location before the next capture.

Photos

Browse and pick from the photo library. Requires :photo_library permission.

socket = Dala.Photos.pick(socket)
socket = Dala.Photos.pick(socket, max: 5)  # pick up to 5

def handle_info({:photos, :picked, photos}, socket) do
  # photos is a list of %{path: path, width: w, height: h} maps
  {:noreply, Dala.Socket.assign(socket, :photos, photos)}
end

def handle_info({:photos, :cancelled}, socket) do
  {:noreply, socket}
end

Files

Open the system file picker:

socket = Dala.Files.pick(socket)
socket = Dala.Files.pick(socket, types: ["public.pdf", "public.text"])  # iOS UTI strings
socket = Dala.Files.pick(socket, types: ["application/pdf", "text/plain"])  # Android MIME types

def handle_info({:files, :picked, files}, socket) do
  # files is a list of %{path: path, name: name, size: bytes} maps
  {:noreply, Dala.Socket.assign(socket, :files, files)}
end

Platform note: types uses iOS UTI strings on iOS ("public.pdf") and MIME type strings on Android ("application/pdf"). To support both platforms with the same call, pass both forms — the platform ignores strings it doesn't recognise. See Platform-specific props for a cleaner pattern.


## Camera preview

Display a live camera feed inline (no OS permission dialog for preview):

def mount(_params, _session, socket) do socket = Dala.Camera.start_preview(socket, facing: :back)

end

def render(assigns) do ~dala""" <Column>

<CameraPreview facing={:back} weight={1} />
<Button text="Flip" on_tap={{self(), :flip}} />

""" end

def terminate(_reason, socket) do Dala.Camera.stop_preview(socket) :ok end


The `:camera_preview` component requires an active preview session  call `start_preview/2` before mounting and `stop_preview/1` in `terminate/2`.

## Audio recording

Requires `:microphone` permission.

socket = Dala.Audio.start_recording(socket) socket = Dala.Audio.start_recording(socket, format: :aac, quality: :medium) socket = Dala.Audio.stop_recording(socket)

def handle_info({:audio, :recorded, %{path: path, duration: seconds}}, socket) do

end

def handle_info({:audio, :error, reason}, socket) do

end


Recording formats: `:aac` (default), `:wav`. Quality: `:low`, `:medium` (default), `:high`.

## Audio playback

No permission needed. Plays local files or remote URLs.

socket = Dala.Audio.play(socket, "/path/to/clip.m4a") socket = Dala.Audio.play(socket, path, loop: true, volume: 0.8) socket = Dala.Audio.stop_playback(socket) socket = Dala.Audio.set_volume(socket, 0.5) # adjust without stopping

def handle_info({:audio, :playback_finished, %{path: path}}, socket) do

end

def handle_info({:audio, :playback_error, %{reason: reason}}, socket) do

end


iOS uses `AVAudioPlayer` / `AVPlayer`. Android uses `MediaPlayer`.

## Location

Requires `:location` permission.

Single fix

socket = Dala.Location.get_once(socket)

Continuous updates

socket = Dala.Location.start(socket) socket = Dala.Location.start(socket, accuracy: :high) # :high | :balanced | :low socket = Dala.Location.stop(socket)

def handle_info({:location, %{lat: lat, lon: lon, accuracy: acc, altitude: alt}}, socket) do {:noreply, Dala.Socket.assign(socket, :location, %{lat: lat, lon: lon})} end

def handle_info({:location, :error, reason}, socket) do

end


iOS uses `CLLocationManager`. Android uses `FusedLocationProviderClient`.

## Motion (accelerometer / gyroscope)

socket = Dala.Motion.start(socket) socket = Dala.Motion.start(socket, interval_ms: 100) socket = Dala.Motion.stop(socket)

def handle_info({:motion, %{ax: ax, ay: ay, az: az, gx: gx, gy: gy, gz: gz}}, socket) do {:noreply, Dala.Socket.assign(socket, :motion, %{ax: ax, ay: ay, az: az})} end


## Biometric authentication

socket = Dala.Biometric.authenticate(socket, reason: "Confirm your identity")

def handle_info({:biometric, :success}, socket) do

end

def handle_info({:biometric, :failure, reason}, socket) do

end


iOS uses Face ID / Touch ID. Android uses `BiometricPrompt`.

## QR / barcode scanner

socket = Dala.Scanner.scan(socket)

def handle_info({:scan, :result, %{type: type, value: value}}, socket) do # type: :qr | :ean | :upc | etc.

end

def handle_info({:scan, :cancelled}, socket) do

end


## Notifications

See also [Dala.Notify](Dala.Notify.html) for the full API.

Requires `:notifications` permission.

### Local notifications

Schedule

Dala.Notify.schedule(socket, id: "reminder_1", title: "Time to check in", body: "Open the app to see today's updates", at: ~U[2026-04-16 09:00:00Z], # or delay_seconds: 60 data: %{screen: "reminders"} )

Cancel

Dala.Notify.cancel(socket, "reminder_1")

Receive in handle_info (all app states: foreground, background, relaunched):

def handle_info({:notification, %{id: id, data: data, source: :local}}, socket) do

end


### Push notifications

Register for push tokens and forward them to your server. A server-side push library (`dala_push`) is in development.

#### Server credentials

**Apple (APNs)  token-based auth (recommended)**

Create a signing key at:
https://developer.apple.com/account/resources/authkeys/add

Enable "Apple Push Notifications service (APNs)", download the `.p8` file, and note
the Key ID shown in the portal. One key works across all your apps, both development
and production environments, and never expires (but can be revoked if compromised).

You need four things server-side:
- `.p8` key file  downloaded when you create the key (only shown once)
- Key ID  https://developer.apple.com/account/resources/authkeys/list
- Team ID  https://developer.apple.com/account (Membership Details section)
- Bundle ID  https://developer.apple.com/account/resources/identifiers/list

Your server signs a short-lived JWT from these at send time; there is no separate
token to store. See the APNs documentation for the JWT format.

**Google (FCM)**

Create a Firebase project, then: Project Settings  Service accounts 
Generate new private key. Drop `google-services.json` into `android/app/` for
the Android client.

After :notifications permission is granted:

Dala.Notify.register_push(socket)

Receive the device token:

def handle_info({:push_token, :ios, token}, socket) do MyApp.Server.register_token(:ios, token)

end

def handle_info({:push_token, :android, token}, socket) do MyApp.Server.register_token(:android, token)

end

Receive push notifications:

def handle_info({:notification, %{title: t, body: b, data: d, source: :push}}, socket) do

end


## Storage

App-local file storage using named locations instead of raw paths. No permission needed.

Resolve a location to its absolute path

path = Dala.Storage.dir(:documents) # persists, user-visible on iOS path = Dala.Storage.dir(:cache) # persists until OS needs space path = Dala.Storage.dir(:temp) # ephemeral, may be purged any time path = Dala.Storage.dir(:app_support) # persists, hidden from user, backed up on iOS

File operations

= Dala.Storage.list(:documents) # returns full paths {:ok, meta} = Dala.Storage.stat("/path/to/file") # %{name, path, size, modified_at} {:ok, path} = Dala.Storage.write("/path/file.txt", "contents") {:ok, data} = Dala.Storage.read("/path/file.txt") {:ok, dest} = Dala.Storage.copy("/path/src.txt", :documents) # keeps basename {:ok, dest} = Dala.Storage.move("/path/src.txt", "/path/dest.txt") :ok = Dala.Storage.delete("/path/file.txt")

ext = Dala.Storage.extension("/tmp/clip.mp4") # => ".mp4"


All operations that can fail return `{:ok, value} | {:error, posix}`. `dir/1` raises on an unknown location atom.

For saving to the native media library (Camera Roll, Downloads), see `Dala.Storage.Apple` and `Dala.Storage.Android`.

## WebView

Embed a native web view and communicate with it over a JS bridge. No permission needed.

def render(assigns) do ~dala""" <WebView url="https://example.com" allow={["https://example.com"]} show_url={true} weight={1} /> """ end

Send a message to Elixir from JS:

window.dala.send({ event: "clicked", id: 42 })

def handle_info({:webview, :message, %{"event" => "clicked", "id" => id}}, socket) do

end

A navigation attempt was blocked by the allow: whitelist

def handle_info({:webview, :blocked, url}, socket) do

end


Push a message from Elixir into the page (calls `window.dala.onMessage` handlers):

socket = Dala.WebView.post_message(socket, %{type: "update", value: 42})


Evaluate arbitrary JavaScript and receive the result:

socket = Dala.WebView.eval_js(socket, "document.title")

Result arrives as:

def handle_info({:webview, :eval_result, result}, socket) do

end


Props: `:url` (required), `:allow` (list of URL prefixes  blocks others), `:show_url` (native URL bar), `:title` (static label overriding `:show_url`), `:width`, `:height`.

> **Platform note:** WebView is supported on both iOS and Android.

## Alerts and toasts

`Dala.Alert` shows native dialogs and status messages. No permission needed.

### Alert dialog

Centered modal for confirmations and errors (iOS: `UIAlertController(.alert)`, Android: `AlertDialog`).

def handle_info({:tap, :delete}, socket) do Dala.Alert.alert(socket,

title:   "Delete item?",
message: "This cannot be undone.",
buttons: [
  [label: "Delete", style: :destructive, action: :confirmed_delete],
  [label: "Cancel", style: :cancel]
]

)

end

def handle_info({:alert, :confirmed_delete}, socket) do

end

def handle_info({:alert, :dismiss}, socket) do

end


Dismissing without tapping a button (e.g. Android back gesture) sends `{:alert, :dismiss}`.

### Action sheet

Bottom-anchored list for choosing between actions (iOS: `UIAlertController(.actionSheet)`, Android: list dialog).

Dala.Alert.action_sheet(socket, title: "Share photo", buttons: [

[label: "Save to Photos", action: :save],
[label: "Copy link",      action: :copy],
[label: "Cancel",         style: :cancel]

] )

def handle_info({:alert, :save}, socket), do: {:noreply, save_photo(socket)} def handle_info({:alert, :copy}, socket), do: {:noreply, copy_link(socket)} def handle_info({:alert, :dismiss}, socket), do: {:noreply, socket}


### Toast

Ephemeral status message with no callback.

Dala.Alert.toast(socket, "Saved!") Dala.Alert.toast(socket, "File uploaded", duration: :long)


Duration: `:short` (default, ~2 s) or `:long` (~4 s). iOS renders a floating label overlay; Android uses `Toast`.

### Button options

| Key | Values | Default |
|-----|--------|---------|
| `:label` | string | `""` |
| `:style` | `:default`, `:cancel`, `:destructive` | `:default` |
| `:action` | atom  delivered as `{:alert, atom}` to `handle_info/2` | `:dismiss` |