فهرست منبع

Add new module AppDiagnostics
* New module for user to opt-in/-out of diagnostics sharing outside of onboarding
* Module under Settings > Features > App Diagnostics
* Refactored diagnostics sharing storage from binary property list file to standard UserDefault entry

Deniz Cengiz 1 سال پیش
والد
کامیت
61258aaf8b

+ 32 - 0
Trio.xcodeproj/project.pbxproj

@@ -640,6 +640,10 @@
 		DDF68FFC2D9ECF7F008BF16C /* OnboardingStateModel+Nightscout.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF68FFB2D9ECF77008BF16C /* OnboardingStateModel+Nightscout.swift */; };
 		DDF6902C2DA028D3008BF16C /* DiagnosticsStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF6902B2DA028D3008BF16C /* DiagnosticsStepView.swift */; };
 		DDF6905C2DA0AFC5008BF16C /* WelcomeStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF6905B2DA0AFC5008BF16C /* WelcomeStepView.swift */; };
+		DDF691012DA2CA11008BF16C /* AppDiagnosticsDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF691002DA2CA0B008BF16C /* AppDiagnosticsDataFlow.swift */; };
+		DDF691032DA2CA1E008BF16C /* AppDiagnosticsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF691022DA2CA14008BF16C /* AppDiagnosticsProvider.swift */; };
+		DDF691052DA2CA23008BF16C /* AppDiagnosticsStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF691042DA2CA20008BF16C /* AppDiagnosticsStateModel.swift */; };
+		DDF691072DA2CA2D008BF16C /* AppDiagnosticsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF691062DA2CA28008BF16C /* AppDiagnosticsRootView.swift */; };
 		DDF847DD2C5C28720049BB3B /* LiveActivitySettingsDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF847DC2C5C28720049BB3B /* LiveActivitySettingsDataFlow.swift */; };
 		DDF847DF2C5C28780049BB3B /* LiveActivitySettingsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF847DE2C5C28780049BB3B /* LiveActivitySettingsProvider.swift */; };
 		DDF847E12C5C287F0049BB3B /* LiveActivitySettingsStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF847E02C5C287F0049BB3B /* LiveActivitySettingsStateModel.swift */; };
