Fetch and display weather data (temp, condition, hi/lo, humidity) when users log a mood. Weather is stored as JSON on MoodEntryModel and shown as a card in EntryDetailView. Premium-gated with location permission prompt. Includes BGTask retry for failed fetches and full analytics. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
666 lines
23 KiB
Swift
666 lines
23 KiB
Swift
//
|
|
// Analytics.swift
|
|
// Reflect
|
|
//
|
|
// Singleton analytics manager wrapping PostHog SDK.
|
|
// All analytics events flow through this single manager.
|
|
//
|
|
|
|
import Foundation
|
|
import PostHog
|
|
import UIKit
|
|
|
|
// MARK: - Analytics Manager
|
|
|
|
@MainActor
|
|
final class AnalyticsManager {
|
|
|
|
// MARK: - Singleton
|
|
|
|
static let shared = AnalyticsManager()
|
|
|
|
// MARK: - Configuration
|
|
|
|
private static let apiKey = "phc_3GsB3oqNft8Ykg2bJfE9MaJktzLAwr2EPMXQgwEFzAs"
|
|
private static let host = "https://analytics.88oakapps.com"
|
|
private static let optOutKey = "analyticsOptedOut"
|
|
private static let sessionReplayKey = "analytics_session_replay_enabled"
|
|
private static let iso8601Formatter = ISO8601DateFormatter()
|
|
|
|
// MARK: - State
|
|
|
|
var isOptedOut: Bool {
|
|
UserDefaults.standard.bool(forKey: Self.optOutKey)
|
|
}
|
|
|
|
var sessionReplayEnabled: Bool {
|
|
get {
|
|
if UserDefaults.standard.object(forKey: Self.sessionReplayKey) == nil {
|
|
return true
|
|
}
|
|
return UserDefaults.standard.bool(forKey: Self.sessionReplayKey)
|
|
}
|
|
set {
|
|
UserDefaults.standard.set(newValue, forKey: Self.sessionReplayKey)
|
|
if newValue {
|
|
PostHogSDK.shared.startSessionRecording()
|
|
} else {
|
|
PostHogSDK.shared.stopSessionRecording()
|
|
}
|
|
}
|
|
}
|
|
|
|
private var isConfigured = false
|
|
|
|
// MARK: - Initialization
|
|
|
|
private init() {}
|
|
|
|
// MARK: - Setup
|
|
|
|
func configure() {
|
|
guard !isConfigured else { return }
|
|
|
|
let config = PostHogConfig(apiKey: Self.apiKey, host: Self.host)
|
|
|
|
// Person profiles — .always creates anonymous person profiles for every user,
|
|
// enabling accurate unique user counts without requiring identify() calls
|
|
config.personProfiles = .always
|
|
|
|
// Auto-capture — disabled for SwiftUI (PostHog docs recommend manual tracking)
|
|
// captureElementInteractions: UIKit-only, generates noisy $autocapture events
|
|
// captureScreenViews: produces meaningless SwiftUI internal names, duplicates manual trackScreen()
|
|
config.captureElementInteractions = false
|
|
config.captureApplicationLifecycleEvents = true
|
|
config.captureScreenViews = false
|
|
|
|
// Session replay
|
|
config.sessionReplay = sessionReplayEnabled
|
|
config.sessionReplayConfig.maskAllTextInputs = true
|
|
config.sessionReplayConfig.maskAllImages = false
|
|
config.sessionReplayConfig.captureNetworkTelemetry = true
|
|
config.sessionReplayConfig.screenshotMode = true // Required for SwiftUI
|
|
|
|
// Respect user opt-out preference
|
|
if isOptedOut {
|
|
config.optOut = true
|
|
}
|
|
|
|
#if DEBUG
|
|
config.optOut = true // Prevent debug/test data from polluting production analytics
|
|
#endif
|
|
|
|
PostHogSDK.shared.setup(config)
|
|
isConfigured = true
|
|
|
|
// Register super properties
|
|
updateSuperProperties()
|
|
}
|
|
|
|
// MARK: - Super Properties (attached to every event)
|
|
|
|
func updateSuperProperties() {
|
|
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown"
|
|
let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "unknown"
|
|
let device = UIDevice.current.model
|
|
let osVersion = UIDevice.current.systemVersion
|
|
|
|
let defaults = GroupUserDefaults.groupDefaults
|
|
var props: [String: Any] = [
|
|
"app_version": version,
|
|
"build_number": build,
|
|
"device_model": device,
|
|
"os_version": osVersion,
|
|
"is_pro": IAPManager.shared.isSubscribed,
|
|
"animations_enabled": !UIAccessibility.isReduceMotionEnabled,
|
|
"selected_sports": [String](),
|
|
"theme": "n/a",
|
|
"icon_pack": "n/a",
|
|
"voting_layout": "n/a",
|
|
"day_view_style": "n/a",
|
|
"mood_shape": "n/a",
|
|
"personality_pack": "n/a",
|
|
"privacy_lock_enabled": false,
|
|
"healthkit_enabled": false,
|
|
"days_filter_count": 0,
|
|
"days_filter_all": false,
|
|
]
|
|
|
|
// Theme
|
|
if let themeRaw = defaults.string(forKey: UserDefaultsStore.Keys.theme.rawValue) {
|
|
props["theme"] = themeRaw
|
|
}
|
|
|
|
// Icon pack
|
|
if let iconPackRaw = defaults.object(forKey: UserDefaultsStore.Keys.moodImages.rawValue) as? Int,
|
|
let iconPack = MoodImages(rawValue: iconPackRaw) {
|
|
props["icon_pack"] = String(describing: iconPack)
|
|
}
|
|
|
|
// Voting layout
|
|
if let layoutRaw = defaults.object(forKey: UserDefaultsStore.Keys.votingLayoutStyle.rawValue) as? Int,
|
|
let layout = VotingLayoutStyle(rawValue: layoutRaw) {
|
|
props["voting_layout"] = layout.displayName
|
|
}
|
|
|
|
// Day view style
|
|
if let styleRaw = defaults.object(forKey: UserDefaultsStore.Keys.dayViewStyle.rawValue) as? Int,
|
|
let style = DayViewStyle(rawValue: styleRaw) {
|
|
props["day_view_style"] = style.displayName
|
|
}
|
|
|
|
// Shape
|
|
if let shapeRaw = defaults.object(forKey: UserDefaultsStore.Keys.shape.rawValue) as? Int,
|
|
let shape = BGShape(rawValue: shapeRaw) {
|
|
props["mood_shape"] = String(describing: shape)
|
|
}
|
|
|
|
// Personality pack
|
|
if let packRaw = defaults.object(forKey: UserDefaultsStore.Keys.personalityPack.rawValue) as? Int,
|
|
let pack = PersonalityPack(rawValue: packRaw) {
|
|
props["personality_pack"] = pack.title()
|
|
}
|
|
|
|
// Privacy lock
|
|
props["privacy_lock_enabled"] = defaults.bool(forKey: UserDefaultsStore.Keys.privacyLockEnabled.rawValue)
|
|
|
|
// HealthKit
|
|
props["healthkit_enabled"] = defaults.bool(forKey: UserDefaultsStore.Keys.healthKitEnabled.rawValue)
|
|
|
|
// Days filter
|
|
let daysFilter = UserDefaultsStore.getDaysFilter()
|
|
props["days_filter_count"] = daysFilter.count
|
|
props["days_filter_all"] = daysFilter.count == 7
|
|
|
|
PostHogSDK.shared.register(props)
|
|
}
|
|
|
|
func registerSuperProperties() {
|
|
updateSuperProperties()
|
|
}
|
|
|
|
// MARK: - Event Tracking
|
|
|
|
func track(_ event: Event) {
|
|
guard isConfigured else { return }
|
|
let (name, properties) = event.payload
|
|
#if DEBUG
|
|
print("[Analytics] \(name)", properties ?? [:])
|
|
#endif
|
|
PostHogSDK.shared.capture(name, properties: properties)
|
|
}
|
|
|
|
// MARK: - Screen Tracking (manual supplement to auto-capture)
|
|
|
|
func trackScreen(_ screen: Screen, properties: [String: Any]? = nil) {
|
|
guard isConfigured else { return }
|
|
var props: [String: Any] = ["screen_name": screen.rawValue]
|
|
if let properties { props.merge(properties) { _, new in new } }
|
|
|
|
#if DEBUG
|
|
print("[Analytics] screen_viewed: \(screen.rawValue)")
|
|
#endif
|
|
PostHogSDK.shared.capture("screen_viewed", properties: props)
|
|
}
|
|
|
|
// MARK: - Subscription Funnel
|
|
|
|
func trackPaywallViewed(source: String) {
|
|
guard isConfigured else { return }
|
|
PostHogSDK.shared.capture("paywall_viewed", properties: [
|
|
"source": source
|
|
])
|
|
}
|
|
|
|
func trackPurchaseStarted(productId: String, source: String) {
|
|
guard isConfigured else { return }
|
|
PostHogSDK.shared.capture("purchase_started", properties: [
|
|
"product_id": productId,
|
|
"source": source
|
|
])
|
|
}
|
|
|
|
func trackPurchaseCompleted(productId: String, source: String) {
|
|
guard isConfigured else { return }
|
|
PostHogSDK.shared.capture("purchase_completed", properties: [
|
|
"product_id": productId,
|
|
"source": source
|
|
])
|
|
}
|
|
|
|
func trackPurchaseFailed(productId: String?, source: String, error: String) {
|
|
guard isConfigured else { return }
|
|
var props: [String: Any] = [
|
|
"source": source,
|
|
"error": error
|
|
]
|
|
if let productId {
|
|
props["product_id"] = productId
|
|
}
|
|
PostHogSDK.shared.capture("purchase_failed", properties: props)
|
|
}
|
|
|
|
func trackPurchaseRestored(source: String) {
|
|
guard isConfigured else { return }
|
|
PostHogSDK.shared.capture("purchase_restored", properties: [
|
|
"source": source
|
|
])
|
|
}
|
|
|
|
// MARK: - Opt In / Opt Out
|
|
|
|
func optIn() {
|
|
UserDefaults.standard.set(false, forKey: Self.optOutKey)
|
|
if isConfigured {
|
|
PostHogSDK.shared.optIn()
|
|
PostHogSDK.shared.capture("analytics_toggled", properties: ["enabled": true])
|
|
}
|
|
}
|
|
|
|
func optOut() {
|
|
if isConfigured {
|
|
PostHogSDK.shared.capture("analytics_toggled", properties: ["enabled": false])
|
|
}
|
|
UserDefaults.standard.set(true, forKey: Self.optOutKey)
|
|
if isConfigured {
|
|
PostHogSDK.shared.optOut()
|
|
}
|
|
}
|
|
|
|
// MARK: - Person Properties (subscription segmentation)
|
|
|
|
func updateSubscriptionStatus(_ status: String, type: String) {
|
|
guard isConfigured else { return }
|
|
PostHogSDK.shared.capture("$set", properties: [
|
|
"$set": [
|
|
"subscription_status": status,
|
|
"subscription_type": type
|
|
]
|
|
])
|
|
}
|
|
|
|
func trackSubscriptionStatusObserved(
|
|
status: String,
|
|
type: String,
|
|
source: String,
|
|
isSubscribed: Bool,
|
|
hasFullAccess: Bool,
|
|
productId: String?,
|
|
willAutoRenew: Bool?,
|
|
isInGracePeriod: Bool?,
|
|
trialDaysRemaining: Int?,
|
|
expirationDate: Date?
|
|
) {
|
|
guard isConfigured else { return }
|
|
|
|
var props: [String: Any] = [
|
|
"status": status,
|
|
"type": type,
|
|
"source": source,
|
|
"is_subscribed": isSubscribed,
|
|
"has_full_access": hasFullAccess
|
|
]
|
|
|
|
if let productId {
|
|
props["product_id"] = productId
|
|
}
|
|
if let willAutoRenew {
|
|
props["will_auto_renew"] = willAutoRenew
|
|
}
|
|
if let isInGracePeriod {
|
|
props["is_in_grace_period"] = isInGracePeriod
|
|
}
|
|
if let trialDaysRemaining {
|
|
props["trial_days_remaining"] = trialDaysRemaining
|
|
}
|
|
if let expirationDate {
|
|
props["expiration_date"] = Self.iso8601Formatter.string(from: expirationDate)
|
|
}
|
|
|
|
PostHogSDK.shared.capture("subscription_status_changed", properties: props)
|
|
updateSubscriptionStatus(status, type: type)
|
|
}
|
|
|
|
// MARK: - Lifecycle
|
|
|
|
func flush() {
|
|
guard isConfigured else { return }
|
|
PostHogSDK.shared.flush()
|
|
}
|
|
|
|
func reset() {
|
|
guard isConfigured else { return }
|
|
PostHogSDK.shared.reset()
|
|
}
|
|
}
|
|
|
|
// MARK: - Screen Names
|
|
|
|
extension AnalyticsManager {
|
|
enum Screen: String {
|
|
case day = "day"
|
|
case month = "month"
|
|
case monthDetail = "month_detail"
|
|
case year = "year"
|
|
case insights = "insights"
|
|
case settings = "settings"
|
|
case customize = "customize"
|
|
case onboarding = "onboarding"
|
|
case onboardingSubscription = "onboarding_subscription"
|
|
case paywall = "paywall"
|
|
case entryDetail = "entry_detail"
|
|
case noteEditor = "note_editor"
|
|
case lockScreen = "lock_screen"
|
|
case sharing = "sharing"
|
|
case themePicker = "theme_picker"
|
|
case iconPackPicker = "icon_pack_picker"
|
|
case votingLayoutPicker = "voting_layout_picker"
|
|
case dayStylePicker = "day_style_picker"
|
|
case shapePicker = "shape_picker"
|
|
case personalityPackPicker = "personality_pack_picker"
|
|
case appThemePicker = "app_theme_picker"
|
|
case dayFilterPicker = "day_filter_picker"
|
|
case iconPicker = "icon_picker"
|
|
case widgetCreator = "widget_creator"
|
|
case widgetPicker = "widget_picker"
|
|
case privacy = "privacy"
|
|
case eula = "eula"
|
|
case specialThanks = "special_thanks"
|
|
case reminderTimePicker = "reminder_time_picker"
|
|
case exportView = "export_view"
|
|
}
|
|
}
|
|
|
|
// MARK: - Event Definitions
|
|
|
|
extension AnalyticsManager {
|
|
enum Event {
|
|
// MARK: Mood
|
|
case moodLogged(mood: Int, entryType: String)
|
|
case moodUpdated(mood: Int)
|
|
case noteUpdated(characterCount: Int)
|
|
case photoAdded
|
|
case photoDeleted
|
|
case missingEntriesFilled(count: Int)
|
|
case entryDeleted(mood: Int)
|
|
case allDataCleared
|
|
case duplicatesRemoved(count: Int)
|
|
|
|
// MARK: Customization
|
|
case themeChanged(themeId: String)
|
|
case themeApplied(themeName: String)
|
|
case iconPackChanged(packId: Int)
|
|
case votingLayoutChanged(layout: String)
|
|
case dayViewStyleChanged(style: String)
|
|
case moodShapeChanged(shapeId: Int)
|
|
case personalityPackChanged(packTitle: String)
|
|
case appIconChanged(iconTitle: String)
|
|
case celebrationAnimationChanged(animation: String)
|
|
|
|
// MARK: Widget
|
|
case widgetViewed
|
|
case widgetCreateTapped
|
|
case widgetCreated
|
|
case widgetUsed
|
|
case widgetDeleted
|
|
case widgetShuffled
|
|
case widgetRandomized
|
|
case widgetEyeUpdated(style: String)
|
|
case widgetMouthUpdated(style: String)
|
|
case widgetBackgroundUpdated(style: String)
|
|
case widgetColorUpdated(part: String)
|
|
|
|
// MARK: Notifications
|
|
case notificationEnabled
|
|
case notificationDisabled
|
|
case reminderTimeUpdated
|
|
case reminderTimeTapped
|
|
|
|
// MARK: Onboarding
|
|
case onboardingCompleted(dayId: String?)
|
|
case onboardingSubscribeTapped
|
|
case onboardingSkipped
|
|
|
|
// MARK: Subscription
|
|
case paywallSubscribeTapped(source: String)
|
|
|
|
// MARK: Settings
|
|
case settingsClosed
|
|
case deleteToggleChanged(enabled: Bool)
|
|
case hapticFeedbackToggled(enabled: Bool)
|
|
case exportTapped
|
|
case dataExported(format: String, count: Int)
|
|
case importTapped
|
|
case importSucceeded
|
|
case importFailed(error: String?)
|
|
case onboardingReshown
|
|
|
|
// MARK: Privacy & Security
|
|
case privacyLockEnabled
|
|
case privacyLockDisabled
|
|
case privacyLockEnableFailed
|
|
case appLocked
|
|
case biometricUnlockSuccess
|
|
case biometricUnlockFailed(error: String)
|
|
|
|
// MARK: HealthKit
|
|
case healthKitAuthorized
|
|
case healthKitAuthFailed(error: String)
|
|
case healthKitEnableFailed
|
|
case healthKitDisabled
|
|
case healthKitNotAuthorized
|
|
case healthKitSyncCompleted(total: Int, success: Int, failed: Int)
|
|
|
|
// MARK: Weather
|
|
case weatherToggled(enabled: Bool)
|
|
case weatherFetched
|
|
case weatherFetchFailed(error: String)
|
|
|
|
// MARK: Navigation
|
|
case tabSwitched(tab: String)
|
|
case viewHeaderChanged(header: String)
|
|
|
|
// MARK: Sharing
|
|
case shareTemplateViewed(template: String)
|
|
|
|
// MARK: Error
|
|
case storageFallbackActivated
|
|
|
|
// MARK: Legal
|
|
case eulaViewed
|
|
case privacyPolicyViewed
|
|
case specialThanksViewed
|
|
|
|
var payload: (name: String, properties: [String: Any]?) {
|
|
switch self {
|
|
// Mood
|
|
case .moodLogged(let mood, let entryType):
|
|
return ("mood_logged", ["mood_value": mood, "entry_type": entryType])
|
|
case .moodUpdated(let mood):
|
|
return ("mood_updated", ["mood_value": mood])
|
|
case .noteUpdated(let count):
|
|
return ("note_updated", ["character_count": count])
|
|
case .photoAdded:
|
|
return ("photo_added", nil)
|
|
case .photoDeleted:
|
|
return ("photo_deleted", nil)
|
|
case .missingEntriesFilled(let count):
|
|
return ("missing_entries_filled", ["count": count])
|
|
case .entryDeleted(let mood):
|
|
return ("entry_deleted", ["mood": mood])
|
|
case .allDataCleared:
|
|
return ("all_data_cleared", nil)
|
|
case .duplicatesRemoved(let count):
|
|
return ("duplicates_removed", ["count": count])
|
|
|
|
// Customization
|
|
case .themeChanged(let id):
|
|
return ("theme_changed", ["theme_id": id])
|
|
case .themeApplied(let name):
|
|
return ("theme_applied", ["theme_name": name])
|
|
case .iconPackChanged(let id):
|
|
return ("icon_pack_changed", ["pack_id": id])
|
|
case .votingLayoutChanged(let layout):
|
|
return ("voting_layout_changed", ["layout": layout])
|
|
case .dayViewStyleChanged(let style):
|
|
return ("day_view_style_changed", ["style": style])
|
|
case .moodShapeChanged(let id):
|
|
return ("mood_shape_changed", ["shape_id": id])
|
|
case .personalityPackChanged(let title):
|
|
return ("personality_pack_changed", ["pack_title": title])
|
|
case .appIconChanged(let title):
|
|
return ("app_icon_changed", ["icon_title": title])
|
|
case .celebrationAnimationChanged(let animation):
|
|
return ("celebration_animation_changed", ["animation": animation])
|
|
|
|
// Widget
|
|
case .widgetViewed:
|
|
return ("widget_viewed", nil)
|
|
case .widgetCreateTapped:
|
|
return ("widget_create_tapped", nil)
|
|
case .widgetCreated:
|
|
return ("widget_created", nil)
|
|
case .widgetUsed:
|
|
return ("widget_used", nil)
|
|
case .widgetDeleted:
|
|
return ("widget_deleted", nil)
|
|
case .widgetShuffled:
|
|
return ("widget_shuffled", nil)
|
|
case .widgetRandomized:
|
|
return ("widget_randomized", nil)
|
|
case .widgetEyeUpdated(let style):
|
|
return ("widget_eye_updated", ["style": style])
|
|
case .widgetMouthUpdated(let style):
|
|
return ("widget_mouth_updated", ["style": style])
|
|
case .widgetBackgroundUpdated(let style):
|
|
return ("widget_background_updated", ["style": style])
|
|
case .widgetColorUpdated(let part):
|
|
return ("widget_color_updated", ["part": part])
|
|
|
|
// Notifications
|
|
case .notificationEnabled:
|
|
return ("notification_enabled", nil)
|
|
case .notificationDisabled:
|
|
return ("notification_disabled", nil)
|
|
case .reminderTimeUpdated:
|
|
return ("reminder_time_updated", nil)
|
|
case .reminderTimeTapped:
|
|
return ("reminder_time_tapped", nil)
|
|
|
|
// Onboarding
|
|
case .onboardingCompleted(let dayId):
|
|
var props: [String: Any] = [:]
|
|
if let dayId { props["day_id"] = dayId }
|
|
return ("onboarding_completed", props.isEmpty ? nil : props)
|
|
case .onboardingSubscribeTapped:
|
|
return ("onboarding_subscribe_tapped", nil)
|
|
case .onboardingSkipped:
|
|
return ("onboarding_skipped", nil)
|
|
|
|
// Subscription
|
|
case .paywallSubscribeTapped(let source):
|
|
return ("paywall_subscribe_tapped", ["source": source])
|
|
|
|
// Settings
|
|
case .settingsClosed:
|
|
return ("settings_closed", nil)
|
|
case .deleteToggleChanged(let enabled):
|
|
return ("delete_toggle_changed", ["enabled": enabled])
|
|
case .hapticFeedbackToggled(let enabled):
|
|
return ("haptic_feedback_toggled", ["enabled": enabled])
|
|
case .exportTapped:
|
|
return ("export_tapped", nil)
|
|
case .dataExported(let format, let count):
|
|
return ("data_exported", ["format": format, "count": count])
|
|
case .importTapped:
|
|
return ("import_tapped", nil)
|
|
case .importSucceeded:
|
|
return ("import_succeeded", nil)
|
|
case .importFailed(let error):
|
|
return ("import_failed", error != nil ? ["error": error!] : nil)
|
|
case .onboardingReshown:
|
|
return ("onboarding_reshown", nil)
|
|
|
|
// Privacy & Security
|
|
case .privacyLockEnabled:
|
|
return ("privacy_lock_enabled", nil)
|
|
case .privacyLockDisabled:
|
|
return ("privacy_lock_disabled", nil)
|
|
case .privacyLockEnableFailed:
|
|
return ("privacy_lock_enable_failed", nil)
|
|
case .appLocked:
|
|
return ("app_locked", nil)
|
|
case .biometricUnlockSuccess:
|
|
return ("biometric_unlock_success", nil)
|
|
case .biometricUnlockFailed(let error):
|
|
return ("biometric_unlock_failed", ["error": error])
|
|
|
|
// HealthKit
|
|
case .healthKitAuthorized:
|
|
return ("healthkit_authorized", nil)
|
|
case .healthKitAuthFailed(let error):
|
|
return ("healthkit_auth_failed", ["error": error])
|
|
case .healthKitEnableFailed:
|
|
return ("healthkit_enable_failed", nil)
|
|
case .healthKitDisabled:
|
|
return ("healthkit_disabled", nil)
|
|
case .healthKitNotAuthorized:
|
|
return ("healthkit_not_authorized", nil)
|
|
case .healthKitSyncCompleted(let total, let success, let failed):
|
|
return ("healthkit_sync_completed", ["total": total, "success": success, "failed": failed])
|
|
|
|
// Weather
|
|
case .weatherToggled(let enabled):
|
|
return ("weather_toggled", ["enabled": enabled])
|
|
case .weatherFetched:
|
|
return ("weather_fetched", nil)
|
|
case .weatherFetchFailed(let error):
|
|
return ("weather_fetch_failed", ["error": error])
|
|
|
|
// Navigation
|
|
case .tabSwitched(let tab):
|
|
return ("tab_switched", ["tab": tab])
|
|
case .viewHeaderChanged(let header):
|
|
return ("view_header_changed", ["header": header])
|
|
|
|
// Sharing
|
|
case .shareTemplateViewed(let template):
|
|
return ("share_template_viewed", ["template": template])
|
|
|
|
// Error
|
|
case .storageFallbackActivated:
|
|
return ("storage_fallback_activated", nil)
|
|
|
|
// Legal
|
|
case .eulaViewed:
|
|
return ("eula_viewed", nil)
|
|
case .privacyPolicyViewed:
|
|
return ("privacy_policy_viewed", nil)
|
|
case .specialThanksViewed:
|
|
return ("special_thanks_viewed", nil)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - SwiftUI Screen Tracking Modifier
|
|
|
|
import SwiftUI
|
|
|
|
struct ScreenTrackingModifier: ViewModifier {
|
|
let screen: AnalyticsManager.Screen
|
|
let properties: [String: Any]?
|
|
|
|
func body(content: Content) -> some View {
|
|
content.onAppear {
|
|
AnalyticsManager.shared.trackScreen(screen, properties: properties)
|
|
}
|
|
}
|
|
}
|
|
|
|
extension View {
|
|
func trackScreen(_ screen: AnalyticsManager.Screen, properties: [String: Any]? = nil) -> some View {
|
|
modifier(ScreenTrackingModifier(screen: screen, properties: properties))
|
|
}
|
|
}
|