BusinessCard/BusinessCardClip/State/ClipCardStore.swift

73 lines
2.3 KiB
Swift

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
}
}