@@ -1425,6 +1429,10 @@
 		DDF68FFB2D9ECF77008BF16C /* OnboardingStateModel+Nightscout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OnboardingStateModel+Nightscout.swift"; sourceTree = "<group>"; };
 		DDF6902B2DA028D3008BF16C /* DiagnosticsStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagnosticsStepView.swift; sourceTree = "<group>"; };
 		DDF6905B2DA0AFC5008BF16C /* WelcomeStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeStepView.swift; sourceTree = "<group>"; };
+		DDF691002DA2CA0B008BF16C /* AppDiagnosticsDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDiagnosticsDataFlow.swift; sourceTree = "<group>"; };
+		DDF691022DA2CA14008BF16C /* AppDiagnosticsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDiagnosticsProvider.swift; sourceTree = "<group>"; };
+		DDF691042DA2CA20008BF16C /* AppDiagnosticsStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDiagnosticsStateModel.swift; sourceTree = "<group>"; };
+		DDF691062DA2CA28008BF16C /* AppDiagnosticsRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDiagnosticsRootView.swift; sourceTree = "<group>"; };
 		DDF847DC2C5C28720049BB3B /* LiveActivitySettingsDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivitySettingsDataFlow.swift; sourceTree = "<group>"; };
 		DDF847DE2C5C28780049BB3B /* LiveActivitySettingsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivitySettingsProvider.swift; sourceTree = "<group>"; };
 		DDF847E02C5C287F0049BB3B /* LiveActivitySettingsStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivitySettingsStateModel.swift; sourceTree = "<group>"; };
@@ -1785,6 +1793,7 @@
 		3811DE0325C9D31700A708ED /* Modules */ = {
 			isa = PBXGroup;
 			children = (
+				DDF690FE2DA2C9EE008BF16C /* AppDiagnostics */,
 				BD47FD142D88AACC0043966B /* Onboarding */,
 				DDD163032C4C67B400CD525A /* Adjustments */,
 				DD1745382C55BF8B00211FAC /* AlgorithmAdvancedSettings */,
@@ -3400,6 +3409,25 @@
 			path = "Classes+Properties";
 			sourceTree = "<group>";
 		};
+		DDF690FE2DA2C9EE008BF16C /* AppDiagnostics */ = {
+			isa = PBXGroup;
+			children = (
+				DDF691042DA2CA20008BF16C /* AppDiagnosticsStateModel.swift */,
+				DDF691022DA2CA14008BF16C /* AppDiagnosticsProvider.swift */,
+				DDF691002DA2CA0B008BF16C /* AppDiagnosticsDataFlow.swift */,
+				DDF690FF2DA2CA03008BF16C /* View */,
+			);
+			path = AppDiagnostics;
+			sourceTree = "<group>";
+		};
+		DDF690FF2DA2CA03008BF16C /* View */ = {
+			isa = PBXGroup;
+			children = (
+				DDF691062DA2CA28008BF16C /* AppDiagnosticsRootView.swift */,
+			);
+			path = View;
+			sourceTree = "<group>";
+		};
 		DDF847DB2C5C28550049BB3B /* LiveActivitySettings */ = {
 			isa = PBXGroup;
 			children = (
@@ -4011,6 +4039,7 @@
 				DD1745482C55C61D00211FAC /* AutosensSettingsStateModel.swift in Sources */,
 				DD1745462C55C61500211FAC /* AutosensSettingsProvider.swift in Sources */,
 				DDA6E2852D2361F800C2988C /* LoopStatusView.swift in Sources */,
+				DDF691012DA2CA11008BF16C /* AppDiagnosticsDataFlow.swift in Sources */,
 				DDA6E3202D258E0500C2988C /* OverrideHelpView.swift in Sources */,
 				DDA6E2502D22187500C2988C /* ChartLegendView.swift in Sources */,
 				3811DEAF25C9D88300A708ED /* KeyValueStorage.swift in Sources */,
@@ -4110,6 +4139,7 @@
 				DD940BAC2CA75889000830A5 /* DynamicGlucoseColor.swift in Sources */,
 				3883581C25EE79BB00E024B2 /* TextFieldWithToolBar.swift in Sources */,
 				58D08B302C8DEA7500AA37D3 /* ForecastView.swift in Sources */,
+				DDF691072DA2CA2D008BF16C /* AppDiagnosticsRootView.swift in Sources */,
 				6B1A8D2E2B156EEF00E76752 /* LiveActivityManager.swift in Sources */,
 				581516A92BCEEDF800BF67D7 /* NSPredicates.swift in Sources */,
 				DD4FFF332D458EE600B6CFF9 /* GarminWatchState.swift in Sources */,
@@ -4334,6 +4364,7 @@
 				58645B992CA2D1A4008AFCE7 /* GlucoseSetup.swift in Sources */,
 				7BCFACB97C821041BA43A114 /* ManualTempBasalRootView.swift in Sources */,
 				38E44534274E411700EC9A94 /* Disk+InternalHelpers.swift in Sources */,
+				DDF691052DA2CA23008BF16C /* AppDiagnosticsStateModel.swift in Sources */,
 				38A00B2325FC2B55006BC0B0 /* LRUCache.swift in Sources */,
 				DDD163122C4C689900CD525A /* AdjustmentsStateModel.swift in Sources */,
 				BD47FDD72D8B64D20043966B /* CarbRatioStepView.swift in Sources */,
@@ -4366,6 +4397,7 @@
 				DD88C8E22C50420800F2D558 /* DefinitionRow.swift in Sources */,
 				B7C465E9472624D8A2BE2A6A /* (null) in Sources */,
 				71D44AAB2CA5F5EA0036EE9E /* AlertPermissionsChecker.swift in Sources */,
+				DDF691032DA2CA1E008BF16C /* AppDiagnosticsProvider.swift in Sources */,
 				320D030F724170A637F06D50 /* (null) in Sources */,
 				19E1F7E829D082D0005C8D20 /* IconConfigDataFlow.swift in Sources */,
 				5A2325522BFCBF55003518CA /* NightscoutUploadView.swift in Sources */,

+ 6 - 0
Trio/Sources/Localizations/Main/Localizable.xcstrings

@@ -36667,6 +36667,9 @@
         }
       }
     },
