Ask for permissions
Every mobile OS gates the camera, location, notifications, and their kin behind user consent, and
each one asks differently. Day splits the job in two: a build-time declaration in Day.toml that
day build turns into each platform’s manifest entries, and a runtime request through
day-part-permissions:
use day_part_permissions::{Permission, Status, request, status};
if status(Permission::Camera) != Status::Granted {
request(Permission::Camera, |s| println!("camera: {s}"));
}
Works on: every target. iOS, macOS, Android, HarmonyOS, and the web keep consent records and
can prompt; desktop Linux and Windows have no consent database, so there gate() answers
Ungated and status() answers Granted: proceed, and let a real failure surface at the device.
The full per-platform matrix is in the permissions reference.
1. Declare it in Day.toml
The declaration half is required: every mobile OS demands a manifest entry in addition to the runtime request, and skipping it is not a graceful failure (see pitfalls). Declare what you use, with the reason the OS shows the user in its own prompt:
[permissions]
camera = "Attach photos to your notes."
location-when-in-use = "Show stations near you."
notifications = true # needs no reason on any platform
From this, day build writes the Android manifest permissions, the NS…UsageDescription keys in
Info.plist, and the HarmonyOS module.json5 entries. The reason is a runtime parameter on no
platform (every OS reads it from the manifest), which is why it lives here and why
request() takes none. For permissions outside the portable set, [permissions.raw] passes
platform-native names through. day lint flags a permission your code requests but Day.toml
doesn’t declare, so the mismatch is caught in CI rather than on a device.
2. Check, request, react
The runtime half is day_part_permissions. status(perm) answers what the OS will do right now,
never blocking; request(perm, on_done) asks, showing the system prompt when one would appear.
A worked notifications flow, feeding the answer into UI through a signal:
use day_part_permissions::{Permission, Status, can_prompt, open_settings, request, status};
let granted = Signal::new(status(Permission::Notifications) == Status::Granted);
button("Enable reminders").action(move || {
if can_prompt(Permission::Notifications) {
let set = granted.setter();
request(Permission::Notifications, move |s| {
set.set(s == Status::Granted);
});
} else {
// The answer is final for this launch; the OS settings page is the remedy.
open_settings(Permission::Notifications);
}
});
Three things this snippet leans on:
- The callback runs on an unspecified thread, possibly the UI thread, so it delivers into UI
state through a
Setter, not by touching aSignaldirectly. There is deliberately no blockingrequest: the prompt is drawn by the very thread a blocking call would park.request_futureandstatus_futureexist for async code. can_promptpicks the affordance. Apple never re-prompts after a denial and Android may stop, so oncecan_promptis false a “grant access” button is a control that does nothing; offeropen_settingsinstead.should_show_rationaleis Android’s “explain first” signal for drawing your own priming UI before the real prompt.statuscan answerUnknownon first call where the platform is async-only (the web, and Apple notifications).status_async/status_futurewait for the platform’s own answer and never return it.
Concurrent requests for the same permission coalesce into one prompt, and
request_many batches several into one prompt sequence. The permission this example requests is
put to work in Send local notifications.
3. Library crates declare needs, apps declare reasons
A library crate that uses a gated capability declares the machine-facing half in its own manifest:
[package.metadata.day.permissions]
uses = ["camera"]
That names which permission, never the reason: the reason is app copy, shown to your user in
the OS prompt, and it belongs in the app’s Day.toml where you write and localize it. A
contribution whose reason is missing from the app’s Day.toml is a hard build error on the
platforms that show one (iOS and HarmonyOS), naming the crate and the lines to paste.
4. What each platform does with a request
Briefly, since the reference carries the full matrix: Apple
platforms go through each framework’s own authorization API and ask the user once; after a
denial, only Settings can change the answer. Android shows its dialog via requestPermissions
and cannot tell “never asked” from “permanently denied” without app-side state, which Day
deliberately keeps none of; record it yourself in the request callback if you need the
distinction. HarmonyOS can check but not yet prompt from Day, so can_prompt is false there. The
web answers through navigator.permissions and the per-API request calls. Desktop Linux and
Windows resolve immediately as Granted.
Pitfalls
- Requesting before declaring. iOS and macOS terminate the process when it touches a gated
API without the matching
Info.plistkey, not an exception you can catch. Android reports an undeclared permission asStatus::Restricted: the request resolves denied in the same frame, with no dialog, and Settings offers nothing. HarmonyOS refuses the request outright. Step 1 is not optional, andday lintcatches the mismatch. - The capability doesn’t exist here.
gate()answersAbsentandstatus()answersUnsupported; arequestresolves immediately withUnsupportedand no prompt. The reverse case also has a shape:Grantedon an ungated desktop is not a promise the hardware exists; ask the capability’s own part about that. - macOS dev builds can’t be granted anything.
day launch -p macos-appkitruns a bare binary, and TCC reads usage descriptions from a bundle’sInfo.plist, so an unbundled process is denied or killed regardless of whatDay.tomlsays.day pack -p macos-appkitproduces the bundle that can hold a grant. - Dropping a
StatusFuturedoes not dismiss the prompt. No platform can take its own permission dialog off the screen. Dropping stops you listening; the user’s answer is still recorded, and the nextstatus()reflects it.
Reference
permissions — the portable-to-native mapping table, the generated
manifest files and what Day owns in them, the day lint codes, and the raw escape hatches. The
metadata contribution mechanism is extending, and
Send local notifications walks the permission this guide’s example
requests.