M2-04: Location opt-in and the permission prompt #45
No reviewers
Labels
No labels
area/agent
area/capture
area/editor
area/geo
area/security
area/sync
area/transcription
area/ui
area/vault
area/voice-memos
blocked
milestone/M1
milestone/M2
milestone/M3
milestone/M4
needs-decision
needs-hardware
p0
p1
p2
type/bug
type/enhancement
type/feature
type/idea
type/infrastructure
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Stash/stash-ios!45
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "m2-04-location-opt-in"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #31.
A Settings toggle, off on a fresh install, that asks iOS for location and holds it. Nothing
collects a coordinate.
LocationCollection.isAvailableisfalse, the screen says so in as manywords, and both go with the change that lands collection — a permission granted against a promise
this build does not keep would be worse than the feature being absent.
The screen shows what iOS actually permits rather than what was asked for, which is the failure the
issue names: a toggle that reads as on while iOS is refusing.
The invariant
A stored opt-in exists only while iOS is granting. The preference is true exactly when the
toggle reads on, so there is never consent on disk with nothing on screen to show it or clear it.
Three behaviours follow, and each looks like a bug without it:
tap can write is on; a preference standing behind a refusal is unclearable and would take
effect by itself if the permission came back.
at the tap, it outlives a process killed while the prompt is up.
.notDeterminedclears it, like a refusal. Allow Once reverts to that status oncethe app stops being used.
And a permission granted in iOS Settings that Stash never asked for does not turn the feature on.
Decisions in this PR
Always authorization, superseding the When In Use decision of 2026-08-08. Wes: "Go full time
access." What it buys is that the blue lock screen indicator stops being compulsory. What it costs
belongs in any description of this feature: iOS is no longer what enforces "only while
recording" — under When In Use the system would have cut location off the moment Stash was not in
use, so #33's rule is now a promise only our code keeps. The superseded reasoning is kept rather
than deleted.
No accuracy request anywhere. Wes: "I don't want to re-enable access with each recording."
requestTemporaryFullAccuracyAuthorizationis the only API for full accuracy and it lapses when theapp stops being engaged with, so using it would mean re-prompting around every recording. Precise
Location granted at the first system sheet is already permanent; reduced accuracy gets a sentence
and a route to iOS Settings, and still collects — a coarse route is a degraded feature, not a
disabled one.
locationjoinsUIBackgroundModesbefore anything uses it, because settingallowsBackgroundLocationUpdateswhile the mode is absent is documented as a fatal error ratherthan a failed call.
Shape
Stash/Location/LocationOptIn.swift— the rules as pure functions: the resting state, what a tapdoes, and whether a stored opt-in survives what iOS just said. Same separation as
PlaybackGate,and here because it is the one decision that can tell somebody their whereabouts are not being
recorded when they are.
Stash/Location/LocationPermission.swift— the shared observable state. Owns aCLLocationManagerfor authorization only and makes no decisions of its own.Stash/Location/LocationPreferenceStore.swift,LocationCopy.swift,LocationCollection.swiftStash/Views/SettingsView.swift,project.ymlThe copy is one string source because M2-10's heat map empty state offers the same opt-in and the
two must not drift.
Codex
Four passes, in
.agent/m2-04-location-opt-in/, with every finding's disposition inSUMMARY.md.The three that changed the design:
lapsed Allow Once left it saved behind a toggle reading off. This produced the invariant above.
One rejection: no test covers the toggle's
Bindinggetter. A SwiftUI binding needs a UI testtarget that does not exist, and adding one for a one-expression getter is not worth maintaining. It
is covered by the device pass and named as unverified by the suite rather than left implied.
The new rules were mutation-checked — inverting the refusal branches turns four tests red.
Validated
make test— 358 tests green.make lint— clean.plutil -pon the built plist: both usage descriptions,audioandlocationinUIBackgroundModes, and no temporary-accuracy dictionary.denial row and Open Settings render with location revoked; a permission granted from outside the
app leaves the toggle off, which is the invariant working.
wes-iphone— Wes ran it and reported it good.Not validated
Synthetic input does not reach the Simulator on this machine, so nothing behind a tap is
exercised by the automated pass — the prompt, a grant committing the preference, a denial, and
the reduced-accuracy row are all covered only by the device run above and by no test.
.woodpecker/build.ymlstill has no macOS agent, so it sits pending rather than green.Also
The decision file landed in
stash-docson them2-03-transcript-seek-decisionbranch by mistake,so it is on stash-docs PR #6 rather than one of its own. Restoring that branch needed a force
push, which was declined, so it is left where it is rather than worked around.