+    "Anonymized Data Sharing" : {
+
+    },
     "API secret" : {
       "comment" : "API secret in NS",
       "localizations" : {
@@ -36768,6 +36771,9 @@
         }
       }
     },
+    "App Diagnostics" : {
+
+    },
     "App Expires" : {
       "localizations" : {
         "bg" : {

+ 5 - 0
Trio/Sources/Modules/AppDiagnostics/AppDiagnosticsDataFlow.swift

@@ -0,0 +1,5 @@
+enum AppDiagnostics {
+    enum Config {}
+}
+
+protocol AppDiagnosticsProvider {}

+ 3 - 0
Trio/Sources/Modules/AppDiagnostics/AppDiagnosticsProvider.swift

@@ -0,0 +1,3 @@
+extension AppDiagnostics {
+    final class Provider: BaseProvider, AppDiagnosticsProvider {}
+}

+ 33 - 0
Trio/Sources/Modules/AppDiagnostics/AppDiagnosticsStateModel.swift

@@ -0,0 +1,33 @@
+import Observation
+import SwiftUI
+
+extension AppDiagnostics {
+    @Observable final class StateModel: BaseStateModel<Provider> {
+        // MARK: - Diagnostics Sharing Option
+
+        var diagnosticsSharingOption: DiagnosticsSharingOption = .enabled
+
+        override func subscribe() {
+            loadDiagnostics()
+        }
+
+        /// Loads the diagnostics sharing option from UserDefaults as a boolean.
+        func loadDiagnostics() {
+            if let storedDiagnosticsSharingOption = UserDefaults.standard.value(forKey: "DiagnosticsSharing") as? Bool {
+                diagnosticsSharingOption = storedDiagnosticsSharingOption ? .enabled : .disabled
+            } else {
+                diagnosticsSharingOption = .enabled
+            }
+        }
+
+        /// Persists the current diagnostics sharing option to UserDefaults as a boolean.
+        func applyDiagnostics() {
+            let booleanValue: Bool = diagnosticsSharingOption == .enabled
+            UserDefaults.standard.set(booleanValue, forKey: "DiagnosticsSharing")
+        }
+    }
+}
+
+extension AppDiagnostics.StateModel: SettingsObserver {
+    func settingsDidChange(_: TrioSettings) {}
+}

+ 82 - 0
Trio/Sources/Modules/AppDiagnostics/View/AppDiagnosticsRootView.swift

@@ -0,0 +1,82 @@
+import SwiftUI
+import Swinject
+
+extension AppDiagnostics {
+    struct RootView: BaseView {
+        let resolver: Resolver
+
+        @StateObject var state = StateModel()
+
+        @Environment(\.colorScheme) var colorScheme
+        @Environment(AppState.self) var appState
+
+        var body: some View {
+            List {
+                Section(
+                    header: Text("Anonymized Data Sharing"),
+                    content: {
+                        VStack {
+                            ForEach(DiagnosticsSharingOption.allCases, id: \.self) { option in
+                                Button(action: {
+                                    state.diagnosticsSharingOption = option
+                                }) {
+                                    HStack {
+                                        Image(
+                                            systemName: state
+                                                .diagnosticsSharingOption == option ? "largecircle.fill.circle" : "circle"
+                                        )
+                                        .foregroundColor(state.diagnosticsSharingOption == option ? .accentColor : .secondary)
+                                        .imageScale(.large)
+
+                                        Text(option.displayName)
+                                            .foregroundColor(.primary)
+
+                                        Spacer()
+                                    }
+                                    .background(Color.chart.opacity(0.65))
+                                    .cornerRadius(10)
+                                }
+                                .buttonStyle(.plain)
+                            }
+                            .padding()
+                        }
+                        .onChange(of: state.diagnosticsSharingOption, { _, _ in
+                            state.applyDiagnostics()
+                        })
+                    }
+                ).listRowBackground(Color.chart)
+
+                Section {
+                    VStack(alignment: .leading, spacing: 8) {
+                        Text("Why does Trio collect this data?").bold()
+                        VStack(alignment: .leading, spacing: 4) {
+                            Text(
+                                "•  App diagnostic insights help us enhance app stability, ensure safety for all users, and enable us to quickly identify and resolve critical issues."
+                            )
+                            Text(
+                                "•  Trio collects the app's state on crash, device, iOS and general system info, and a stack trace."
+                            )
+                            Text(
+                                "•  Trio does not collect any health related data, e.g. glucose readings, insulin rates or doses, meal data, setting values, or similar."
+                            )
+                            Text(
+                                "•  Trio does not track any usage metrics or any other personal data about users other than the used iPhone model and iOS version."
+                            )
+                        }
+                        Text(
+                            "Diagnostics are sent to a Google Firebase Crashlytics project, which is securely maintained and accessed only by the Trio team."
+                        )
+                    }
+                    .font(.footnote)
+                    .multilineTextAlignment(.leading)
+                    .foregroundStyle(Color.secondary)
+                }.listRowBackground(Color.clear)
+            }
+            .listSectionSpacing(sectionSpacing)
+            .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
+            .onAppear(perform: configureView)
+            .navigationBarTitle("App Diagnostics")
+            .navigationBarTitleDisplayMode(.automatic)
+        }
+    }
+}

+ 3 - 3
Trio/Sources/Modules/Onboarding/OnboardingStateModel.swift

@@ -397,10 +397,10 @@ extension Onboarding {
             saveISFValues()
         }
 
-        /// Persists the current diagnostics sharing option to a local property list file.
+        /// Persists the current diagnostics sharing option to UserDefaults as a boolean.
         func applyDiagnostics() {
-            @PersistedProperty(key: "DiagnosticsSharingState") var storedDiagnosticsOption: String?
-            storedDiagnosticsOption = diagnosticsSharingOption.rawValue
+            let booleanValue: Bool = diagnosticsSharingOption == .enabled
+            UserDefaults.standard.set(booleanValue, forKey: "DiagnosticsSharing")
         }
 
         /// Applies the selected glucose units to the app's settings.

+ 8 - 0
Trio/Sources/Modules/Settings/View/Subviews/FeatureSettingsView.swift

@@ -37,6 +37,14 @@ struct FeatureSettingsView: BaseView {
                 }
             )
             .listRowBackground(Color.chart)
+
+            Section(
+                header: Text("Anonymized Data Sharing"),
+                content: {
+                    Text("App Diagnostics").navigationLink(to: .appDiagnostics, from: self)
+                }
+            )
+            .listRowBackground(Color.chart)
         }
         .scrollContentBackground(.hidden)
         .background(appState.trioBackgroundColor(for: colorScheme))

+ 3 - 0
Trio/Sources/Router/Screen.swift

@@ -48,6 +48,7 @@ enum Screen: Identifiable, Hashable {
     case targetBehavior
     case algorithmAdvancedSettings
     case unitsAndLimits
+    case appDiagnostics
 
     var id: Int { String(reflecting: self).hashValue }
 }
@@ -147,6 +148,8 @@ extension Screen {
             AlgorithmAdvancedSettings.RootView(resolver: resolver)
         case .unitsAndLimits:
             UnitsLimitsSettings.RootView(resolver: resolver)
+        case .appDiagnostics:
+            AppDiagnostics.RootView(resolver: resolver)
         }
     }