Replace EventLogger with typed AnalyticsManager using PostHog
Complete analytics overhaul: delete EventLogger.swift, create Analytics.swift with typed event enum (~45 events), screen tracking, super properties (theme, icon pack, voting layout, etc.), session replay with kill switch, autocapture, and network telemetry. Replace all 99 call sites across 38 files with compiler-enforced typed events in object_action naming convention. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -46,8 +46,7 @@ struct CreateWidgetView: View {
|
||||
}
|
||||
|
||||
func update(eye: CustomWidgetEyes, eyeOption: CustomWidgeImageOptions) {
|
||||
EventLogger.log(event: "create_widget_view_update_eye",
|
||||
withData: ["eye_value": eye.rawValue, "eye_option_value": eyeOption.rawValue])
|
||||
AnalyticsManager.shared.track(.widgetEyeUpdated(style: eyeOption.rawValue))
|
||||
switch eye {
|
||||
case .left:
|
||||
customWidget.leftEye = eyeOption
|
||||
@@ -57,7 +56,7 @@ struct CreateWidgetView: View {
|
||||
}
|
||||
|
||||
func createRandom() {
|
||||
EventLogger.log(event: "create_widget_view_create_random")
|
||||
AnalyticsManager.shared.track(.widgetRandomized)
|
||||
customWidget.bgColor = Color.random()
|
||||
customWidget.innerColor = Color.random()
|
||||
customWidget.bgOverlayColor = Color.random()
|
||||
@@ -74,14 +73,12 @@ struct CreateWidgetView: View {
|
||||
}
|
||||
|
||||
func update(mouthOption: CustomWidgeImageOptions) {
|
||||
EventLogger.log(event: "create_widget_view_update_mouth",
|
||||
withData: ["mouthOption": mouthOption.rawValue])
|
||||
AnalyticsManager.shared.track(.widgetMouthUpdated(style: mouthOption.rawValue))
|
||||
customWidget.mouth = mouthOption
|
||||
}
|
||||
|
||||
func update(background: CustomWidgetBackGroundOptions) {
|
||||
EventLogger.log(event: "create_widget_view_update_background",
|
||||
withData: ["background": background.rawValue])
|
||||
AnalyticsManager.shared.track(.widgetBackgroundUpdated(style: background.rawValue))
|
||||
customWidget.background = background
|
||||
}
|
||||
|
||||
@@ -101,7 +98,7 @@ struct CreateWidgetView: View {
|
||||
var bottomBarButtons: some View {
|
||||
HStack(alignment: .center, spacing: 0) {
|
||||
Button(action: {
|
||||
EventLogger.log(event: "create_widget_view_shuffle")
|
||||
AnalyticsManager.shared.track(.widgetShuffled)
|
||||
createRandom()
|
||||
}, label: {
|
||||
Image(systemName: "shuffle")
|
||||
@@ -114,7 +111,7 @@ struct CreateWidgetView: View {
|
||||
.background(.blue)
|
||||
|
||||
Button(action: {
|
||||
EventLogger.log(event: "create_widget_view_save_widget")
|
||||
AnalyticsManager.shared.track(.widgetCreated)
|
||||
UserDefaultsStore.saveCustomWidget(widgetModel: customWidget, inUse: false)
|
||||
let impactMed = UIImpactFeedbackGenerator(style: .heavy)
|
||||
impactMed.impactOccurred()
|
||||
@@ -132,7 +129,7 @@ struct CreateWidgetView: View {
|
||||
.background(.green)
|
||||
|
||||
Button(action: {
|
||||
EventLogger.log(event: "customize_view_use_widget")
|
||||
AnalyticsManager.shared.track(.widgetUsed)
|
||||
UserDefaultsStore.saveCustomWidget(widgetModel: customWidget, inUse: true)
|
||||
let impactMed = UIImpactFeedbackGenerator(style: .heavy)
|
||||
impactMed.impactOccurred()
|
||||
@@ -151,7 +148,7 @@ struct CreateWidgetView: View {
|
||||
|
||||
if customWidget.isSaved {
|
||||
Button(action: {
|
||||
EventLogger.log(event: "customize_view_delete_widget")
|
||||
AnalyticsManager.shared.track(.widgetDeleted)
|
||||
UserDefaultsStore.deleteCustomWidget(withUUID: customWidget.uuid)
|
||||
let impactMed = UIImpactFeedbackGenerator(style: .heavy)
|
||||
impactMed.impactOccurred()
|
||||
@@ -178,7 +175,7 @@ struct CreateWidgetView: View {
|
||||
Text(String(localized: "create_widget_background_color"))
|
||||
ColorPicker("", selection: $customWidget.bgColor)
|
||||
.onChange(of: customWidget.bgColor) {
|
||||
EventLogger.log(event: "create_widget_view_update_background_color")
|
||||
AnalyticsManager.shared.track(.widgetColorUpdated(part: "background"))
|
||||
}
|
||||
.labelsHidden()
|
||||
}
|
||||
@@ -188,7 +185,7 @@ struct CreateWidgetView: View {
|
||||
Text(String(localized: "create_widget_inner_color"))
|
||||
ColorPicker("", selection: $customWidget.innerColor)
|
||||
.onChange(of: customWidget.innerColor) {
|
||||
EventLogger.log(event: "create_widget_view_update_inner_color")
|
||||
AnalyticsManager.shared.track(.widgetColorUpdated(part: "inner"))
|
||||
}
|
||||
.labelsHidden()
|
||||
}
|
||||
@@ -198,7 +195,7 @@ struct CreateWidgetView: View {
|
||||
Text(String(localized: "create_widget_face_outline_color"))
|
||||
ColorPicker("", selection: $customWidget.circleStrokeColor)
|
||||
.onChange(of: customWidget.circleStrokeColor) {
|
||||
EventLogger.log(event: "create_widget_view_update_outline_color")
|
||||
AnalyticsManager.shared.track(.widgetColorUpdated(part: "outline"))
|
||||
}
|
||||
.labelsHidden()
|
||||
}
|
||||
@@ -210,7 +207,7 @@ struct CreateWidgetView: View {
|
||||
Text(String(localized: "create_widget_view_left_eye_color"))
|
||||
ColorPicker("", selection: $customWidget.leftEyeColor)
|
||||
.onChange(of: customWidget.leftEyeColor) {
|
||||
EventLogger.log(event: "create_widget_view_update_left_eye_color")
|
||||
AnalyticsManager.shared.track(.widgetColorUpdated(part: "left_eye"))
|
||||
}
|
||||
.labelsHidden()
|
||||
}
|
||||
@@ -220,7 +217,7 @@ struct CreateWidgetView: View {
|
||||
Text(String(localized: "create_widget_view_right_eye_color"))
|
||||
ColorPicker("", selection: $customWidget.rightEyeColor)
|
||||
.onChange(of: customWidget.rightEyeColor) {
|
||||
EventLogger.log(event: "create_widget_view_update_right_eye_color")
|
||||
AnalyticsManager.shared.track(.widgetColorUpdated(part: "right_eye"))
|
||||
}
|
||||
.labelsHidden()
|
||||
}
|
||||
@@ -230,7 +227,7 @@ struct CreateWidgetView: View {
|
||||
Text(String(localized: "create_widget_view_mouth_color"))
|
||||
ColorPicker("", selection: $customWidget.mouthColor)
|
||||
.onChange(of: customWidget.mouthColor) {
|
||||
EventLogger.log(event: "create_widget_view_update_mouth_color")
|
||||
AnalyticsManager.shared.track(.widgetColorUpdated(part: "mouth"))
|
||||
}
|
||||
.labelsHidden()
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ struct CustomizeContentView: View {
|
||||
.padding(.bottom, 32)
|
||||
}
|
||||
.onAppear(perform: {
|
||||
EventLogger.log(event: "show_customize_view")
|
||||
AnalyticsManager.shared.trackScreen(.customize)
|
||||
})
|
||||
.customizeLayoutTip()
|
||||
}
|
||||
@@ -175,10 +175,10 @@ struct CustomizeView: View {
|
||||
.padding(.bottom, 32)
|
||||
}
|
||||
.onAppear(perform: {
|
||||
EventLogger.log(event: "show_customize_view")
|
||||
AnalyticsManager.shared.trackScreen(.customize)
|
||||
})
|
||||
.sheet(isPresented: $showSubscriptionStore) {
|
||||
FeelsSubscriptionStoreView()
|
||||
FeelsSubscriptionStoreView(source: "customize")
|
||||
}
|
||||
.background(
|
||||
theme.currentTheme.bg
|
||||
@@ -253,7 +253,7 @@ struct ThemePickerCompact: View {
|
||||
ForEach(Theme.allCases, id: \.rawValue) { aTheme in
|
||||
Button(action: {
|
||||
theme = aTheme
|
||||
EventLogger.log(event: "change_theme_id", withData: ["id": aTheme.rawValue])
|
||||
AnalyticsManager.shared.track(.themeChanged(themeId: aTheme.rawValue))
|
||||
}) {
|
||||
VStack(spacing: 8) {
|
||||
ZStack {
|
||||
@@ -300,7 +300,7 @@ struct ImagePackPickerCompact: View {
|
||||
let impactMed = UIImpactFeedbackGenerator(style: .medium)
|
||||
impactMed.impactOccurred()
|
||||
imagePack = images
|
||||
EventLogger.log(event: "change_image_pack_id", withData: ["id": images.rawValue])
|
||||
AnalyticsManager.shared.track(.iconPackChanged(packId: images.rawValue))
|
||||
}) {
|
||||
HStack {
|
||||
HStack(spacing: 16) {
|
||||
@@ -358,7 +358,7 @@ struct VotingLayoutPickerCompact: View {
|
||||
votingLayoutStyle = layout.rawValue
|
||||
}
|
||||
}
|
||||
EventLogger.log(event: "change_voting_layout", withData: ["layout": layout.displayName])
|
||||
AnalyticsManager.shared.track(.votingLayoutChanged(layout: layout.displayName))
|
||||
}) {
|
||||
VStack(spacing: 6) {
|
||||
layoutIcon(for: layout)
|
||||
@@ -490,7 +490,7 @@ struct CustomWidgetSection: View {
|
||||
.frame(width: 60, height: 60)
|
||||
.cornerRadius(12)
|
||||
.onTapGesture {
|
||||
EventLogger.log(event: "show_widget")
|
||||
AnalyticsManager.shared.track(.widgetViewed)
|
||||
selectedWidget.selectedItem = widget.copy() as? CustomWidgetModel
|
||||
selectedWidget.showSheet = true
|
||||
}
|
||||
@@ -498,7 +498,7 @@ struct CustomWidgetSection: View {
|
||||
|
||||
// Add button
|
||||
Button(action: {
|
||||
EventLogger.log(event: "tap_create_new_widget")
|
||||
AnalyticsManager.shared.track(.widgetCreateTapped)
|
||||
selectedWidget.selectedItem = CustomWidgetModel.randomWidget
|
||||
selectedWidget.showSheet = true
|
||||
}) {
|
||||
@@ -547,12 +547,11 @@ struct PersonalityPackPickerCompact: View {
|
||||
Button(action: {
|
||||
// if aPack.rawValue == PersonalityPack.Rude.rawValue && !showNSFW {
|
||||
// showOver18Alert = true
|
||||
// EventLogger.log(event: "show_over_18_alert")
|
||||
// } else {
|
||||
let impactMed = UIImpactFeedbackGenerator(style: .medium)
|
||||
impactMed.impactOccurred()
|
||||
personalityPack = aPack
|
||||
EventLogger.log(event: "change_personality_pack", withData: ["pack_title": aPack.title()])
|
||||
AnalyticsManager.shared.track(.personalityPackChanged(packTitle: aPack.title()))
|
||||
LocalNotification.rescheduleNotifiations()
|
||||
// }
|
||||
}) {
|
||||
@@ -651,7 +650,7 @@ struct SubscriptionBannerView: View {
|
||||
|
||||
private var notSubscribedView: some View {
|
||||
Button(action: {
|
||||
EventLogger.log(event: "customize_subscribe_tapped")
|
||||
AnalyticsManager.shared.track(.paywallSubscribeTapped(source: "customize"))
|
||||
showSubscriptionStore = true
|
||||
}) {
|
||||
HStack(spacing: 12) {
|
||||
@@ -722,7 +721,7 @@ struct DayViewStylePickerCompact: View {
|
||||
}
|
||||
let impactMed = UIImpactFeedbackGenerator(style: .medium)
|
||||
impactMed.impactOccurred()
|
||||
EventLogger.log(event: "change_day_view_style", withData: ["style": style.displayName])
|
||||
AnalyticsManager.shared.track(.dayViewStyleChanged(style: style.displayName))
|
||||
}) {
|
||||
VStack(spacing: 6) {
|
||||
styleIcon(for: style)
|
||||
|
||||
@@ -24,7 +24,7 @@ struct CustomWigetView: View {
|
||||
.frame(width: 50, height: 50)
|
||||
.cornerRadius(10)
|
||||
.onTapGesture {
|
||||
EventLogger.log(event: "show_widget")
|
||||
AnalyticsManager.shared.track(.widgetViewed)
|
||||
selectedWidget.selectedItem = widget.copy() as? CustomWidgetModel
|
||||
selectedWidget.showSheet = true
|
||||
}
|
||||
@@ -35,7 +35,7 @@ struct CustomWigetView: View {
|
||||
Image(systemName: "plus")
|
||||
)
|
||||
.onTapGesture {
|
||||
EventLogger.log(event: "tap_create_new_widget")
|
||||
AnalyticsManager.shared.track(.widgetCreateTapped)
|
||||
selectedWidget.selectedItem = CustomWidgetModel.randomWidget
|
||||
selectedWidget.showSheet = true
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ struct IconPickerView: View {
|
||||
HStack {
|
||||
Button(action: {
|
||||
UIApplication.shared.setAlternateIconName(nil)
|
||||
EventLogger.log(event: "change_icon_title", withData: ["title": "default"])
|
||||
AnalyticsManager.shared.track(.appIconChanged(iconTitle: "default"))
|
||||
}, label: {
|
||||
Image("AppIconImage", bundle: .main)
|
||||
.resizable()
|
||||
@@ -73,7 +73,7 @@ struct IconPickerView: View {
|
||||
UIApplication.shared.setAlternateIconName(iconSet.1) { (error) in
|
||||
// FIXME: Handle error
|
||||
}
|
||||
EventLogger.log(event: "change_icon_title", withData: ["title": iconSet.1])
|
||||
AnalyticsManager.shared.track(.appIconChanged(iconTitle: iconSet.1))
|
||||
}, label: {
|
||||
Image(iconSet.0, bundle: .main)
|
||||
.resizable()
|
||||
|
||||
@@ -45,7 +45,7 @@ struct ImagePackPickerView: View {
|
||||
let impactMed = UIImpactFeedbackGenerator(style: .heavy)
|
||||
impactMed.impactOccurred()
|
||||
imagePack = images
|
||||
EventLogger.log(event: "change_image_pack_id", withData: ["id": images.rawValue])
|
||||
AnalyticsManager.shared.track(.iconPackChanged(packId: images.rawValue))
|
||||
}
|
||||
if images.rawValue != (MoodImages.allCases.sorted(by: { $0.rawValue > $1.rawValue }).first?.rawValue) ?? 0 {
|
||||
Divider()
|
||||
|
||||
@@ -41,14 +41,10 @@ struct PersonalityPackPickerView: View {
|
||||
.padding(5)
|
||||
)
|
||||
.onTapGesture {
|
||||
// if aPack.rawValue == PersonalityPack.Rude.rawValue && !showNSFW {
|
||||
// showOver18Alert = true
|
||||
// EventLogger.log(event: "show_over_18_alert")
|
||||
// } else {
|
||||
let impactMed = UIImpactFeedbackGenerator(style: .heavy)
|
||||
impactMed.impactOccurred()
|
||||
personalityPack = aPack
|
||||
EventLogger.log(event: "change_personality_pack", withData: ["pack_title": aPack.title()])
|
||||
AnalyticsManager.shared.track(.personalityPackChanged(packTitle: aPack.title()))
|
||||
LocalNotification.rescheduleNotifiations()
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ struct ShapePickerView: View {
|
||||
let impactMed = UIImpactFeedbackGenerator(style: .heavy)
|
||||
impactMed.impactOccurred()
|
||||
shape = ashape
|
||||
EventLogger.log(event: "change_mood_shape_id", withData: ["id": shape.rawValue])
|
||||
AnalyticsManager.shared.track(.moodShapeChanged(shapeId: shape.rawValue))
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.background(
|
||||
|
||||
@@ -71,7 +71,7 @@ struct ThemePickerView: View {
|
||||
selectedTheme = theme
|
||||
}
|
||||
|
||||
EventLogger.log(event: "change_theme_id", withData: ["id": theme.rawValue])
|
||||
AnalyticsManager.shared.track(.themeChanged(themeId: theme.rawValue))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ struct VotingLayoutPickerView: View {
|
||||
votingLayoutStyle = layout.rawValue
|
||||
}
|
||||
}
|
||||
EventLogger.log(event: "change_voting_layout", withData: ["layout": layout.displayName])
|
||||
AnalyticsManager.shared.track(.votingLayoutChanged(layout: layout.displayName))
|
||||
}) {
|
||||
VStack(spacing: 6) {
|
||||
layoutIcon(for: layout)
|
||||
|
||||
@@ -40,7 +40,7 @@ struct DayView: View {
|
||||
ZStack {
|
||||
mainView
|
||||
.onAppear(perform: {
|
||||
EventLogger.log(event: "show_home_view")
|
||||
AnalyticsManager.shared.trackScreen(.day)
|
||||
})
|
||||
.sheet(isPresented: $showingSheet) {
|
||||
SettingsView()
|
||||
|
||||
@@ -16,6 +16,7 @@ struct FeelsSubscriptionStoreView: View {
|
||||
@AppStorage(UserDefaultsStore.Keys.paywallStyle.rawValue, store: GroupUserDefaults.groupDefaults)
|
||||
private var paywallStyleRaw: Int = 0
|
||||
|
||||
var source: String = "unknown"
|
||||
var style: PaywallStyle?
|
||||
|
||||
private var currentStyle: PaywallStyle {
|
||||
@@ -33,9 +34,29 @@ struct FeelsSubscriptionStoreView: View {
|
||||
.storeButton(.visible, for: .restorePurchases)
|
||||
.subscriptionStoreButtonLabel(.multiline)
|
||||
.tint(tintColor)
|
||||
.onInAppPurchaseCompletion { _, result in
|
||||
if case .success(.success(_)) = result {
|
||||
.onAppear {
|
||||
AnalyticsManager.shared.trackPaywallViewed(source: source)
|
||||
}
|
||||
.onInAppPurchaseStart { product in
|
||||
AnalyticsManager.shared.trackPurchaseStarted(productId: product.id, source: source)
|
||||
}
|
||||
.onInAppPurchaseCompletion { product, result in
|
||||
switch result {
|
||||
case .success(.success(_)):
|
||||
AnalyticsManager.shared.trackPurchaseCompleted(productId: product.id, source: source)
|
||||
Task { @MainActor in
|
||||
await iapManager.checkSubscriptionStatus()
|
||||
iapManager.trackSubscriptionAnalytics(source: "purchase_success")
|
||||
}
|
||||
dismiss()
|
||||
case .success(.userCancelled):
|
||||
AnalyticsManager.shared.trackPurchaseFailed(productId: product.id, source: source, error: "user_cancelled")
|
||||
case .success(.pending):
|
||||
AnalyticsManager.shared.trackPurchaseFailed(productId: product.id, source: source, error: "pending")
|
||||
case .failure(let error):
|
||||
AnalyticsManager.shared.trackPurchaseFailed(productId: product.id, source: source, error: error.localizedDescription)
|
||||
@unknown default:
|
||||
AnalyticsManager.shared.trackPurchaseFailed(productId: product.id, source: source, error: "unknown_result")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ struct IAPWarningView: View {
|
||||
.padding()
|
||||
.background(theme.currentTheme.secondaryBGColor)
|
||||
.sheet(isPresented: $showSubscriptionStore) {
|
||||
FeelsSubscriptionStoreView()
|
||||
FeelsSubscriptionStoreView(source: "iap_warning")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,14 +171,14 @@ struct InsightsView: View {
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showSubscriptionStore) {
|
||||
FeelsSubscriptionStoreView()
|
||||
FeelsSubscriptionStoreView(source: "insights_gate")
|
||||
}
|
||||
.background(
|
||||
theme.currentTheme.bg
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
)
|
||||
.onAppear {
|
||||
EventLogger.log(event: "show_insights_view")
|
||||
AnalyticsManager.shared.trackScreen(.insights)
|
||||
viewModel.generateInsights()
|
||||
}
|
||||
.padding(.top)
|
||||
|
||||
@@ -77,7 +77,7 @@ struct MonthDetailView: View {
|
||||
)
|
||||
}
|
||||
.onAppear(perform: {
|
||||
EventLogger.log(event: "show_month_detail_view")
|
||||
AnalyticsManager.shared.trackScreen(.monthDetail)
|
||||
})
|
||||
.background(
|
||||
theme.currentTheme.bg
|
||||
|
||||
@@ -337,10 +337,10 @@ struct MonthView: View {
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showSubscriptionStore) {
|
||||
FeelsSubscriptionStoreView()
|
||||
FeelsSubscriptionStoreView(source: "month_gate")
|
||||
}
|
||||
.onAppear(perform: {
|
||||
EventLogger.log(event: "show_month_view")
|
||||
AnalyticsManager.shared.trackScreen(.month)
|
||||
})
|
||||
.padding([.top])
|
||||
.background(
|
||||
|
||||
@@ -32,7 +32,7 @@ struct PurchaseButtonView: View {
|
||||
.background(theme.currentTheme.secondaryBGColor)
|
||||
.cornerRadius(10)
|
||||
.sheet(isPresented: $showSubscriptionStore) {
|
||||
FeelsSubscriptionStoreView()
|
||||
FeelsSubscriptionStoreView(source: "purchase_button")
|
||||
}
|
||||
.manageSubscriptionsSheet(isPresented: $showManageSubscriptions)
|
||||
}
|
||||
@@ -104,7 +104,8 @@ struct PurchaseButtonView: View {
|
||||
|
||||
private var subscriptionStatusBadge: some View {
|
||||
Group {
|
||||
if case .subscribed(_, let willAutoRenew) = iapManager.state {
|
||||
switch iapManager.state {
|
||||
case .subscribed(_, let willAutoRenew):
|
||||
if willAutoRenew {
|
||||
Text(String(localized: "subscription_status_active"))
|
||||
.font(.caption)
|
||||
@@ -122,6 +123,16 @@ struct PurchaseButtonView: View {
|
||||
.background(Color.orange)
|
||||
.cornerRadius(4)
|
||||
}
|
||||
case .billingRetry, .gracePeriod:
|
||||
Text("Payment Issue")
|
||||
.font(.caption)
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 4)
|
||||
.background(Color.orange)
|
||||
.cornerRadius(4)
|
||||
default:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -166,7 +177,7 @@ struct PurchaseButtonView: View {
|
||||
// Restore purchases
|
||||
Button {
|
||||
Task {
|
||||
await iapManager.restore()
|
||||
await iapManager.restore(source: "purchase_button")
|
||||
}
|
||||
} label: {
|
||||
Text(String(localized: "purchase_view_restore"))
|
||||
|
||||
@@ -38,7 +38,7 @@ struct PaywallPreviewSettingsView: View {
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showFullPreview) {
|
||||
FeelsSubscriptionStoreView(style: selectedStyle)
|
||||
FeelsSubscriptionStoreView(source: "paywall_preview", style: selectedStyle)
|
||||
.environmentObject(iapManager)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ struct SettingsTabView: View {
|
||||
WhyUpgradeView()
|
||||
}
|
||||
.sheet(isPresented: $showSubscriptionStore) {
|
||||
FeelsSubscriptionStoreView()
|
||||
FeelsSubscriptionStoreView(source: "settings_tab")
|
||||
.environmentObject(iapManager)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ struct SettingsContentView: View {
|
||||
legalSectionHeader
|
||||
eulaButton
|
||||
privacyButton
|
||||
analyticsToggle
|
||||
|
||||
addTestDataButton
|
||||
|
||||
@@ -108,7 +109,7 @@ struct SettingsContentView: View {
|
||||
ReminderTimePickerView()
|
||||
}
|
||||
.onAppear(perform: {
|
||||
EventLogger.log(event: "show_settings_view")
|
||||
AnalyticsManager.shared.trackScreen(.settings)
|
||||
FeelsTipsManager.shared.onSettingsViewed()
|
||||
})
|
||||
}
|
||||
@@ -119,7 +120,7 @@ struct SettingsContentView: View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
Button(action: {
|
||||
EventLogger.log(event: "tap_reminder_time")
|
||||
AnalyticsManager.shared.track(.reminderTimeTapped)
|
||||
showReminderTimePicker = true
|
||||
}, label: {
|
||||
HStack(spacing: 12) {
|
||||
@@ -832,7 +833,7 @@ struct SettingsContentView: View {
|
||||
if newValue {
|
||||
let success = await authManager.enableLock()
|
||||
if !success {
|
||||
EventLogger.log(event: "privacy_lock_enable_failed")
|
||||
AnalyticsManager.shared.track(.privacyLockEnableFailed)
|
||||
}
|
||||
} else {
|
||||
authManager.disableLock()
|
||||
@@ -927,16 +928,16 @@ struct SettingsContentView: View {
|
||||
// Sync all existing moods to HealthKit
|
||||
await HealthKitManager.shared.syncAllMoods()
|
||||
} else {
|
||||
EventLogger.log(event: "healthkit_state_of_mind_not_authorized")
|
||||
AnalyticsManager.shared.track(.healthKitNotAuthorized)
|
||||
}
|
||||
} catch {
|
||||
print("HealthKit authorization failed: \(error)")
|
||||
EventLogger.log(event: "healthkit_enable_failed")
|
||||
AnalyticsManager.shared.track(.healthKitEnableFailed)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
healthService.isEnabled = false
|
||||
EventLogger.log(event: "healthkit_disabled")
|
||||
AnalyticsManager.shared.track(.healthKitDisabled)
|
||||
}
|
||||
}
|
||||
))
|
||||
@@ -988,7 +989,7 @@ struct SettingsContentView: View {
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
.healthKitSyncTip()
|
||||
.sheet(isPresented: $showSubscriptionStore) {
|
||||
FeelsSubscriptionStoreView()
|
||||
FeelsSubscriptionStoreView(source: "settings")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -998,7 +999,7 @@ struct SettingsContentView: View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
Button(action: {
|
||||
EventLogger.log(event: "tap_export_data")
|
||||
AnalyticsManager.shared.track(.exportTapped)
|
||||
showExportView = true
|
||||
}, label: {
|
||||
HStack(spacing: 12) {
|
||||
@@ -1035,7 +1036,7 @@ struct SettingsContentView: View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
Button(action: {
|
||||
EventLogger.log(event: "tap_show_onboarding")
|
||||
AnalyticsManager.shared.track(.onboardingReshown)
|
||||
showOnboarding.toggle()
|
||||
}, label: {
|
||||
Text(String(localized: "settings_view_show_onboarding"))
|
||||
@@ -1055,7 +1056,7 @@ struct SettingsContentView: View {
|
||||
Toggle(String(localized: "settings_use_delete_enable"),
|
||||
isOn: $deleteEnabled)
|
||||
.onChange(of: deleteEnabled) { _, newValue in
|
||||
EventLogger.log(event: "toggle_can_delete", withData: ["value": newValue])
|
||||
AnalyticsManager.shared.track(.deleteToggleChanged(enabled: newValue))
|
||||
}
|
||||
.foregroundColor(textColor)
|
||||
.accessibilityHint(String(localized: "Allow deleting mood entries by swiping"))
|
||||
@@ -1070,7 +1071,7 @@ struct SettingsContentView: View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
Button(action: {
|
||||
EventLogger.log(event: "show_eula")
|
||||
AnalyticsManager.shared.track(.eulaViewed)
|
||||
if let url = URL(string: "https://feels.app/eula.html") {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
@@ -1089,7 +1090,7 @@ struct SettingsContentView: View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
Button(action: {
|
||||
EventLogger.log(event: "show_privacy")
|
||||
AnalyticsManager.shared.track(.privacyPolicyViewed)
|
||||
if let url = URL(string: "https://feels.app/privacy.html") {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
@@ -1104,6 +1105,48 @@ struct SettingsContentView: View {
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
}
|
||||
|
||||
// MARK: - Analytics Toggle
|
||||
|
||||
private var analyticsToggle: some View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: "chart.bar.xaxis")
|
||||
.font(.title2)
|
||||
.foregroundColor(.accentColor)
|
||||
.frame(width: 32)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Share Analytics")
|
||||
.foregroundColor(textColor)
|
||||
|
||||
Text("Help improve Feels by sharing anonymous usage data")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Toggle("", isOn: Binding(
|
||||
get: { !AnalyticsManager.shared.isOptedOut },
|
||||
set: { enabled in
|
||||
if enabled {
|
||||
AnalyticsManager.shared.optIn()
|
||||
} else {
|
||||
AnalyticsManager.shared.optOut()
|
||||
}
|
||||
}
|
||||
))
|
||||
.labelsHidden()
|
||||
.accessibilityLabel("Share Analytics")
|
||||
.accessibilityHint("Toggle anonymous usage analytics")
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
}
|
||||
|
||||
// MARK: - Helper Functions
|
||||
|
||||
private func openInFilesApp(_ url: URL) {
|
||||
@@ -1175,7 +1218,7 @@ struct ReminderTimePickerView: View {
|
||||
// This handles notification scheduling and Live Activity rescheduling
|
||||
OnboardingDataDataManager.shared.updateOnboardingData(onboardingData: onboardingData)
|
||||
|
||||
EventLogger.log(event: "reminder_time_updated")
|
||||
AnalyticsManager.shared.track(.reminderTimeUpdated)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1231,6 +1274,7 @@ struct SettingsView: View {
|
||||
legalSectionHeader
|
||||
eulaButton
|
||||
privacyButton
|
||||
analyticsToggle
|
||||
// specialThanksCell
|
||||
}
|
||||
|
||||
@@ -1268,7 +1312,7 @@ struct SettingsView: View {
|
||||
))
|
||||
}
|
||||
.onAppear(perform: {
|
||||
EventLogger.log(event: "show_settings_view")
|
||||
AnalyticsManager.shared.trackScreen(.settings)
|
||||
})
|
||||
.background(
|
||||
theme.currentTheme.bg
|
||||
@@ -1282,7 +1326,7 @@ struct SettingsView: View {
|
||||
onCompletion: { result in
|
||||
switch result {
|
||||
case .success(let url):
|
||||
EventLogger.log(event: "exported_file")
|
||||
AnalyticsManager.shared.track(.dataExported(format: "file", count: 0))
|
||||
print("Saved to \(url)")
|
||||
case .failure(let error):
|
||||
print(error.localizedDescription)
|
||||
@@ -1319,13 +1363,13 @@ struct SettingsView: View {
|
||||
DataController.shared.add(mood: mood, forDate: forDate, entryType: entryType)
|
||||
}
|
||||
DataController.shared.saveAndRunDataListeners()
|
||||
EventLogger.log(event: "import_file")
|
||||
AnalyticsManager.shared.track(.importSucceeded)
|
||||
} else {
|
||||
EventLogger.log(event: "error_import_file")
|
||||
AnalyticsManager.shared.track(.importFailed(error: nil))
|
||||
}
|
||||
} catch {
|
||||
// Handle failure.
|
||||
EventLogger.log(event: "error_import_file", withData: ["error": error.localizedDescription])
|
||||
AnalyticsManager.shared.track(.importFailed(error: error.localizedDescription))
|
||||
print("Unable to read file contents")
|
||||
print(error.localizedDescription)
|
||||
}
|
||||
@@ -1402,7 +1446,7 @@ struct SettingsView: View {
|
||||
if newValue {
|
||||
let success = await authManager.enableLock()
|
||||
if !success {
|
||||
EventLogger.log(event: "privacy_lock_enable_failed")
|
||||
AnalyticsManager.shared.track(.privacyLockEnableFailed)
|
||||
}
|
||||
} else {
|
||||
authManager.disableLock()
|
||||
@@ -1465,16 +1509,16 @@ struct SettingsView: View {
|
||||
// Sync all existing moods to HealthKit
|
||||
await HealthKitManager.shared.syncAllMoods()
|
||||
} else {
|
||||
EventLogger.log(event: "healthkit_state_of_mind_not_authorized")
|
||||
AnalyticsManager.shared.track(.healthKitNotAuthorized)
|
||||
}
|
||||
} catch {
|
||||
print("HealthKit authorization failed: \(error)")
|
||||
EventLogger.log(event: "healthkit_enable_failed")
|
||||
AnalyticsManager.shared.track(.healthKitEnableFailed)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
healthService.isEnabled = false
|
||||
EventLogger.log(event: "healthkit_disabled")
|
||||
AnalyticsManager.shared.track(.healthKitDisabled)
|
||||
}
|
||||
}
|
||||
))
|
||||
@@ -1518,7 +1562,7 @@ struct SettingsView: View {
|
||||
.background(theme.currentTheme.secondaryBGColor)
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
.sheet(isPresented: $showSubscriptionStore) {
|
||||
FeelsSubscriptionStoreView()
|
||||
FeelsSubscriptionStoreView(source: "settings")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1528,7 +1572,7 @@ struct SettingsView: View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
Button(action: {
|
||||
EventLogger.log(event: "tap_export_data")
|
||||
AnalyticsManager.shared.track(.exportTapped)
|
||||
showExportView = true
|
||||
}, label: {
|
||||
HStack(spacing: 12) {
|
||||
@@ -1558,12 +1602,12 @@ struct SettingsView: View {
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
}
|
||||
|
||||
|
||||
private var closeButtonView: some View {
|
||||
HStack{
|
||||
Spacer()
|
||||
Button(action: {
|
||||
EventLogger.log(event: "tap_settings_close")
|
||||
AnalyticsManager.shared.track(.settingsClosed)
|
||||
dismiss()
|
||||
}, label: {
|
||||
Text(String(localized: "settings_view_exit"))
|
||||
@@ -1578,7 +1622,7 @@ struct SettingsView: View {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
VStack {
|
||||
Button(action: {
|
||||
EventLogger.log(event: "tap_show_special_thanks")
|
||||
AnalyticsManager.shared.track(.specialThanksViewed)
|
||||
withAnimation{
|
||||
showSpecialThanks.toggle()
|
||||
}
|
||||
@@ -1753,7 +1797,7 @@ struct SettingsView: View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
Button(action: {
|
||||
EventLogger.log(event: "tap_show_onboarding")
|
||||
AnalyticsManager.shared.track(.onboardingReshown)
|
||||
showOnboarding.toggle()
|
||||
}, label: {
|
||||
Text(String(localized: "settings_view_show_onboarding"))
|
||||
@@ -1769,7 +1813,7 @@ struct SettingsView: View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
Button(action: {
|
||||
EventLogger.log(event: "show_eula")
|
||||
AnalyticsManager.shared.track(.eulaViewed)
|
||||
openURL(URL(string: "https://feels.app/eula.html")!)
|
||||
}, label: {
|
||||
Text(String(localized: "settings_view_show_eula"))
|
||||
@@ -1785,7 +1829,7 @@ struct SettingsView: View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
Button(action: {
|
||||
EventLogger.log(event: "show_privacy")
|
||||
AnalyticsManager.shared.track(.privacyPolicyViewed)
|
||||
openURL(URL(string: "https://feels.app/privacy.html")!)
|
||||
}, label: {
|
||||
Text(String(localized: "settings_view_show_privacy"))
|
||||
@@ -1796,7 +1840,47 @@ struct SettingsView: View {
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
}
|
||||
|
||||
|
||||
private var analyticsToggle: some View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: "chart.bar.xaxis")
|
||||
.font(.title2)
|
||||
.foregroundColor(.accentColor)
|
||||
.frame(width: 32)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Share Analytics")
|
||||
.foregroundColor(textColor)
|
||||
|
||||
Text("Help improve Feels by sharing anonymous usage data")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Toggle("", isOn: Binding(
|
||||
get: { !AnalyticsManager.shared.isOptedOut },
|
||||
set: { enabled in
|
||||
if enabled {
|
||||
AnalyticsManager.shared.optIn()
|
||||
} else {
|
||||
AnalyticsManager.shared.optOut()
|
||||
}
|
||||
}
|
||||
))
|
||||
.labelsHidden()
|
||||
.accessibilityLabel("Share Analytics")
|
||||
.accessibilityHint("Toggle anonymous usage analytics")
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
}
|
||||
|
||||
private var canDelete: some View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
@@ -1804,7 +1888,7 @@ struct SettingsView: View {
|
||||
Toggle(String(localized: "settings_use_delete_enable"),
|
||||
isOn: $deleteEnabled)
|
||||
.onChange(of: deleteEnabled) { _, newValue in
|
||||
EventLogger.log(event: "toggle_can_delete", withData: ["value": newValue])
|
||||
AnalyticsManager.shared.track(.deleteToggleChanged(enabled: newValue))
|
||||
}
|
||||
.foregroundColor(textColor)
|
||||
.padding()
|
||||
@@ -1819,7 +1903,7 @@ struct SettingsView: View {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
Button(action: {
|
||||
showingExporter.toggle()
|
||||
EventLogger.log(event: "export_data", withData: ["title": "default"])
|
||||
AnalyticsManager.shared.track(.exportTapped)
|
||||
}, label: {
|
||||
Text("Export")
|
||||
.foregroundColor(textColor)
|
||||
@@ -1835,7 +1919,7 @@ struct SettingsView: View {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
Button(action: {
|
||||
showingImporter.toggle()
|
||||
EventLogger.log(event: "import_data", withData: ["title": "default"])
|
||||
AnalyticsManager.shared.track(.importTapped)
|
||||
}, label: {
|
||||
Text("Import")
|
||||
.foregroundColor(textColor)
|
||||
|
||||
@@ -98,8 +98,7 @@ struct SwitchableView: View {
|
||||
.onTapGesture {
|
||||
viewType = viewType.next()
|
||||
self.headerTypeChanged(viewType)
|
||||
EventLogger.log(event: "switchable_view_header_changed",
|
||||
withData: ["view_type_id": viewType.rawValue, "days_back": daysBack])
|
||||
AnalyticsManager.shared.track(.viewHeaderChanged(header: String(describing: viewType)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,7 +273,7 @@ struct YearView: View {
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showSubscriptionStore) {
|
||||
FeelsSubscriptionStoreView()
|
||||
FeelsSubscriptionStoreView(source: "year_gate")
|
||||
}
|
||||
.sheet(item: $sharePickerData) { data in
|
||||
SharingStylePickerView(title: data.title, designs: data.designs)
|
||||
|
||||
Reference in New Issue
Block a user