Files
honeyDueKMP/iosApp/iosApp/Residence/ResidenceSharingManager.swift
Trey t c334ce0bd0 Add PostHog analytics integration for Android and iOS
Implement comprehensive analytics tracking across both platforms:

Android (Kotlin):
- Add PostHog SDK dependency and initialization in MainActivity
- Create expect/actual pattern for cross-platform analytics (commonMain/androidMain/iosMain/jvmMain/jsMain/wasmJsMain)
- Track screen views: registration, login, residences, tasks, contractors, documents, notifications, profile
- Track key events: user_registered, user_signed_in, residence_created, task_created, contractor_created, document_created
- Track paywall events: contractor_paywall_shown, documents_paywall_shown
- Track sharing events: residence_shared, contractor_shared
- Track theme_changed event

iOS (Swift):
- Add PostHog iOS SDK via SPM
- Create PostHogAnalytics wrapper and AnalyticsEvents constants
- Initialize SDK in iOSApp with session replay support
- Track same screen views and events as Android
- Track user identification after login/registration

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-07 23:53:00 -06:00

211 lines
7.3 KiB
Swift

import Foundation
import ComposeApp
/// Manages residence share package creation and import via .casera files.
/// For residences, the share code is generated server-side (unlike contractors which are exported client-side).
@MainActor
class ResidenceSharingManager: ObservableObject {
// MARK: - Singleton
static let shared = ResidenceSharingManager()
// MARK: - Published Properties
/// True while generating a share package from the server
@Published var isGeneratingPackage: Bool = false
/// True while importing a residence from a share package
@Published var isImporting: Bool = false
/// Error message if generation or import fails
@Published var errorMessage: String?
/// True after successful import
@Published var importSuccess: Bool = false
/// Name of the imported residence
@Published var importedResidenceName: String?
// MARK: - Private Properties
private let jsonEncoder: JSONEncoder = {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
return encoder
}()
private let jsonDecoder = JSONDecoder()
private init() {}
// MARK: - Export (Share)
/// Creates a shareable .casera file for a residence.
/// This calls the backend to generate a one-time share code, then packages it.
/// - Parameter residence: The residence to share
/// - Returns: URL to the temporary file, or nil if creation failed
func createShareableFile(residence: ResidenceResponse) async -> URL? {
isGeneratingPackage = true
errorMessage = nil
defer { isGeneratingPackage = false }
// Call API to generate share package
let result: Any
do {
result = try await APILayer.shared.generateSharePackage(residenceId: residence.id)
} catch {
errorMessage = "Failed to generate share code: \(error.localizedDescription)"
return nil
}
guard let success = result as? ApiResultSuccess<SharedResidence>,
let sharedResidence = success.data else {
if let error = result as? ApiResultError {
errorMessage = ErrorMessageParser.parse(error.message)
} else {
errorMessage = "Failed to generate share code"
}
return nil
}
// Create Swift-compatible structure for JSON encoding
let exportData = SharedResidenceExport(from: sharedResidence)
guard let jsonData = try? jsonEncoder.encode(exportData) else {
print("ResidenceSharingManager: Failed to encode residence to JSON")
errorMessage = "Failed to create share file"
return nil
}
// Create a safe filename
let safeName = residence.name
.replacingOccurrences(of: " ", with: "_")
.replacingOccurrences(of: "/", with: "-")
.prefix(50)
let fileName = "\(safeName).casera"
let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(fileName)
do {
try jsonData.write(to: tempURL)
// Track residence shared event
PostHogAnalytics.shared.capture(AnalyticsEvents.residenceShared, properties: ["method": "file"])
return tempURL
} catch {
print("ResidenceSharingManager: Failed to write .casera file: \(error)")
errorMessage = "Failed to save share file"
return nil
}
}
// MARK: - Import
/// Imports a residence share from a .casera file URL.
/// This validates the share code with the server and adds the user to the residence.
/// - Parameters:
/// - url: The URL to the .casera file
/// - completion: Called with true on success, false on failure
func importResidence(from url: URL, completion: @escaping (Bool) -> Void) {
isImporting = true
errorMessage = nil
// Verify user is authenticated
guard TokenStorage.shared.getToken() != nil else {
errorMessage = "You must be logged in to join a residence"
isImporting = false
completion(false)
return
}
// Start accessing security-scoped resource if needed
let accessing = url.startAccessingSecurityScopedResource()
defer {
if accessing {
url.stopAccessingSecurityScopedResource()
}
}
do {
let data = try Data(contentsOf: url)
let exportData = try jsonDecoder.decode(SharedResidenceExport.self, from: data)
// Use the share code to join the residence
Task {
do {
let result = try await APILayer.shared.joinWithCode(code: exportData.shareCode)
if let success = result as? ApiResultSuccess<JoinResidenceResponse>,
let joinResponse = success.data {
self.importedResidenceName = joinResponse.residence.name
self.importSuccess = true
self.isImporting = false
completion(true)
} else if let error = result as? ApiResultError {
self.errorMessage = ErrorMessageParser.parse(error.message)
self.isImporting = false
completion(false)
} else {
self.errorMessage = "Unknown error occurred"
self.isImporting = false
completion(false)
}
} catch {
self.errorMessage = error.localizedDescription
self.isImporting = false
completion(false)
}
}
} catch {
errorMessage = "Failed to read residence share file: \(error.localizedDescription)"
isImporting = false
completion(false)
}
}
/// Resets the import state after showing success/error feedback
func resetState() {
errorMessage = nil
importSuccess = false
importedResidenceName = nil
}
}
// MARK: - Swift Codable Structure
/// Swift-native Codable structure for .casera residence share format.
/// This mirrors the Kotlin SharedResidence model for JSON serialization.
struct SharedResidenceExport: Codable {
let version: Int
let type: String
let shareCode: String
let residenceName: String
let sharedBy: String?
let expiresAt: String?
let exportedAt: String?
let exportedBy: String?
enum CodingKeys: String, CodingKey {
case version
case type
case shareCode = "share_code"
case residenceName = "residence_name"
case sharedBy = "shared_by"
case expiresAt = "expires_at"
case exportedAt = "exported_at"
case exportedBy = "exported_by"
}
/// Initialize from Kotlin SharedResidence
init(from sharedResidence: SharedResidence) {
self.version = Int(sharedResidence.version)
self.type = sharedResidence.type
self.shareCode = sharedResidence.shareCode
self.residenceName = sharedResidence.residenceName
self.sharedBy = sharedResidence.sharedBy
self.expiresAt = sharedResidence.expiresAt
self.exportedAt = sharedResidence.exportedAt
self.exportedBy = sharedResidence.exportedBy
}
}