initial commit: Proximity iOS app proposal, full Swift code scaffold, reference backend, testing docs, generated Xcode project

This commit is contained in:
2026-08-12 00:22:01 +00:00
commit 9d1b0f8dd9
52 changed files with 6271 additions and 0 deletions

View File

@@ -0,0 +1,171 @@
import Foundation
import Combine
/// Root application state. Owns the long-lived services and wires them together.
///
/// The central philosophy: **Presence is foreground-first.** The app is not a
/// passive background listener it is an active instrument you hold while you
/// are out in the world. This is a feature, not a limitation.
@MainActor
final class AppState: ObservableObject {
// MARK: - Published state
/// Whether the user is currently "open to connection" and the app is
/// actively broadcasting + listening. This is the master switch.
@Published var isPresent: Bool = false
/// The user's chosen radius tier. Defaults to "right here" (UWB).
@Published var radiusTier: DistanceTier = .rightHere
/// The current proximity engine (UWB / BLE / beacon) in use.
@Published private(set) var activeEngine: ProximityEngine = .uwb
// MARK: - Debug / simulation state
#if DEBUG
/// Whether we're using the simulated adapter (no hardware).
@Published var isSimulated = true
/// When true, the simulated adapter emits nearby users on a timer.
@Published var autoBroadcast = false {
didSet { configureSimulation() }
}
let simulatedAdapter = SimulatedAdapter()
#endif
// MARK: - Services
let presenceEngine: PresenceEngine
let tokenManager: TokenManager
let cryptoManager: CryptoManager
let apiClient: APIClient
let authService: AuthService
let profileImportService: ProfileImportService
let graphStore: GraphStore
let realtime: RealtimeClient
let revealFlow: RevealFlow
let chatViewModel: ChatViewModel
let presenceViewModel: PresenceViewModel
private var cancellables = Set<AnyCancellable>()
init() {
let tokenManager = TokenManager()
let cryptoManager = CryptoManager()
#if DEBUG
// In the simulator, point at the local reference server.
let apiClient = APIClient.local()
#else
let apiClient = APIClient()
#endif
self.tokenManager = tokenManager
self.cryptoManager = cryptoManager
self.apiClient = apiClient
let authService = AuthService(apiClient: apiClient)
self.authService = authService
let profileImportService = ProfileImportService()
self.profileImportService = profileImportService
let graphStore = GraphStore()
self.graphStore = graphStore
let realtime = RealtimeClient()
self.realtime = realtime
let revealFlow = RevealFlow(
apiClient: apiClient,
cryptoManager: cryptoManager,
profileService: profileImportService,
graphStore: graphStore,
realtime: realtime
)
self.revealFlow = revealFlow
let chatViewModel = ChatViewModel(
apiClient: apiClient,
cryptoManager: cryptoManager,
graphStore: graphStore,
realtime: realtime
)
self.chatViewModel = chatViewModel
let presenceEngine: PresenceEngine
#if DEBUG
// In DEBUG (simulator), drive the engine with the simulated adapter
// so the whole interaction loop can be tested without hardware.
presenceEngine = PresenceEngine(
tokenManager: tokenManager,
cryptoManager: cryptoManager,
apiClient: apiClient,
uwbAdapter: simulatedAdapter,
bleAdapter: simulatedAdapter,
beaconAdapter: simulatedAdapter
)
#else
presenceEngine = PresenceEngine(
tokenManager: tokenManager,
cryptoManager: cryptoManager,
apiClient: apiClient
)
#endif
self.presenceEngine = presenceEngine
self.presenceViewModel = PresenceViewModel(presenceEngine: presenceEngine)
// When the user toggles presence, drive the engine and open the
// live socket so mutual waves and messages arrive in real time.
$isPresent
.sink { [weak presenceEngine, weak realtime] present in
Task {
await presenceEngine?.setPresent(present)
if present {
realtime?.connect(token: tokenManager.currentToken())
} else {
realtime?.disconnect()
}
}
}
.store(in: &cancellables)
$radiusTier
.sink { [weak presenceEngine] tier in
Task { await presenceEngine?.setRadiusTier(tier) }
}
.store(in: &cancellables)
#if DEBUG
configureSimulation()
#endif
}
// MARK: - Debug / simulation
#if DEBUG
/// Forward the simulated adapter's events into the engine and set up
/// auto-broadcast when enabled.
private func configureSimulation() {
simulatedAdapter.autoBroadcast = autoBroadcast
// The engine already wired onEncounter/onBroadcast/onRegion to the
// simulated adapter at init. Auto-broadcast is handled internally.
}
/// Used by the debug menu to simulate a single nearby user.
func simulateEncounter() {
simulatedAdapter.simulateEncounter(remoteToken: "sim-\(Int.random(in: 100_000...999_999))")
}
/// Used by the debug menu to simulate a crowd.
func simulateCrowd() {
simulatedAdapter.simulateCrowd(count: 20)
}
/// Reconnect the realtime socket (e.g. after starting the local server).
func restartRealtime() {
realtime.connect(token: tokenManager.currentToken())
}
#endif
}

View File

@@ -0,0 +1,41 @@
import SwiftUI
@main
struct ProximityApp: App {
@StateObject private var appState = AppState()
var body: some Scene {
WindowGroup {
RootView()
.environmentObject(appState)
.environmentObject(appState.presenceViewModel)
.environmentObject(appState.authService)
.environmentObject(appState.profileImportService)
.environmentObject(appState.revealFlow)
.environmentObject(appState.chatViewModel)
.environmentObject(appState.realtime)
}
}
}
/// Routes between the sign-in flow and the main app based on auth state.
struct RootView: View {
@EnvironmentObject private var appState: AppState
@EnvironmentObject private var authService: AuthService
var body: some View {
Group {
switch authService.state {
case .signedOut:
SignInView()
case .authenticating:
ProgressView("Signing in…")
case .authenticated:
ContentView()
}
}
.task {
await authService.restoreSession()
}
}
}