Remove old PostHogAnalytics singleton and replace with guide-based two-file architecture: AnalyticsManager (singleton wrapper with super properties, session replay, opt-out, subscription funnel) and AnalyticsEvent (type-safe enum with associated values). Key changes: - New API key, self-hosted analytics endpoint - All 19 events ported to type-safe AnalyticsEvent enum - Screen tracking via AnalyticsManager.Screen enum + SwiftUI modifier - Remove all identify() calls — fully anonymous analytics - Add lifecycle hooks: flush on background, update super properties on foreground - Add privacy opt-out toggle in Settings - Subscription funnel methods ready for IAP integration Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
297 lines
12 KiB
Swift
297 lines
12 KiB
Swift
import Foundation
|
|
import ComposeApp
|
|
import Combine
|
|
|
|
/// ViewModel for residence management.
|
|
/// Observes DataManagerObservable for cached data.
|
|
/// Kicks off API calls that update DataManager, letting views react to cache updates.
|
|
@MainActor
|
|
class ResidenceViewModel: ObservableObject {
|
|
// MARK: - Published Properties (from DataManager observation)
|
|
@Published var myResidences: MyResidencesResponse?
|
|
@Published var residences: [ResidenceResponse] = []
|
|
@Published var totalSummary: TotalSummary?
|
|
|
|
// MARK: - Local State
|
|
@Published var selectedResidence: ResidenceResponse?
|
|
@Published var isLoading: Bool = false
|
|
@Published var errorMessage: String?
|
|
@Published var isGeneratingReport: Bool = false
|
|
@Published var reportMessage: String?
|
|
|
|
// MARK: - Private Properties
|
|
private var cancellables = Set<AnyCancellable>()
|
|
|
|
// MARK: - Initialization
|
|
init() {
|
|
// Observe DataManagerObservable for residence data
|
|
DataManagerObservable.shared.$myResidences
|
|
.receive(on: DispatchQueue.main)
|
|
.sink { [weak self] myResidences in
|
|
self?.myResidences = myResidences
|
|
// Clear loading state when data arrives
|
|
if myResidences != nil {
|
|
self?.isLoading = false
|
|
}
|
|
// Auto-update selectedResidence if it exists in the updated list
|
|
if let currentSelected = self?.selectedResidence,
|
|
let updatedResidence = myResidences?.residences.first(where: { $0.id == currentSelected.id }) {
|
|
self?.selectedResidence = updatedResidence
|
|
}
|
|
}
|
|
.store(in: &cancellables)
|
|
|
|
DataManagerObservable.shared.$residences
|
|
.receive(on: DispatchQueue.main)
|
|
.sink { [weak self] residences in
|
|
self?.residences = residences
|
|
// Auto-update selectedResidence if it exists in the updated list
|
|
if let currentSelected = self?.selectedResidence,
|
|
let updatedResidence = residences.first(where: { $0.id == currentSelected.id }) {
|
|
self?.selectedResidence = updatedResidence
|
|
}
|
|
}
|
|
.store(in: &cancellables)
|
|
|
|
DataManagerObservable.shared.$totalSummary
|
|
.receive(on: DispatchQueue.main)
|
|
.sink { [weak self] summary in
|
|
self?.totalSummary = summary
|
|
}
|
|
.store(in: &cancellables)
|
|
}
|
|
|
|
// MARK: - Public Methods
|
|
|
|
/// Load summary - kicks off API call that updates DataManager
|
|
func loadSummary(forceRefresh: Bool = false) {
|
|
errorMessage = nil
|
|
|
|
// Check if we have cached data and don't need to refresh
|
|
if !forceRefresh && totalSummary != nil {
|
|
return
|
|
}
|
|
|
|
isLoading = true
|
|
|
|
// Kick off API call - DataManager will be updated, which updates DataManagerObservable
|
|
Task {
|
|
do {
|
|
let result = try await APILayer.shared.getSummary(forceRefresh: forceRefresh)
|
|
|
|
// Only handle errors - success updates DataManager automatically
|
|
if let error = ApiResultBridge.error(from: result) {
|
|
self.errorMessage = ErrorMessageParser.parse(error.message)
|
|
}
|
|
self.isLoading = false
|
|
} catch {
|
|
self.errorMessage = ErrorMessageParser.parse(error.localizedDescription)
|
|
self.isLoading = false
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Load my residences - checks cache first, then fetches if needed
|
|
func loadMyResidences(forceRefresh: Bool = false) {
|
|
errorMessage = nil
|
|
|
|
// Check if we have cached data and don't need to refresh
|
|
if !forceRefresh && DataManagerObservable.shared.myResidences != nil {
|
|
// Data already available via observation, no API call needed
|
|
return
|
|
}
|
|
|
|
isLoading = true
|
|
|
|
// Kick off API call - DataManager will be updated, which updates DataManagerObservable,
|
|
// which updates our @Published myResidences via the sink above
|
|
Task {
|
|
do {
|
|
let result = try await APILayer.shared.getMyResidences(forceRefresh: forceRefresh)
|
|
|
|
// Only handle errors - success updates DataManager automatically
|
|
if let error = ApiResultBridge.error(from: result) {
|
|
self.errorMessage = ErrorMessageParser.parse(error.message)
|
|
self.isLoading = false
|
|
}
|
|
} catch {
|
|
self.errorMessage = ErrorMessageParser.parse(error.localizedDescription)
|
|
self.isLoading = false
|
|
}
|
|
}
|
|
}
|
|
|
|
func getResidence(id: Int32) {
|
|
isLoading = true
|
|
errorMessage = nil
|
|
|
|
Task {
|
|
do {
|
|
let result = try await APILayer.shared.getResidence(id: id, forceRefresh: false)
|
|
|
|
if let success = result as? ApiResultSuccess<ResidenceResponse> {
|
|
self.selectedResidence = success.data
|
|
self.isLoading = false
|
|
} else if let error = ApiResultBridge.error(from: result) {
|
|
self.errorMessage = ErrorMessageParser.parse(error.message)
|
|
self.isLoading = false
|
|
} else {
|
|
self.errorMessage = "Failed to load residence"
|
|
self.isLoading = false
|
|
}
|
|
} catch {
|
|
self.errorMessage = ErrorMessageParser.parse(error.localizedDescription)
|
|
self.isLoading = false
|
|
}
|
|
}
|
|
}
|
|
|
|
func createResidence(request: ResidenceCreateRequest, completion: @escaping (Bool) -> Void) {
|
|
createResidence(request: request) { result in
|
|
completion(result != nil)
|
|
}
|
|
}
|
|
|
|
/// Creates a residence and returns the created residence on success
|
|
func createResidence(request: ResidenceCreateRequest, completion: @escaping (ResidenceResponse?) -> Void) {
|
|
isLoading = true
|
|
errorMessage = nil
|
|
|
|
Task {
|
|
do {
|
|
print("🏠 ResidenceVM: Calling API...")
|
|
let result = try await APILayer.shared.createResidence(request: request)
|
|
print("🏠 ResidenceVM: Got result: \(String(describing: result))")
|
|
|
|
await MainActor.run {
|
|
if let success = result as? ApiResultSuccess<ResidenceResponse> {
|
|
print("🏠 ResidenceVM: Is ApiResultSuccess, data = \(String(describing: success.data))")
|
|
if let residence = success.data {
|
|
print("🏠 ResidenceVM: Got residence with id \(residence.id)")
|
|
self.isLoading = false
|
|
completion(residence)
|
|
} else {
|
|
print("🏠 ResidenceVM: success.data is nil")
|
|
self.isLoading = false
|
|
completion(nil)
|
|
}
|
|
} else if let error = ApiResultBridge.error(from: result) {
|
|
print("🏠 ResidenceVM: Is ApiResultError: \(error.message ?? "nil")")
|
|
self.errorMessage = ErrorMessageParser.parse(error.message)
|
|
self.isLoading = false
|
|
completion(nil)
|
|
} else {
|
|
print("🏠 ResidenceVM: Unknown result type: \(type(of: result))")
|
|
self.isLoading = false
|
|
completion(nil)
|
|
}
|
|
}
|
|
} catch {
|
|
print("🏠 ResidenceVM: Exception: \(error)")
|
|
await MainActor.run {
|
|
self.errorMessage = ErrorMessageParser.parse(error.localizedDescription)
|
|
self.isLoading = false
|
|
completion(nil)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func updateResidence(id: Int32, request: ResidenceCreateRequest, completion: @escaping (Bool) -> Void) {
|
|
isLoading = true
|
|
errorMessage = nil
|
|
|
|
Task {
|
|
do {
|
|
let result = try await APILayer.shared.updateResidence(id: id, request: request)
|
|
|
|
if let success = result as? ApiResultSuccess<ResidenceResponse> {
|
|
self.selectedResidence = success.data
|
|
self.isLoading = false
|
|
// DataManager is updated by APILayer (including refreshMyResidences),
|
|
// which updates DataManagerObservable, which updates our @Published
|
|
// myResidences via Combine subscription
|
|
completion(true)
|
|
} else if let error = ApiResultBridge.error(from: result) {
|
|
self.errorMessage = ErrorMessageParser.parse(error.message)
|
|
self.isLoading = false
|
|
completion(false)
|
|
} else {
|
|
self.errorMessage = "Failed to update residence"
|
|
self.isLoading = false
|
|
completion(false)
|
|
}
|
|
} catch {
|
|
self.errorMessage = ErrorMessageParser.parse(error.localizedDescription)
|
|
self.isLoading = false
|
|
completion(false)
|
|
}
|
|
}
|
|
}
|
|
|
|
func generateTasksReport(residenceId: Int32, email: String? = nil) {
|
|
isGeneratingReport = true
|
|
reportMessage = nil
|
|
|
|
Task {
|
|
do {
|
|
let result = try await APILayer.shared.generateTasksReport(residenceId: residenceId, email: email)
|
|
|
|
if let success = result as? ApiResultSuccess<GenerateReportResponse> {
|
|
self.reportMessage = success.data?.message ?? "Report generated, but no message returned."
|
|
self.isGeneratingReport = false
|
|
} else if let error = ApiResultBridge.error(from: result) {
|
|
self.reportMessage = ErrorMessageParser.parse(error.message)
|
|
self.isGeneratingReport = false
|
|
} else {
|
|
self.reportMessage = "Failed to generate report"
|
|
self.isGeneratingReport = false
|
|
}
|
|
} catch {
|
|
self.reportMessage = ErrorMessageParser.parse(error.localizedDescription)
|
|
self.isGeneratingReport = false
|
|
}
|
|
}
|
|
}
|
|
|
|
func clearError() {
|
|
errorMessage = nil
|
|
}
|
|
|
|
func loadResidenceContractors(residenceId: Int32) {
|
|
// This can now be handled directly via APILayer if needed
|
|
// or through DataManagerObservable.shared.contractors
|
|
}
|
|
|
|
func joinWithCode(code: String, completion: @escaping (Bool) -> Void) {
|
|
isLoading = true
|
|
errorMessage = nil
|
|
|
|
Task {
|
|
do {
|
|
let result = try await APILayer.shared.joinWithCode(code: code)
|
|
|
|
if result is ApiResultSuccess<JoinResidenceResponse> {
|
|
self.isLoading = false
|
|
// APILayer updates DataManager with refreshMyResidences,
|
|
// which updates DataManagerObservable, which updates our
|
|
// @Published myResidences via Combine subscription
|
|
completion(true)
|
|
} else if let error = ApiResultBridge.error(from: result) {
|
|
self.errorMessage = ErrorMessageParser.parse(error.message)
|
|
self.isLoading = false
|
|
completion(false)
|
|
} else {
|
|
self.errorMessage = "Failed to join residence"
|
|
self.isLoading = false
|
|
completion(false)
|
|
}
|
|
} catch {
|
|
self.errorMessage = ErrorMessageParser.parse(error.localizedDescription)
|
|
self.isLoading = false
|
|
completion(false)
|
|
}
|
|
}
|
|
}
|
|
}
|