@@ -227,6 +227,9 @@ class TunnelManager: ObservableObject {
227227 self . tunnelStatus = . error
228228 }
229229 VPNLogger . shared. log ( " VPN status updated: \( self . tunnelStatus. rawValue) " )
230+ if connectionStatus == . connected && heartbeatStartPending {
231+ startHeartbeatInBackground ( )
232+ }
230233 }
231234 }
232235
@@ -295,8 +298,8 @@ class TunnelManager: ObservableObject {
295298 }
296299
297300 private func startExistingVPN( manager: NETunnelProviderManager ) {
298- guard tunnelStatus != . connected else {
299- VPNLogger . shared. log ( " Network tunnel is already connected " )
301+ guard tunnelStatus == . disconnected || tunnelStatus == . error else {
302+ VPNLogger . shared. log ( " Ignoring VPN start; current status: \( tunnelStatus . rawValue ) " )
300303 return
301304 }
302305 tunnelStatus = . connecting
@@ -483,12 +486,15 @@ class DNSChecker: ObservableObject {
483486
484487// Global state variable for the heartbeat response.
485488var pubHeartBeat = false
489+ private var heartbeatStartPending = false
490+ private var heartbeatStartInProgress = false
486491
487492@main
488493struct HeartbeatApp : App {
489494 @AppStorage ( " hasLaunchedBefore " ) var hasLaunchedBefore : Bool = false
490495 @AppStorage ( " customAccentColor " ) private var customAccentColorHex : String = " "
491496 @AppStorage ( " appTheme " ) private var appThemeRaw : String = AppTheme . system. rawValue
497+ @AppStorage ( " autoStartVPN " ) private var autoStartVPN = true
492498 @State private var showWelcomeSheet : Bool = false
493499 @State private var show_alert = false
494500 @State private var alert_string = " "
@@ -551,6 +557,14 @@ struct HeartbeatApp: App {
551557 }
552558 }
553559
560+ private func triggerAutoVPNStartIfNeeded( ) {
561+ guard autoStartVPN else { return }
562+ let manager = TunnelManager . shared
563+ if manager. tunnelStatus == . disconnected || manager. tunnelStatus == . error {
564+ manager. startVPN ( )
565+ }
566+ }
567+
554568 private var globalAccent : Color {
555569 themeExpansionManager. resolvedAccentColor ( from: customAccentColorHex)
556570 }
@@ -599,7 +613,7 @@ struct HeartbeatApp: App {
599613 if !hasLaunchedBefore {
600614 showWelcomeSheet = true
601615 } else {
602- TunnelManager . shared . startVPN ( )
616+ triggerAutoVPNStartIfNeeded ( )
603617 }
604618 HeartbeatApp . updateUIKitTint ( customHex: customAccentColorHex,
605619 hasAccess: themeExpansionManager. hasThemeExpansion)
@@ -613,10 +627,10 @@ struct HeartbeatApp: App {
613627 }
614628 . sheet ( isPresented: $showWelcomeSheet) {
615629 WelcomeSheetView {
616- // When the user taps "Continue", mark the app as launched and start the VPN.
630+ // When the user taps "Continue", mark the app as launched and start the VPN if allowed .
617631 hasLaunchedBefore = true
618632 showWelcomeSheet = false
619- TunnelManager . shared . startVPN ( )
633+ triggerAutoVPNStartIfNeeded ( )
620634 }
621635 }
622636 }
@@ -653,8 +667,11 @@ class MountingProgress: ObservableObject {
653667 @Published var coolisMounted : Bool = false
654668
655669 func checkforMounted( ) {
656- DispatchQueue . main. async {
657- self . coolisMounted = isMounted ( )
670+ DispatchQueue . global ( qos: . utility) . async {
671+ let mounted = isMounted ( )
672+ DispatchQueue . main. async {
673+ self . coolisMounted = mounted
674+ }
658675 }
659676 }
660677
@@ -671,10 +688,21 @@ class MountingProgress: ObservableObject {
671688 }
672689
673690 private func mount( ) {
674- self . coolisMounted = isMounted ( )
691+ guard TunnelManager . shared. tunnelStatus == . connected else {
692+ DispatchQueue . main. async {
693+ self . coolisMounted = false
694+ self . mountingThread = nil
695+ }
696+ return
697+ }
698+
699+ let currentlyMounted = isMounted ( )
700+ DispatchQueue . main. async {
701+ self . coolisMounted = currentlyMounted
702+ }
675703 let pairingpath = URL . documentsDirectory. appendingPathComponent ( " pairingFile.plist " ) . path
676704
677- if isPairing ( ) , !isMounted ( ) {
705+ if isPairing ( ) , !currentlyMounted {
678706 if let mountingThread = mountingThread {
679707 mountingThread. cancel ( )
680708 self . mountingThread = nil
@@ -697,7 +725,8 @@ class MountingProgress: ObservableObject {
697725 }
698726 }
699727 } else {
700- self . coolisMounted = isMounted ( )
728+ self . coolisMounted = true
729+ self . checkforMounted ( )
701730 }
702731 self . mountingThread = nil
703732 }
@@ -724,8 +753,37 @@ func isPairing() -> Bool {
724753 return true
725754}
726755
727- func startHeartbeatInBackground( ) {
756+ func startHeartbeatInBackground( requireVPNConnection: Bool = true ) {
757+ assert ( Thread . isMainThread, " startHeartbeatInBackground must be called on the main thread " )
758+ let pairingFileURL = URL . documentsDirectory. appendingPathComponent ( " pairingFile.plist " )
759+
760+ guard FileManager . default. fileExists ( atPath: pairingFileURL. path) else {
761+ heartbeatStartPending = false
762+ return
763+ }
764+
765+ let vpnConnected = TunnelManager . shared. tunnelStatus == . connected
766+ if requireVPNConnection && !vpnConnected {
767+ if !heartbeatStartPending {
768+ print ( " Heartbeat start deferred until VPN connects " )
769+ }
770+ heartbeatStartPending = true
771+ return
772+ }
773+
774+ guard !heartbeatStartInProgress else {
775+ return
776+ }
777+
778+ heartbeatStartPending = false
779+ heartbeatStartInProgress = true
780+
728781 let heartBeatThread = Thread {
782+ defer {
783+ DispatchQueue . main. async {
784+ heartbeatStartInProgress = false
785+ }
786+ }
729787 let completionHandler : @convention ( block) ( Int32 , String ? ) -> Void = { result, message in
730788 if result == 0 {
731789 print ( " Heartbeat started successfully: \( message ?? " " ) " )
@@ -762,7 +820,9 @@ func startHeartbeatInBackground() {
762820 showTryAgain: true
763821 ) { shouldTryAgain in
764822 if shouldTryAgain {
765- startHeartbeatInBackground ( )
823+ DispatchQueue . main. async {
824+ startHeartbeatInBackground ( )
825+ }
766826 }
767827 }
768828 }
0 commit comments