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 }