initial commit: Proximity iOS app proposal, full Swift code scaffold, reference backend, testing docs, generated Xcode project
This commit is contained in:
116
Proximity/Services/Auth/ProfileImportService.swift
Normal file
116
Proximity/Services/Auth/ProfileImportService.swift
Normal file
@@ -0,0 +1,116 @@
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
/// Imports a user's **social profile** (Instagram, X) to enrich a revealed
|
||||
/// connection.
|
||||
///
|
||||
/// **Critical privacy boundary:** This service is *never* used to build the
|
||||
/// user's own discoverable account. It is only invoked at **reveal time**,
|
||||
/// after a mutual encounter, when the user explicitly chooses to share a
|
||||
/// richer identity with someone they've actually met.
|
||||
///
|
||||
/// Two separate identities exist in Proximity:
|
||||
/// - **Account identity** (AuthService): who you are *to the app* — private.
|
||||
/// - **Social identity** (this service): what you *choose to reveal* — opt-in.
|
||||
///
|
||||
/// Importing a profile is like handing someone your business card after
|
||||
/// meeting them — not broadcasting it to the world.
|
||||
@MainActor
|
||||
final class ProfileImportService: ObservableObject {
|
||||
|
||||
@Published private(set) var importedProfiles: [ImportedProfile] = []
|
||||
|
||||
private let providers: [ProfileProviderID: ProfileProvider]
|
||||
|
||||
init(providers: [ProfileProviderID: ProfileProvider] = [
|
||||
.instagram: InstagramProfileProvider(),
|
||||
.x: XProfileProvider()
|
||||
]) {
|
||||
self.providers = providers
|
||||
}
|
||||
|
||||
/// Import a profile from a provider. Called only at reveal time.
|
||||
func importProfile(from providerID: ProfileProviderID) async throws -> ImportedProfile {
|
||||
guard let provider = providers[providerID] else {
|
||||
throw ProfileImportError.providerUnavailable
|
||||
}
|
||||
let profile = try await provider.fetchProfile()
|
||||
importedProfiles.append(profile)
|
||||
return profile
|
||||
}
|
||||
|
||||
/// Attach an imported profile to a revealed connection.
|
||||
/// This is the *only* place a social identity can be linked to a person.
|
||||
func attach(_ profile: ImportedProfile, to connectionID: UUID) {
|
||||
// In production: persist the link locally, E2E-encrypted.
|
||||
// The server never stores the raw social profile by default.
|
||||
}
|
||||
|
||||
/// Remove an imported profile (user revokes a shared identity).
|
||||
func remove(_ profile: ImportedProfile) {
|
||||
importedProfiles.removeAll { $0.id == profile.id }
|
||||
}
|
||||
}
|
||||
|
||||
/// A social profile imported at reveal time.
|
||||
struct ImportedProfile: Identifiable, Codable, Hashable {
|
||||
let id: UUID
|
||||
let providerID: ProfileProviderID
|
||||
let handle: String
|
||||
let displayName: String?
|
||||
let bio: String?
|
||||
let avatarURL: URL?
|
||||
let importedAt: Date
|
||||
|
||||
init(
|
||||
id: UUID = UUID(),
|
||||
providerID: ProfileProviderID,
|
||||
handle: String,
|
||||
displayName: String? = nil,
|
||||
bio: String? = nil,
|
||||
avatarURL: URL? = nil,
|
||||
importedAt: Date = Date()
|
||||
) {
|
||||
self.id = id
|
||||
self.providerID = providerID
|
||||
self.handle = handle
|
||||
self.displayName = displayName
|
||||
self.bio = bio
|
||||
self.avatarURL = avatarURL
|
||||
self.importedAt = importedAt
|
||||
}
|
||||
}
|
||||
|
||||
/// Providers that can supply a social profile.
|
||||
enum ProfileProviderID: String, Codable, CaseIterable {
|
||||
case instagram
|
||||
case x
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .instagram: return "Instagram"
|
||||
case .x: return "X"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors from profile import.
|
||||
enum ProfileImportError: Error, LocalizedError {
|
||||
case providerUnavailable
|
||||
case authorizationFailed
|
||||
case noProfile
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .providerUnavailable: return "This profile provider is unavailable."
|
||||
case .authorizationFailed: return "Authorization with the provider failed."
|
||||
case .noProfile: return "No profile was returned."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A provider capable of fetching a user's own social profile.
|
||||
protocol ProfileProvider {
|
||||
var providerID: ProfileProviderID { get }
|
||||
func fetchProfile() async throws -> ImportedProfile
|
||||
}
|
||||
Reference in New Issue
Block a user