TelemetryMigrationSheetView.swift 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. import FirebaseCrashlytics
  2. import SwiftUI
  3. /// One-shot sheet shown on first foreground for users who completed onboarding
  4. /// before telemetry existed. Mirrors the onboarding `DiagnosticsStepView`
  5. /// chooser but is presented standalone, with a Privacy-Policy acceptance gate
  6. /// and no "skip" path — the user must explicitly pick one of the three options.
  7. ///
  8. /// Once dismissed, `telemetryConsentDecisionMade` is set to `true` so the sheet
  9. /// never re-appears for this install.
  10. struct TelemetryMigrationSheetView: View {
  11. @Environment(\.dismiss) private var dismiss
  12. @Environment(\.openURL) private var openURL
  13. @State private var selectedOption: DiagnosticsSharingOption = .full
  14. // User already accepted the Privacy Policy during onboarding. This toggle
  15. // is a re-acknowledgment that the policy has been updated to cover the new
  16. // telemetry section — pre-checked so Continue works out of the box; users
  17. // who want to read the updated policy can uncheck and tap the link.
  18. @State private var hasAcceptedPrivacyPolicy: Bool = false
  19. var onDecision: (() -> Void)?
  20. var body: some View {
  21. NavigationView {
  22. ScrollView {
  23. VStack(alignment: .leading, spacing: 20) {
  24. Text("Help us improve Trio")
  25. .font(.title2)
  26. .bold()
  27. Text(
  28. "Until now, Trio could only sent crash reports. You can now also share anonymous usage statistics — things like your iPhone and iOS version, and which pump and CGM you have paired. This helps the Trio team prioritize what to fix and improve next."
  29. )
  30. .font(.subheadline)
  31. Text(
  32. "Your glucose data, therapy settings, credentials, and logs always stay on your device. Pick what you'd like to share — you can change this any time in Settings → App Diagnostics."
  33. )
  34. .font(.footnote)
  35. .foregroundColor(.secondary)
  36. ForEach(DiagnosticsSharingOption.allCases, id: \.self) { option in
  37. Button(action: {
  38. selectedOption = option
  39. }) {
  40. HStack(alignment: .top, spacing: 12) {
  41. Image(systemName: selectedOption == option ? "largecircle.fill.circle" : "circle")
  42. .foregroundColor(selectedOption == option ? .accentColor : .secondary)
  43. .imageScale(.large)
  44. VStack(alignment: .leading, spacing: 4) {
  45. Text(option.displayName)
  46. .foregroundColor(.primary)
  47. .bold()
  48. Text(option.caption)
  49. .font(.footnote)
  50. .foregroundColor(.secondary)
  51. }
  52. Spacer()
  53. }
  54. .padding()
  55. .background(Color(.secondarySystemBackground))
  56. .cornerRadius(10)
  57. }
  58. .buttonStyle(.plain)
  59. }
  60. Toggle(isOn: $hasAcceptedPrivacyPolicy) {
  61. HStack {
  62. Text("I have read and accept the")
  63. Button("Privacy Policy") {
  64. if let url = URL(string: "https://github.com/nightscout/Trio/blob/dev/PRIVACY_POLICY.md") {
  65. openURL(url)
  66. }
  67. }
  68. .foregroundColor(.accentColor)
  69. .underline()
  70. }
  71. .font(.footnote)
  72. }
  73. .toggleStyle(CheckboxToggleStyle(tint: Color.accentColor))
  74. .disabled(selectedOption == .disabled)
  75. .opacity(selectedOption == .disabled ? 0.35 : 1)
  76. NavigationLink {
  77. TelemetryPreviewView()
  78. } label: {
  79. Label("See exactly what's sent", systemImage: "doc.text.magnifyingglass")
  80. }
  81. .padding(.top, 4)
  82. }
  83. .padding()
  84. Spacer()
  85. Button {
  86. confirm()
  87. } label: {
  88. Text("Confirm").bold().frame(maxWidth: .infinity, minHeight: 30, alignment: .center)
  89. }
  90. .buttonStyle(.borderedProminent)
  91. .disabled(selectedOption != .disabled && !hasAcceptedPrivacyPolicy)
  92. .padding(.top)
  93. .padding(.horizontal)
  94. }
  95. .navigationTitle("Improved Diagnostics")
  96. .navigationBarTitleDisplayMode(.inline)
  97. .toolbar {
  98. ToolbarItem(placement: .principal) {
  99. HStack(spacing: 6) {
  100. Text("NEW")
  101. .font(.caption2)
  102. .bold()
  103. .foregroundColor(.white)
  104. .padding(.horizontal, 6)
  105. .padding(.vertical, 2)
  106. .background(Color.accentColor)
  107. .clipShape(Capsule())
  108. Text("Improved Diagnostics")
  109. .font(.headline)
  110. }
  111. }
  112. }
  113. .interactiveDismissDisabled(true)
  114. }
  115. }
  116. private func confirm() {
  117. let wasTelemetryOn = PropertyPersistentFlags.shared.telemetryEnabled == true
  118. PropertyPersistentFlags.shared.diagnosticsSharingEnabled = selectedOption.crashlyticsEnabled
  119. PropertyPersistentFlags.shared.telemetryEnabled = selectedOption.telemetryEnabled
  120. PropertyPersistentFlags.shared.telemetryConsentDecisionMade = true
  121. Crashlytics.crashlytics().setCrashlyticsCollectionEnabled(selectedOption.crashlyticsEnabled)
  122. if selectedOption.telemetryEnabled, !wasTelemetryOn {
  123. TelemetryClient.shared.scheduleRecurring()
  124. Task.detached { await TelemetryClient.shared.maybeSend() }
  125. }
  126. onDecision?()
  127. dismiss()
  128. }
  129. }