import Foundation /// State management for the App Clip card display and save flow. @MainActor @Observable final class ClipCardStore { enum State { case loading case loaded(SharedCardSnapshot) case saved case error(String) } private let cloudKit: ClipCloudKitService private let contactSave: ContactSaveService var state: State = .loading /// The currently loaded card snapshot, if any. var snapshot: SharedCardSnapshot? { if case .loaded(let snap) = state { return snap } return nil } init( cloudKit: ClipCloudKitService = ClipCloudKitService(), contactSave: ContactSaveService = ContactSaveService() ) { self.cloudKit = cloudKit self.contactSave = contactSave } /// Loads a shared card from CloudKit. /// - Parameter recordName: The record name (UUID) to fetch. func load(recordName: String) async { state = .loading do { let snapshot = try await cloudKit.fetchSharedCard(recordName: recordName) state = .loaded(snapshot) } catch { state = .error(userMessage(for: error, fallback: ClipError.fetchFailed)) } } /// Saves the currently loaded card to Contacts via CNContactStore. /// Note: App Clips cannot use CNContactStore; use the share sheet flow instead. func saveToContacts() async { guard let snapshot else { return } do { try await contactSave.saveContact(vCardData: snapshot.vCardData) try? await cloudKit.deleteSharedCard(recordName: snapshot.recordName) state = .saved } catch { state = .error(userMessage(for: error, fallback: ClipError.contactSaveFailed)) } } /// Best-effort cleanup after user shares the vCard (e.g. via share sheet). /// App Clips use the share sheet instead of CNContactStore. func cleanupAfterShare() async { guard let snapshot else { return } try? await cloudKit.deleteSharedCard(recordName: snapshot.recordName) } private func userMessage(for error: Error, fallback: ClipError) -> String { if let clipError = error as? ClipError { return clipError.localizedDescription } return fallback.localizedDescription } }