field_ios

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

commit def6c140a95ae67be2034828ca58e7e60f743170
parent 412a1a940cbbc016bcdecccf081ea34936a336f5
Author: triesap <tyson@radroots.org>
Date:   Sun, 14 Jun 2026 01:15:19 -0700

app: route file roots through apple kit

- add the field local state AppleKit boundary

- use AppleKit file roots for log file placement

- split destructive local reset from identity-only reset

- pin the app to the AppleKit file-access package revision

Diffstat:
MRadroots.xcodeproj/project.pbxproj | 4++++
MRadroots.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 2+-
MRadroots/App/AppState.swift | 5+++--
ARadroots/Runtime/FieldLocalState.swift | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MRadroots/Runtime/FieldSecureIdentityStore.swift | 13-------------
MRadroots/Runtime/LoggingSettings.swift | 10+++++++---
MRadroots/Runtime/Radroots.swift | 2+-
7 files changed, 72 insertions(+), 20 deletions(-)

diff --git a/Radroots.xcodeproj/project.pbxproj b/Radroots.xcodeproj/project.pbxproj @@ -24,6 +24,7 @@ 505A5731ACDBBB0296134340 /* TradeListingCreateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947ADA6D32E42ED2B40A5351 /* TradeListingCreateView.swift */; }; 5AECD474FB2F91855BDD79C0 /* PostFeedViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F21554DA87EEC1E5C5F38365 /* PostFeedViewModel.swift */; }; 657BEA5AAFF129E10177FE63 /* Nostr.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63189EB90A86A9929BECD9ED /* Nostr.swift */; }; + 6E15D30653861F26AC45B501 /* FieldLocalState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EB0A2CFCBBFA9D204C6992B /* FieldLocalState.swift */; }; 7C8DD424F3E3E0AB1B133863 /* RadrootsKitBindings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC881872F750120A184F45E6 /* RadrootsKitBindings.swift */; }; 7FD8FB018DA09568303194B2 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADE61264E2C98E73828E8680 /* Strings.swift */; }; 8B923F78BF5B680C7F6A7CE3 /* PostFeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AE0EB327C10171444553378 /* PostFeedView.swift */; }; @@ -77,6 +78,7 @@ 93D729E070C32490545FA837 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; }; 947ADA6D32E42ED2B40A5351 /* TradeListingCreateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TradeListingCreateView.swift; sourceTree = "<group>"; }; 9AE0EB327C10171444553378 /* PostFeedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostFeedView.swift; sourceTree = "<group>"; }; + 9EB0A2CFCBBFA9D204C6992B /* FieldLocalState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldLocalState.swift; sourceTree = "<group>"; }; A0B0C9861CD86EAD3CAD549E /* View+Nav.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Nav.swift"; sourceTree = "<group>"; }; A71EFADBC7D54AF5B9314773 /* BuildConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildConfig.swift; sourceTree = "<group>"; }; ADE61264E2C98E73828E8680 /* Strings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = "<group>"; }; @@ -174,6 +176,7 @@ children = ( A71EFADBC7D54AF5B9314773 /* BuildConfig.swift */, CA942715DB13FFD494FD35A0 /* FieldIdentityPublicMetadataStore.swift */, + 9EB0A2CFCBBFA9D204C6992B /* FieldLocalState.swift */, E883FDB7004A210C9D7BE27A /* FieldRuntimeService.swift */, 8246B707FA9D218414EC4038 /* FieldSecureIdentityStore.swift */, D4DE3DD8C3BB2F63676F463E /* LoggingSettings.swift */, @@ -401,6 +404,7 @@ D3834AF9A4E1327B7DA557F3 /* CopyRow.swift in Sources */, 4025E63F6603011431B8A0E1 /* DebugDump.swift in Sources */, D4CFDE54747B6D6957977025 /* FieldIdentityPublicMetadataStore.swift in Sources */, + 6E15D30653861F26AC45B501 /* FieldLocalState.swift in Sources */, D25C1E1DC99F5CF8E99AE970 /* FieldRuntimeService.swift in Sources */, D9BF5BE7E4AB5EACBF342539 /* FieldSecureIdentityStore.swift in Sources */, 1E5B41A3E1F9A7D68F63B079 /* HomeView.swift in Sources */, diff --git a/Radroots.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Radroots.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -7,7 +7,7 @@ "location" : "git@github.com:radrootslabs/apple_kit.git", "state" : { "branch" : "master", - "revision" : "d97c2badf6168e8d60289a4aef4190f820e9dfc0" + "revision" : "b62c0988abc15b653423153dd2a7a1f4d12ec952" } } ], diff --git a/Radroots/App/AppState.swift b/Radroots/App/AppState.swift @@ -94,7 +94,8 @@ public final class AppState: ObservableObject { secureIdentityStore = secureStore identityMetadataStore = metadataStore if BuildConfig.bool(.resetLocalState) == true { - try secureStore.resetLocalState(bundleIdentifier: try bundleIdentifier()) + try FieldLocalState.resetFileRoots(bundleIdentifier: try bundleIdentifier()) + try secureStore.deleteSelectedSecret() metadataStore.delete() try await resetRuntimeIdentityState(using: service) applyNoIdentity() @@ -178,7 +179,7 @@ public final class AppState: ObservableObject { public func resetLocalIdentity() async throws { let service = try requireRuntimeService() - try secureIdentityStoreOrConfigured().resetLocalState(bundleIdentifier: try bundleIdentifier()) + try secureIdentityStoreOrConfigured().deleteSelectedSecret() try identityMetadataStoreOrConfigured().delete() try await resetRuntimeIdentityState(using: service) applyNoIdentity() diff --git a/Radroots/Runtime/FieldLocalState.swift b/Radroots/Runtime/FieldLocalState.swift @@ -0,0 +1,56 @@ +import Foundation +import RadrootsKit + +enum FieldLocalStateError: LocalizedError { + case missingBundleIdentifier + case invalidLogFileName(String) + + var errorDescription: String? { + switch self { + case .missingBundleIdentifier: + "Missing field iOS bundle identifier." + case .invalidLogFileName(let value): + "Invalid RADROOTS_FIELD_IOS_LOGGING_FILE_NAME: \(value)." + } + } +} + +enum FieldLocalState { + static func roots(bundleIdentifier: String) throws -> RadrootsAppleFileRoots { + let appIdentifier = try normalizedBundleIdentifier(bundleIdentifier) + return try RadrootsAppleFileRoots.appContainer(appIdentifier: appIdentifier) + } + + static func fileAccess(bundleIdentifier: String) throws -> RadrootsAppleFileAccess { + try RadrootsAppleFileAccess(roots: roots(bundleIdentifier: bundleIdentifier)) + } + + static func logFileURL(bundleIdentifier: String, fileName: String) throws -> URL { + let normalizedFileName = try normalizedLogFileName(fileName) + let file = RadrootsFileReference(scope: .logs, relativePath: normalizedFileName) + return try roots(bundleIdentifier: bundleIdentifier).resolvedURL(for: file) + } + + static func resetFileRoots(bundleIdentifier: String) throws { + try fileAccess(bundleIdentifier: bundleIdentifier).resetFileRoots() + } + + private static func normalizedBundleIdentifier(_ bundleIdentifier: String) throws -> String { + let trimmed = bundleIdentifier.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { + throw FieldLocalStateError.missingBundleIdentifier + } + return trimmed + } + + private static func normalizedLogFileName(_ fileName: String) throws -> String { + let trimmed = fileName.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty, + !trimmed.contains("/"), + !trimmed.contains("\\"), + !trimmed.contains("\0") else { + throw FieldLocalStateError.invalidLogFileName(fileName) + } + return trimmed + } +} diff --git a/Radroots/Runtime/FieldSecureIdentityStore.swift b/Radroots/Runtime/FieldSecureIdentityStore.swift @@ -134,19 +134,6 @@ struct FieldSecureIdentityStore { try store.delete(Self.selectedSecretKey) } - func resetLocalState(bundleIdentifier: String) throws { - let trimmedBundleIdentifier = bundleIdentifier.trimmingCharacters(in: .whitespacesAndNewlines) - guard !trimmedBundleIdentifier.isEmpty else { - throw FieldSecureIdentityStoreError.missingBundleIdentifier - } - try RadrootsAppLocalStateReset.reset( - RadrootsAppLocalStateResetRequest( - appIdentifier: trimmedBundleIdentifier, - keychainServiceNames: [try Self.secureStoreServiceName(servicePrefix: servicePrefix)] - ) - ) - } - static func secureStoreServiceName(servicePrefix: String) throws -> String { try selectedSecretKey.serviceName(servicePrefix: servicePrefix) } diff --git a/Radroots/Runtime/LoggingSettings.swift b/Radroots/Runtime/LoggingSettings.swift @@ -14,13 +14,17 @@ struct LoggingSettings: Equatable { return LoggingSettings(stdout: stdout, fileEnabled: fileEnabled, fileName: fileName, level: level) } - func apply() throws { + func apply(bundleIdentifier: String) throws { if let level { setenv("RUST_LOG", level, 1) } if fileEnabled { - let dir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!.path - try initLogging(dir: dir, fileName: fileName, isStdout: stdout) + let logFileURL = try FieldLocalState.logFileURL(bundleIdentifier: bundleIdentifier, fileName: fileName) + try initLogging( + dir: logFileURL.deletingLastPathComponent().path, + fileName: logFileURL.lastPathComponent, + isStdout: stdout + ) } else { try initLogging(dir: nil, fileName: fileName, isStdout: stdout) } diff --git a/Radroots/Runtime/Radroots.swift b/Radroots/Runtime/Radroots.swift @@ -15,7 +15,7 @@ public final class Radroots: ObservableObject { ) throws -> FieldRuntimeService { let settings = LoggingSettings.load() do { - try settings.apply() + try settings.apply(bundleIdentifier: bundleId) } catch { try? initLoggingStdout() }