|
|
@@ -0,0 +1,142 @@
|
|
|
+//
|
|
|
+// BluetoothPermissionStepView.swift
|
|
|
+// Trio
|
|
|
+//
|
|
|
+// Created by Cengiz Deniz on 18.04.25.
|
|
|
+//
|
|
|
+import CoreBluetooth
|
|
|
+import SwiftUI
|
|
|
+import UIKit
|
|
|
+
|
|
|
+struct BluetoothPermissionStepView: View {
|
|
|
+ @Bindable var state: Onboarding.StateModel
|
|
|
+ var bluetoothManager: BluetoothStateManager
|
|
|
+ var currentStep: Binding<OnboardingStep>
|
|
|
+
|
|
|
+ var body: some View {
|
|
|
+ VStack(alignment: .leading, spacing: 20) {
|
|
|
+ Text("Enable device connectivity")
|
|
|
+ .font(.title3)
|
|
|
+ .bold()
|
|
|
+ .multilineTextAlignment(.leading)
|
|
|
+
|
|
|
+ Text("Trio requires Bluetooth to function as a (hybrid) closed‑loop system.")
|
|
|
+ .font(.body)
|
|
|
+ .multilineTextAlignment(.leading)
|
|
|
+ .foregroundColor(Color.secondary)
|
|
|
+ .padding(.bottom)
|
|
|
+
|
|
|
+ VStack(alignment: .leading, spacing: 20) {
|
|
|
+ HStack(spacing: 12) {
|
|
|
+ Image(systemName: "keyboard.onehanded.left.fill")
|
|
|
+ .font(.system(size: 24))
|
|
|
+ .foregroundColor(Color.bgDarkBlue)
|
|
|
+ .frame(width: 44, height: 44)
|
|
|
+ .background(Circle().fill(Color.primary.opacity(0.8)))
|
|
|
+ Text(
|
|
|
+ "Connect to your insulin pump so Trio can send dosing commands and stay active in the background."
|
|
|
+ )
|
|
|
+ .font(.body)
|
|
|
+ .foregroundColor(.primary)
|
|
|
+ }
|
|
|
+
|
|
|
+ HStack(spacing: 12) {
|
|
|
+ Image(systemName: "sensor.tag.radiowaves.forward.fill")
|
|
|
+ .font(.system(size: 24))
|
|
|
+ .foregroundColor(Color.bgDarkBlue)
|
|
|
+ .frame(width: 44, height: 44)
|
|
|
+ .background(Circle().fill(Color.primary.opacity(0.8)))
|
|
|
+ Text("Receive glucose readings every 5 minutes from your CGM to keep the loop running.")
|
|
|
+ .font(.body)
|
|
|
+ .foregroundColor(.primary)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ Text("You can change these permissions any time in the iOS Settings app.")
|
|
|
+ .font(.footnote)
|
|
|
+ .multilineTextAlignment(.leading)
|
|
|
+ .foregroundColor(Color.secondary)
|
|
|
+ .padding(.top)
|
|
|
+ }
|
|
|
+ .padding(.horizontal)
|
|
|
+ .background(
|
|
|
+ SystemAlert(
|
|
|
+ isPresented: $state.shouldDisplayBluetoothRequestAlert,
|
|
|
+ title: String(localized: "“Trio” Would Like to Use Bluetooth"),
|
|
|
+ message: String(
|
|
|
+ localized: "Bluetooth is used to communicate with insulin pump and continuous glucose monitor devices."
|
|
|
+ ),
|
|
|
+ allowTitle: String(localized: "Allow"),
|
|
|
+ denyTitle: String(localized: "Don’t Allow"),
|
|
|
+ onAllow: {
|
|
|
+ bluetoothManager.authorizeBluetooth { auth in
|
|
|
+ DispatchQueue.main.async {
|
|
|
+ state.hasBluetoothGranted = (auth == .authorized)
|
|
|
+ state.shouldDisplayBluetoothRequestAlert = false
|
|
|
+ if let next = currentStep.wrappedValue.next {
|
|
|
+ currentStep.wrappedValue = next
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ onDeny: {
|
|
|
+ state.hasBluetoothGranted = false
|
|
|
+ state.shouldDisplayBluetoothRequestAlert = false
|
|
|
+ if let next = currentStep.wrappedValue.next {
|
|
|
+ currentStep.wrappedValue = next
|
|
|
+ }
|
|
|
+ }
|
|
|
+ )
|
|
|
+ )
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/// Presents a real UIAlertController, pinned to the system's own style
|
|
|
+///
|
|
|
+/// Why use this?
|
|
|
+/// SwiftUI’s built‑in .alert will always inherit the color scheme of its host view (in our case, we have forced .dark for the entire onboarding screen).
|
|
|
+/// There’s no way to tell SwiftUI “use the system setting here only for this one alert.”
|
|
|
+/// The workaround is to present a plain UIKit UIAlertController ourself, in its own representable, and explicitly tell it to use the system’s interface style instead of inheriting our forced dark mode.
|
|
|
+/// We enforce usage of the system's interface style by setting its overrideUserInterfaceStyle to whatever the device is actually using (.light or .dark).
|
|
|
+struct SystemAlert: UIViewControllerRepresentable {
|
|
|
+ @Binding var isPresented: Bool
|
|
|
+
|
|
|
+ let title: String
|
|
|
+ let message: String
|
|
|
+ let allowTitle: String
|
|
|
+ let denyTitle: String
|
|
|
+
|
|
|
+ /// called after Allow or Deny
|
|
|
+ let onAllow: () -> Void
|
|
|
+ let onDeny: () -> Void
|
|
|
+
|
|
|
+ func makeUIViewController(context _: Context) -> UIViewController {
|
|
|
+ // empty container
|
|
|
+ UIViewController()
|
|
|
+ }
|
|
|
+
|
|
|
+ func updateUIViewController(_ uiVC: UIViewController, context _: Context) {
|
|
|
+ guard isPresented, uiVC.presentedViewController == nil else { return }
|
|
|
+
|
|
|
+ let alert = UIAlertController(
|
|
|
+ title: title,
|
|
|
+ message: message,
|
|
|
+ preferredStyle: .alert
|
|
|
+ )
|
|
|
+
|
|
|
+ // force it back to the "real" system style
|
|
|
+ let systemStyle = UIScreen.main.traitCollection.userInterfaceStyle
|
|
|
+ alert.overrideUserInterfaceStyle = systemStyle
|
|
|
+
|
|
|
+ alert.addAction(.init(title: denyTitle, style: .cancel) { _ in
|
|
|
+ isPresented = false
|
|
|
+ onDeny()
|
|
|
+ })
|
|
|
+ alert.addAction(.init(title: allowTitle, style: .default) { _ in
|
|
|
+ isPresented = false
|
|
|
+ onAllow()
|
|
|
+ })
|
|
|
+
|
|
|
+ uiVC.present(alert, animated: true)
|
|
|
+ }
|
|
|
+}
|