Files
Nearfield_Friends/Proximity/Services/Auth/ProfileImportService.swift

117 lines
3.7 KiB
Swift
Raw Permalink Normal View History

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
}