Permissions
Access to the camera, location, and other protected features may require the user’s permission.
Day handles this in two stages: a declaration in Day.toml supplies the platform metadata,
and day-part-permissions checks or requests access while the app is running.
use day_part_permissions::{Permission, Status, request, status};
if status(Permission::Camera) != Status::Granted {
request(Permission::Camera, |s| println!("camera: {s}"));
}
Permission behavior differs by platform. Apple platforms, Android, and web can prompt where
the capability supports it. Day can check HarmonyOS permissions but cannot yet prompt there.
On desktop Linux and Windows, this API reports Ungated and Granted; that does not guarantee
the device or service is available. See platform behavior
for details.
1. Declare it in Day.toml
Declare permissions before calling the protected API. Day uses these entries to generate platform metadata, including user-facing reasons where the OS requires them:
[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 belongs to the app’s metadata, so request() does not take a reason string. 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.
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. This
notifications flow feeds 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);
}
});
- 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 no blockingrequest, because the prompt is drawn by the very thread a blocking call would park.request_futureandstatus_futureexist for async code. can_prompttells you whether to offer a request button. 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_asyncandstatus_futurewait for the platform’s answer instead of returningUnknown.
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 Local notifications.
3. Permissions a library crate uses
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
The reference carries the full matrix. Apple platforms go
through each framework’s 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
does not keep, so 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
- iOS and macOS terminate the process when it touches a gated API without the matching
Info.plistkey; there is no exception to 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 required, andday lintcatches the mismatch. - Where the capability doesn’t exist,
gate()answersAbsentandstatus()answersUnsupported; arequestresolves immediately withUnsupportedand no prompt. The reverse case also exists: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,
manifest entries generated by Day, the day lint codes, and direct platform API access. The
metadata contribution mechanism is extending, and
Local notifications shows how to use notification permission in an app.