Files
Reflect/Shared/FeelsApp.swift
Trey t a08d0d33c0 Add PostHog analytics to replace removed Firebase
Wire PostHog iOS SDK into existing EventLogger pattern so all 60+
call sites flow to self-hosted PostHog instance with zero changes.
Sets subscription person properties for segmentation on foreground.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 13:57:30 -06:00

162 lines
6.4 KiB
Swift

//
// FeelsApp.swift
// Shared
//
// Created by Trey Tartt on 1/5/22.
//
import SwiftUI
import SwiftData
import BackgroundTasks
import WidgetKit
import PostHog
@main
struct FeelsApp: App {
@Environment(\.scenePhase) private var scenePhase
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
let dataController = DataController.shared
@StateObject var iapManager = IAPManager.shared
@StateObject var authManager = BiometricAuthManager()
@StateObject var healthKitManager = HealthKitManager.shared
@AppStorage(UserDefaultsStore.Keys.firstLaunchDate.rawValue, store: GroupUserDefaults.groupDefaults) private var firstLaunchDate = Date()
@State private var showSubscriptionFromWidget = false
init() {
// Initialize PostHog analytics
let posthogConfig = PostHogConfig(apiKey: "phc_3GsB3oqNft8Ykg2bJfE9MaJktzLAwr2EPMXQgwEFzAs", host: "https://analytics.88oakapps.com")
#if DEBUG
posthogConfig.debug = true
#endif
PostHogSDK.shared.setup(posthogConfig)
BGTaskScheduler.shared.cancelAllTaskRequests()
BGTaskScheduler.shared.register(forTaskWithIdentifier: BGTask.updateDBMissingID, using: nil) { (task) in
BGTask.runFillInMissingDatesTask(task: task as! BGProcessingTask)
}
UNUserNotificationCenter.current().setBadgeCount(0)
// Reset tips session on app launch
FeelsTipsManager.shared.resetSession()
// Initialize Live Activity scheduler
LiveActivityScheduler.shared.scheduleBasedOnCurrentTime()
// Initialize Watch Connectivity for cross-device widget updates
_ = WatchConnectivityManager.shared
}
var body: some Scene {
WindowGroup {
ZStack {
MainTabView(dayView: DayView(viewModel: DayViewViewModel(addMonthStartWeekdayPadding: false)),
monthView: MonthView(viewModel: DayViewViewModel(addMonthStartWeekdayPadding: true)),
yearView: YearView(viewModel: YearViewModel()),
insightsView: InsightsView())
.modelContainer(dataController.container)
.environmentObject(iapManager)
.environmentObject(authManager)
.environmentObject(healthKitManager)
.sheet(isPresented: $showSubscriptionFromWidget) {
FeelsSubscriptionStoreView()
.environmentObject(iapManager)
}
.onOpenURL { url in
if url.scheme == "feels" && url.host == "subscribe" {
showSubscriptionFromWidget = true
}
}
// Lock screen overlay
if authManager.isLockEnabled && !authManager.isUnlocked {
LockScreenView(authManager: authManager)
.transition(.opacity)
}
}
}.onChange(of: scenePhase) { _, newPhase in
if newPhase == .background {
BGTask.scheduleBackgroundProcessing()
WidgetCenter.shared.reloadAllTimelines()
// Lock the app when going to background
authManager.lock()
}
if newPhase == .active {
UNUserNotificationCenter.current().setBadgeCount(0)
// Authenticate if locked - this must happen immediately on main thread
if authManager.isLockEnabled && !authManager.isUnlocked {
Task {
await authManager.authenticate()
}
}
// Defer all non-critical foreground work to avoid blocking UI
Task.detached(priority: .utility) { @MainActor in
// Refresh from disk to pick up widget/watch changes
DataController.shared.refreshFromDisk()
// Clean up any duplicate entries first
DataController.shared.removeDuplicates()
// Fill in any missing dates (moved from AppDelegate)
DataController.shared.fillInMissingDates()
// Reschedule notifications for new title
LocalNotification.rescheduleNotifiations()
// Log event
EventLogger.log(event: "app_foregorund")
}
// Defer Live Activity scheduling (heavy DB operations)
Task.detached(priority: .utility) {
await LiveActivityScheduler.shared.scheduleBasedOnCurrentTime()
}
// Catch up on side effects from widget/watch votes
Task.detached(priority: .utility) {
await MoodLogger.shared.processPendingSideEffects()
}
// Check subscription status (network call) - throttled
Task.detached(priority: .background) {
await iapManager.checkSubscriptionStatus()
// Set PostHog person properties for subscription segmentation
let state = await iapManager.state
let subscriptionStatus: String
let subscriptionType: String
switch state {
case .subscribed:
subscriptionStatus = "subscribed"
subscriptionType = await iapManager.currentProduct?.id.contains("yearly") == true ? "yearly" : "monthly"
case .inTrial:
subscriptionStatus = "trial"
subscriptionType = "none"
case .trialExpired:
subscriptionStatus = "trial_expired"
subscriptionType = "none"
case .expired:
subscriptionStatus = "expired"
subscriptionType = "none"
case .unknown:
subscriptionStatus = "unknown"
subscriptionType = "none"
}
PostHogSDK.shared.capture("$set", properties: [
"$set": [
"subscription_status": subscriptionStatus,
"subscription_type": subscriptionType
]
])
}
}
}
}
}