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>
162 lines
6.4 KiB
Swift
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
|
|
]
|
|
])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|