field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | LICENSE

commit 7754b85b01a45adc0160e6b084713cc9c53b5385
parent 927e7eaa35fa7b3e2631742f4792243980389fbf
Author: triesap <tyson@radroots.org>
Date:   Sun, 14 Jun 2026 15:51:53 -0700

external-actions: stabilize location recovery ui

- scope location check-in accessibility identifiers to the active tab surface
- render permission recovery as its own Capture row for stable XCUITest access
- keep app settings recovery behind the AppleKit External Actions boundary
- preserve the Swift-only Nostr field shell while tightening permission UX

Diffstat:
MRadroots/Views/HomeView.swift | 82++++++++++++++++++++++++++++++++++++++++++-------------------------------------
1 file changed, 44 insertions(+), 38 deletions(-)

diff --git a/Radroots/Views/HomeView.swift b/Radroots/Views/HomeView.swift @@ -68,7 +68,9 @@ private struct TodayView: View { Section("Next Actions") { FieldActionRow(title: "Photo Evidence", subtitle: "Document a crop, delivery, or field condition.", systemImage: "camera.fill") - LocationCheckInRow() + LocationCheckInRow( + accessibilityIDPrefix: "field_ios.today.location_check_in" + ) FieldActionRow(title: "Status Log", subtitle: "Capture a short operational update.", systemImage: "text.badge.checkmark") FieldActionRow(title: "Compliance Note", subtitle: "Reserve audit-ready notes for the current workflow.", systemImage: "checkmark.seal.fill") } @@ -173,7 +175,21 @@ private struct CaptureView: View { } Section("Field Context") { - LocationCheckInRow() + LocationCheckInRow( + accessibilityIDPrefix: "field_ios.location_check_in" + ) + if app.locationCheckInState.showsAppSettingsRecovery { + ExternalActionButton( + title: "Open Settings", + systemImage: "gearshape.fill", + accessibilityID: "field_ios.location_check_in.open_settings" + ) { + await app.openAppSettingsRecovery() + } + if let status = app.externalActionStatus { + ExternalActionStatusText(status: status) + } + } FieldActionRow(title: "Status Log", subtitle: "Record observations from the field.", systemImage: "square.and.pencil") FieldActionRow(title: "Compliance Note", subtitle: "Prepare traceability notes for review.", systemImage: "checkmark.seal.fill") } @@ -361,6 +377,7 @@ private struct FieldActionRow: View { private struct LocationCheckInRow: View { @EnvironmentObject private var app: AppState + let accessibilityIDPrefix: String var body: some View { VStack(alignment: .leading, spacing: 12) { @@ -373,21 +390,22 @@ private struct LocationCheckInRow: View { VStack(alignment: .leading, spacing: 4) { Text("Location Check-in") .font(.headline) + .accessibilityIdentifier("\(accessibilityIDPrefix).card") Text(statusText) .font(.subheadline) .foregroundStyle(.secondary) - .accessibilityIdentifier("field_ios.location_check_in.status") + .accessibilityIdentifier("\(accessibilityIDPrefix).status") if let detailText { Text(detailText) .font(.footnote) .foregroundStyle(.secondary) - .accessibilityIdentifier("field_ios.location_check_in.detail") + .accessibilityIdentifier("\(accessibilityIDPrefix).detail") } } Spacer() if isChecking { ProgressView() - .accessibilityIdentifier("field_ios.location_check_in.progress") + .accessibilityIdentifier("\(accessibilityIDPrefix).progress") } } @@ -402,23 +420,9 @@ private struct LocationCheckInRow: View { } .buttonStyle(.borderedProminent) .disabled(isChecking) - .accessibilityIdentifier("field_ios.location_check_in.action") - - if showsSettingsRecovery { - ExternalActionButton( - title: "Open Settings", - systemImage: "gearshape.fill", - accessibilityID: "field_ios.location_check_in.open_settings" - ) { - await app.openAppSettingsRecovery() - } - if let status = app.externalActionStatus { - ExternalActionStatusText(status: status) - } - } + .accessibilityIdentifier("\(accessibilityIDPrefix).action") } .padding(.vertical, 4) - .accessibilityIdentifier("field_ios.location_check_in.card") .task { await app.refreshLocationCheckInStatus() } @@ -485,24 +489,6 @@ private struct LocationCheckInRow: View { } } - private var showsSettingsRecovery: Bool { - guard let availability = app.locationCheckInState.availability else { - return false - } - guard !isChecking else { - return false - } - guard availability.locationServicesEnabled else { - return true - } - switch availability.authorization { - case .denied, .restricted, .unavailable: - return true - case .notDetermined, .authorizedWhenInUse, .authorizedAlways, .unsupported: - return false - } - } - private func statusText(for availability: RadrootsLocationServicesAvailability) -> String { guard availability.locationServicesEnabled else { return "Location Services are disabled." @@ -524,6 +510,26 @@ private struct LocationCheckInRow: View { } } +private extension FieldLocationCheckInState { + var showsAppSettingsRecovery: Bool { + if case .checking = self { + return false + } + guard let availability else { + return false + } + guard availability.locationServicesEnabled else { + return true + } + switch availability.authorization { + case .denied, .restricted, .unavailable: + return true + case .notDetermined, .authorizedWhenInUse, .authorizedAlways, .unsupported: + return false + } + } +} + private struct ExternalActionButton: View { let title: String let systemImage: String