Files
honeyDueKMP/iosApp/iosApp/Residence/ResidenceViewModel.swift
Trey t 2fc4a48fc9 Replace PostHog integration with AnalyticsManager architecture
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>
2026-02-11 09:48:49 -06:00

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