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,102 @@
import Foundation
import Combine
/// Orchestrates authentication and owns the authenticated session.
///
/// **Philosophy:** Auth establishes *account* identity (who you are to the
/// app) it never, by itself, reveals anything to other users. Social
/// identity is a separate, opt-in concern handled by `ProfileImportService`
/// at reveal time. This separation is what keeps the "anonymous until mutual
/// consent" promise intact.
@MainActor
final class AuthService: ObservableObject {
@Published private(set) var state: AuthState = .signedOut
@Published private(set) var account: Account?
private let providers: [AuthProviderID: AuthProvider]
private let apiClient: APIClient
private let keychain: KeychainStore
init(
apiClient: APIClient,
keychain: KeychainStore = KeychainStore(),
providers: [AuthProviderID: AuthProvider] = [
.apple: AppleAuthProvider(),
.google: GoogleAuthProvider()
]
) {
self.apiClient = apiClient
self.keychain = keychain
self.providers = providers
}
/// Restore a previously authenticated session from the keychain.
func restoreSession() async {
guard let session = keychain.loadSession() else { return }
state = .authenticated(session)
account = session.account
}
/// Sign in with a given provider.
func signIn(with providerID: AuthProviderID) async throws {
guard let provider = providers[providerID] else {
throw AuthError.providerUnavailable
}
state = .authenticating
do {
let result = try await provider.signIn()
// Exchange the provider ID token for a Proximity session token.
let session = try await apiClient.exchangeToken(result)
keychain.save(session)
state = .authenticated(session)
account = session.account
} catch {
state = .signedOut
throw error
}
}
/// Sign out locally and revoke the backend session.
func signOut() async {
if case let .authenticated(session) = state {
await apiClient.revokeSession(session)
}
keychain.clearSession()
state = .signedOut
account = nil
}
}
/// The user's authenticated account (account identity only).
struct Account: Codable, Hashable {
let id: String
let providerID: AuthProviderID
let displayName: String
let email: String?
}
/// The session state machine.
enum AuthState: Equatable {
case signedOut
case authenticating
case authenticated(Session)
static func == (lhs: AuthState, rhs: AuthState) -> Bool {
switch (lhs, rhs) {
case (.signedOut, .signedOut): return true
case (.authenticating, .authenticating): return true
case (.authenticated(let a), .authenticated(let b)): return a.token == b.token
default: return false
}
}
}
/// A verified Proximity session.
struct Session: Codable {
let token: String
let account: Account
}