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>
This commit is contained in:
@@ -131,7 +131,7 @@ struct ManageUsersView: View {
|
||||
let responseData = successResult.data as? [ResidenceUserResponse] {
|
||||
self.users = responseData
|
||||
self.isLoading = false
|
||||
} else if let errorResult = result as? ApiResultError {
|
||||
} else if let errorResult = ApiResultBridge.error(from: result) {
|
||||
self.errorMessage = ErrorMessageParser.parse(errorResult.message)
|
||||
self.isLoading = false
|
||||
} else {
|
||||
@@ -181,7 +181,7 @@ struct ManageUsersView: View {
|
||||
let response = successResult.data {
|
||||
self.shareCode = response.shareCode
|
||||
self.isGeneratingCode = false
|
||||
} else if let errorResult = result as? ApiResultError {
|
||||
} else if let errorResult = ApiResultBridge.error(from: result) {
|
||||
self.errorMessage = ErrorMessageParser.parse(errorResult.message)
|
||||
self.isGeneratingCode = false
|
||||
} else {
|
||||
@@ -209,7 +209,7 @@ struct ManageUsersView: View {
|
||||
if result is ApiResultSuccess<RemoveUserResponse> {
|
||||
// Remove user from local list
|
||||
self.users.removeAll { $0.id == userId }
|
||||
} else if let errorResult = result as? ApiResultError {
|
||||
} else if let errorResult = ApiResultBridge.error(from: result) {
|
||||
self.errorMessage = ErrorMessageParser.parse(errorResult.message)
|
||||
} else {
|
||||
self.errorMessage = "Failed to remove user"
|
||||
|
||||
@@ -246,6 +246,8 @@ private extension ResidenceDetailView {
|
||||
selectedTaskForComplete: $selectedTaskForComplete,
|
||||
selectedTaskForArchive: $selectedTaskForArchive,
|
||||
showArchiveConfirmation: $showArchiveConfirmation,
|
||||
selectedTaskForCancel: $selectedTaskForCancel,
|
||||
showCancelConfirmation: $showCancelConfirmation,
|
||||
reloadTasks: { loadResidenceTasks(forceRefresh: true) }
|
||||
)
|
||||
} else if isLoadingTasks {
|
||||
@@ -436,7 +438,7 @@ private extension ResidenceDetailView {
|
||||
|
||||
if result is ApiResultSuccess<KotlinUnit> {
|
||||
dismiss()
|
||||
} else if let errorResult = result as? ApiResultError {
|
||||
} else if let errorResult = ApiResultBridge.error(from: result) {
|
||||
self.viewModel.errorMessage = ErrorMessageParser.parse(errorResult.message)
|
||||
} else {
|
||||
self.viewModel.errorMessage = "Failed to delete residence"
|
||||
@@ -468,7 +470,7 @@ private extension ResidenceDetailView {
|
||||
if let successResult = result as? ApiResultSuccess<NSArray> {
|
||||
self.contractors = (successResult.data as? [ContractorSummary]) ?? []
|
||||
self.isLoadingContractors = false
|
||||
} else if let errorResult = result as? ApiResultError {
|
||||
} else if let errorResult = ApiResultBridge.error(from: result) {
|
||||
self.contractorsError = errorResult.message
|
||||
self.isLoadingContractors = false
|
||||
} else {
|
||||
@@ -495,6 +497,8 @@ private struct TasksSectionContainer: View {
|
||||
@Binding var selectedTaskForComplete: TaskResponse?
|
||||
@Binding var selectedTaskForArchive: TaskResponse?
|
||||
@Binding var showArchiveConfirmation: Bool
|
||||
@Binding var selectedTaskForCancel: TaskResponse?
|
||||
@Binding var showCancelConfirmation: Bool
|
||||
|
||||
let reloadTasks: () -> Void
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ class ResidenceSharingManager: ObservableObject {
|
||||
|
||||
guard let success = result as? ApiResultSuccess<SharedResidence>,
|
||||
let sharedResidence = success.data else {
|
||||
if let error = result as? ApiResultError {
|
||||
if let error = ApiResultBridge.error(from: result) {
|
||||
errorMessage = ErrorMessageParser.parse(error.message)
|
||||
} else {
|
||||
errorMessage = "Failed to generate share code"
|
||||
@@ -90,7 +90,7 @@ class ResidenceSharingManager: ObservableObject {
|
||||
do {
|
||||
try jsonData.write(to: tempURL)
|
||||
// Track residence shared event
|
||||
PostHogAnalytics.shared.capture(AnalyticsEvents.residenceShared, properties: ["method": "file"])
|
||||
AnalyticsManager.shared.track(.residenceShared(method: "file"))
|
||||
return tempURL
|
||||
} catch {
|
||||
print("ResidenceSharingManager: Failed to write .casera file: \(error)")
|
||||
@@ -141,7 +141,7 @@ class ResidenceSharingManager: ObservableObject {
|
||||
self.importSuccess = true
|
||||
self.isImporting = false
|
||||
completion(true)
|
||||
} else if let error = result as? ApiResultError {
|
||||
} else if let error = ApiResultBridge.error(from: result) {
|
||||
self.errorMessage = ErrorMessageParser.parse(error.message)
|
||||
self.isImporting = false
|
||||
completion(false)
|
||||
|
||||
@@ -80,7 +80,7 @@ class ResidenceViewModel: ObservableObject {
|
||||
let result = try await APILayer.shared.getSummary(forceRefresh: forceRefresh)
|
||||
|
||||
// Only handle errors - success updates DataManager automatically
|
||||
if let error = result as? ApiResultError {
|
||||
if let error = ApiResultBridge.error(from: result) {
|
||||
self.errorMessage = ErrorMessageParser.parse(error.message)
|
||||
}
|
||||
self.isLoading = false
|
||||
@@ -110,7 +110,7 @@ class ResidenceViewModel: ObservableObject {
|
||||
let result = try await APILayer.shared.getMyResidences(forceRefresh: forceRefresh)
|
||||
|
||||
// Only handle errors - success updates DataManager automatically
|
||||
if let error = result as? ApiResultError {
|
||||
if let error = ApiResultBridge.error(from: result) {
|
||||
self.errorMessage = ErrorMessageParser.parse(error.message)
|
||||
self.isLoading = false
|
||||
}
|
||||
@@ -132,9 +132,12 @@ class ResidenceViewModel: ObservableObject {
|
||||
if let success = result as? ApiResultSuccess<ResidenceResponse> {
|
||||
self.selectedResidence = success.data
|
||||
self.isLoading = false
|
||||
} else if let error = result as? ApiResultError {
|
||||
} 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)
|
||||
@@ -172,7 +175,7 @@ class ResidenceViewModel: ObservableObject {
|
||||
self.isLoading = false
|
||||
completion(nil)
|
||||
}
|
||||
} else if let error = result as? ApiResultError {
|
||||
} else if let error = ApiResultBridge.error(from: result) {
|
||||
print("🏠 ResidenceVM: Is ApiResultError: \(error.message ?? "nil")")
|
||||
self.errorMessage = ErrorMessageParser.parse(error.message)
|
||||
self.isLoading = false
|
||||
@@ -209,10 +212,14 @@ class ResidenceViewModel: ObservableObject {
|
||||
// which updates DataManagerObservable, which updates our @Published
|
||||
// myResidences via Combine subscription
|
||||
completion(true)
|
||||
} else if let error = result as? ApiResultError {
|
||||
} 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)
|
||||
@@ -233,9 +240,12 @@ class ResidenceViewModel: ObservableObject {
|
||||
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 = result as? ApiResultError {
|
||||
} 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)
|
||||
@@ -267,10 +277,14 @@ class ResidenceViewModel: ObservableObject {
|
||||
// which updates DataManagerObservable, which updates our
|
||||
// @Published myResidences via Combine subscription
|
||||
completion(true)
|
||||
} else if let error = result as? ApiResultError {
|
||||
} 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)
|
||||
|
||||
@@ -8,6 +8,8 @@ struct ResidencesListView: View {
|
||||
@State private var showingJoinResidence = false
|
||||
@State private var showingUpgradePrompt = false
|
||||
@State private var showingSettings = false
|
||||
@State private var pushTargetResidenceId: Int32?
|
||||
@State private var navigateToPushResidence = false
|
||||
@StateObject private var authManager = AuthenticationManager.shared
|
||||
@StateObject private var subscriptionCache = SubscriptionCacheWrapper.shared
|
||||
@Environment(\.scenePhase) private var scenePhase
|
||||
@@ -103,7 +105,10 @@ struct ResidencesListView: View {
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
PostHogAnalytics.shared.screen(AnalyticsEvents.residenceScreenShown)
|
||||
AnalyticsManager.shared.trackScreen(.residences)
|
||||
if let pendingResidenceId = PushNotificationManager.shared.pendingNavigationResidenceId {
|
||||
navigateToResidenceFromPush(residenceId: pendingResidenceId)
|
||||
}
|
||||
if authManager.isAuthenticated {
|
||||
viewModel.loadMyResidences()
|
||||
// Also load tasks to populate summary stats
|
||||
@@ -135,6 +140,32 @@ struct ResidencesListView: View {
|
||||
viewModel.myResidences = nil
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .navigateToResidence)) { notification in
|
||||
if let residenceId = notification.userInfo?["residenceId"] as? Int {
|
||||
navigateToResidenceFromPush(residenceId: residenceId)
|
||||
}
|
||||
}
|
||||
.background(
|
||||
NavigationLink(
|
||||
destination: Group {
|
||||
if let residenceId = pushTargetResidenceId {
|
||||
ResidenceDetailView(residenceId: residenceId)
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
},
|
||||
isActive: $navigateToPushResidence
|
||||
) {
|
||||
EmptyView()
|
||||
}
|
||||
.hidden()
|
||||
)
|
||||
}
|
||||
|
||||
private func navigateToResidenceFromPush(residenceId: Int) {
|
||||
pushTargetResidenceId = Int32(residenceId)
|
||||
navigateToPushResidence = true
|
||||
PushNotificationManager.shared.pendingNavigationResidenceId = nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user