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:
Trey t
2026-02-11 09:48:49 -06:00
parent 09be5fa444
commit 2fc4a48fc9
50 changed files with 2191 additions and 335 deletions

View File

@@ -73,12 +73,12 @@ class DocumentViewModelWrapper: ObservableObject {
Task {
do {
let result = try await APILayer.shared.getDocuments(
residenceId: residenceId != nil ? KotlinInt(int: residenceId!) : nil,
residenceId: residenceId.asKotlin,
documentType: documentType,
category: category,
contractorId: contractorId != nil ? KotlinInt(int: contractorId!) : nil,
isActive: isActive != nil ? KotlinBoolean(bool: isActive!) : nil,
expiringSoon: expiringSoon != nil ? KotlinInt(int: expiringSoon!) : nil,
contractorId: contractorId.asKotlin,
isActive: isActive.asKotlin,
expiringSoon: expiringSoon.asKotlin,
tags: tags,
search: search,
forceRefresh: false
@@ -88,8 +88,10 @@ class DocumentViewModelWrapper: ObservableObject {
if let success = result as? ApiResultSuccess<NSArray> {
let documents = success.data as? [Document] ?? []
self.documentsState = DocumentStateSuccess(documents: documents)
} else if let error = result as? ApiResultError {
} else if let error = ApiResultBridge.error(from: result) {
self.documentsState = DocumentStateError(message: error.message)
} else {
self.documentsState = DocumentStateError(message: "Failed to load documents")
}
}
} catch {
@@ -112,8 +114,10 @@ class DocumentViewModelWrapper: ObservableObject {
await MainActor.run {
if let success = result as? ApiResultSuccess<Document>, let document = success.data {
self.documentDetailState = DocumentDetailStateSuccess(document: document)
} else if let error = result as? ApiResultError {
} else if let error = ApiResultBridge.error(from: result) {
self.documentDetailState = DocumentDetailStateError(message: error.message)
} else {
self.documentDetailState = DocumentDetailStateError(message: "Failed to load document")
}
}
} catch {
@@ -179,8 +183,10 @@ class DocumentViewModelWrapper: ObservableObject {
self.updateState = UpdateStateSuccess(document: document)
// Also refresh the detail state
self.documentDetailState = DocumentDetailStateSuccess(document: document)
} else if let error = result as? ApiResultError {
} else if let error = ApiResultBridge.error(from: result) {
self.updateState = UpdateStateError(message: error.message)
} else {
self.updateState = UpdateStateError(message: "Failed to update document")
}
}
} catch {
@@ -203,8 +209,10 @@ class DocumentViewModelWrapper: ObservableObject {
await MainActor.run {
if result is ApiResultSuccess<KotlinUnit> {
self.deleteState = DeleteStateSuccess()
} else if let error = result as? ApiResultError {
} else if let error = ApiResultBridge.error(from: result) {
self.deleteState = DeleteStateError(message: error.message)
} else {
self.deleteState = DeleteStateError(message: "Failed to delete document")
}
}
} catch {
@@ -239,8 +247,10 @@ class DocumentViewModelWrapper: ObservableObject {
await MainActor.run {
if result is ApiResultSuccess<KotlinUnit> {
self.deleteImageState = DeleteImageStateSuccess()
} else if let error = result as? ApiResultError {
} else if let error = ApiResultBridge.error(from: result) {
self.deleteImageState = DeleteImageStateError(message: error.message)
} else {
self.deleteImageState = DeleteImageStateError(message: "Failed to delete image")
}
}
} catch {