From fc4b349d3652f3811652743479e541c080badb28 Mon Sep 17 00:00:00 2001
From: Ryan ZHAO <>
Date: Fri, 21 Mar 2025 10:19:52 +1100
Subject: [PATCH] add delay on permission chain for calls

---
 .../SendMediaNavigationController.swift       |  3 +-
 Session/Meta/AppDelegate.swift                |  5 +-
 Session/Open Groups/JoinOpenGroupVC.swift     |  3 +-
 .../Settings/PrivacySettingsViewModel.swift   | 16 +++---
 Session/Shared/ScanQRCodeScreen.swift         |  3 +-
 Session/Utilities/Permissions.swift           | 52 ++++++++++++++++---
 .../Utilities/Preferences.swift               |  4 ++
 .../Types/UserDefaultsType.swift              |  4 +-
 .../Utilities/Permissions.swift               |  5 --
 9 files changed, 69 insertions(+), 26 deletions(-)

diff --git a/Session/Media Viewing & Editing/SendMediaNavigationController.swift b/Session/Media Viewing & Editing/SendMediaNavigationController.swift
index 594cc1fa7..4b5efa4ca 100644
--- a/Session/Media Viewing & Editing/SendMediaNavigationController.swift	
+++ b/Session/Media Viewing & Editing/SendMediaNavigationController.swift	
@@ -147,7 +147,8 @@ class SendMediaNavigationController: UINavigationController {
     }
 
     private func didTapCameraModeButton() {
-        Permissions.requestCameraPermissionIfNeeded(using: dependencies) { [weak self] in
+        Permissions.requestCameraPermissionIfNeeded(using: dependencies) { [weak self] granted in
+            guard granted else { return }
             DispatchQueue.main.async {
                 self?.fadeTo(viewControllers: ((self?.captureViewController).map { [$0] } ?? []))
             }
diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift
index 49e4f9224..eed100801 100644
--- a/Session/Meta/AppDelegate.swift
+++ b/Session/Meta/AppDelegate.swift
@@ -133,6 +133,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
                     }
                 )
                 
+                /// Do a little migration on an educated guess of if the user has been asked for local network permission based on calls permission
+                dependencies[defaults: .standard, key: .hasRequestedLocalNetworkPermission] = dependencies[singleton: .storage, key: .areCallsEnabled]
+                
                 /// Now that the theme settings have been applied we can complete the migrations
                 self?.completePostMigrationSetup(calledFrom: .finishLaunching)
             },
@@ -291,7 +294,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
         // On every activation, clear old temp directories.
         dependencies[singleton: .fileManager].clearOldTemporaryDirectories()
         
-        if dependencies[singleton: .storage, key: .areCallsEnabled] {
+        if dependencies[singleton: .storage, key: .areCallsEnabled] && dependencies[defaults: .standard, key: .hasRequestedLocalNetworkPermission] {
             Permissions.checkLocalNetworkPermission(using: dependencies)
         }
     }
diff --git a/Session/Open Groups/JoinOpenGroupVC.swift b/Session/Open Groups/JoinOpenGroupVC.swift
index 34a5ab270..00ef3b118 100644
--- a/Session/Open Groups/JoinOpenGroupVC.swift	
+++ b/Session/Open Groups/JoinOpenGroupVC.swift	
@@ -613,7 +613,8 @@ private final class ScanQRCodePlaceholderVC: UIViewController {
     }
 
     @objc private func requestCameraAccess() {
-        Permissions.requestCameraPermissionIfNeeded(using: dependencies) { [weak self] in
+        Permissions.requestCameraPermissionIfNeeded(using: dependencies) { [weak self] granted in
+            guard granted else { return }
             self?.joinOpenGroupVC?.handleCameraAccessGranted()
         }
     }
diff --git a/Session/Settings/PrivacySettingsViewModel.swift b/Session/Settings/PrivacySettingsViewModel.swift
index 2b71ca0a3..18169e45f 100644
--- a/Session/Settings/PrivacySettingsViewModel.swift
+++ b/Session/Settings/PrivacySettingsViewModel.swift
@@ -90,6 +90,7 @@ class PrivacySettingsViewModel: SessionTableViewModel, NavigationItemSource, Nav
         let typingIndicatorsEnabled: Bool
         let areLinkPreviewsEnabled: Bool
         let areCallsEnabled: Bool
+        let localNetworkPermission: Bool
     }
     
     let title: String = "sessionPrivacy".localized()
@@ -102,7 +103,8 @@ class PrivacySettingsViewModel: SessionTableViewModel, NavigationItemSource, Nav
                 areReadReceiptsEnabled: db[.areReadReceiptsEnabled],
                 typingIndicatorsEnabled: db[.typingIndicatorsEnabled],
                 areLinkPreviewsEnabled: db[.areLinkPreviewsEnabled],
-                areCallsEnabled: db[.areCallsEnabled]
+                areCallsEnabled: db[.areCallsEnabled],
+                localNetworkPermission: db[.lastSeenHasLocalNetworkPermission]
             )
         }
         .mapWithPrevious { [dependencies] previous, current -> [SectionModel] in
@@ -132,9 +134,7 @@ class PrivacySettingsViewModel: SessionTableViewModel, NavigationItemSource, Nav
                                 confirmStyle: .danger,
                                 cancelStyle: .alert_text,
                                 onConfirm: { _ in
-                                    Permissions.requestMicrophonePermissionIfNeeded(using: dependencies)
-                                    Permissions.requestCameraPermissionIfNeeded(using: dependencies)
-                                    Permissions.requestLocalNetworkPermissionIfNeeded(using: dependencies)
+                                    Permissions.requestPermissionsForCalls(using: dependencies)
                                 }
                             ),
                             onTap: { [weak self] in
@@ -192,8 +192,8 @@ class PrivacySettingsViewModel: SessionTableViewModel, NavigationItemSource, Nav
                                         title: "Local Network",
                                         subtitle: "Allow access to local network to facilitate voice and video calls",
                                         trailingAccessory: .toggle(
-                                            Permissions.localNetwork(using: dependencies) == .granted,
-                                            oldValue: Permissions.localNetwork(using: dependencies) == .granted,
+                                            current.localNetworkPermission,
+                                            oldValue: (previous ?? current).localNetworkPermission,
                                             accessibility: Accessibility(
                                                 identifier: "Local Network Permission - Switch"
                                             )
@@ -406,9 +406,7 @@ class PrivacySettingsViewModel: SessionTableViewModel, NavigationItemSource, Nav
                     confirmStyle: .danger,
                     cancelStyle: .alert_text,
                     onConfirm: { [dependencies] _ in
-                        Permissions.requestMicrophonePermissionIfNeeded(using: dependencies)
-                        Permissions.requestCameraPermissionIfNeeded(using: dependencies)
-                        Permissions.requestLocalNetworkPermissionIfNeeded(using: dependencies)
+                        Permissions.requestPermissionsForCalls(using: dependencies)
                         dependencies[singleton: .storage].write { db in
                             try db.setAndUpdateConfig(
                                 .areCallsEnabled,
diff --git a/Session/Shared/ScanQRCodeScreen.swift b/Session/Shared/ScanQRCodeScreen.swift
index 52d533600..18a084a82 100644
--- a/Session/Shared/ScanQRCodeScreen.swift
+++ b/Session/Shared/ScanQRCodeScreen.swift
@@ -74,7 +74,8 @@ struct ScanQRCodeScreen: View {
     }
     
     private func requestCameraAccess() {
-        Permissions.requestCameraPermissionIfNeeded(using: dependencies) {
+        Permissions.requestCameraPermissionIfNeeded(using: dependencies) { granted in
+            guard granted else { return }
             hasCameraAccess.toggle()
         }
     }
diff --git a/Session/Utilities/Permissions.swift b/Session/Utilities/Permissions.swift
index 2e28c2582..09f16e8f8 100644
--- a/Session/Utilities/Permissions.swift
+++ b/Session/Utilities/Permissions.swift
@@ -13,11 +13,11 @@ extension Permissions {
     @discardableResult public static func requestCameraPermissionIfNeeded(
         presentingViewController: UIViewController? = nil,
         using dependencies: Dependencies,
-        onAuthorized: (() -> Void)? = nil
+        onAuthorized: ((Bool) -> Void)? = nil
     ) -> Bool {
         switch AVCaptureDevice.authorizationStatus(for: .video) {
             case .authorized:
-                onAuthorized?()
+                onAuthorized?(true)
                 return true
             
             case .denied, .restricted:
@@ -45,8 +45,8 @@ extension Permissions {
                 return false
                 
             case .notDetermined:
-                AVCaptureDevice.requestAccess(for: .video, completionHandler: { _ in
-                    onAuthorized?()
+                AVCaptureDevice.requestAccess(for: .video, completionHandler: { granted in
+                    onAuthorized?(granted)
                 })
                 return false
                 
@@ -57,6 +57,7 @@ extension Permissions {
     public static func requestMicrophonePermissionIfNeeded(
         presentingViewController: UIViewController? = nil,
         using dependencies: Dependencies,
+        onAuthorized: ((Bool) -> Void)? = nil,
         onNotGranted: (() -> Void)? = nil
     ) {
         let handlePermissionDenied: () -> Void = {
@@ -94,6 +95,7 @@ extension Permissions {
                     onNotGranted?()
                     AVAudioApplication.requestRecordPermission { granted in
                         dependencies[defaults: .appGroup, key: .lastSeenHasMicrophonePermission] = granted
+                        onAuthorized?(granted)
                     }
                 default: break
             }
@@ -105,6 +107,7 @@ extension Permissions {
                     onNotGranted?()
                     AVAudioSession.sharedInstance().requestRecordPermission { granted in
                         dependencies[defaults: .appGroup, key: .lastSeenHasMicrophonePermission] = granted
+                        onAuthorized?(granted)
                     }
                 default: break
             }
@@ -167,7 +170,15 @@ extension Permissions {
         }
     }
     
+    // MARK: - Local Network Premission
+    
+    public static func localNetwork(using dependencies: Dependencies) -> Status {
+        let status: Bool = dependencies[singleton: .storage, key: .lastSeenHasLocalNetworkPermission]
+        return status ? .granted : .denied
+    }
+    
     public static func requestLocalNetworkPermissionIfNeeded(using dependencies: Dependencies) {
+        dependencies[defaults: .standard, key: .hasRequestedLocalNetworkPermission] = true
         checkLocalNetworkPermission(using: dependencies)
     }
     
@@ -176,10 +187,14 @@ extension Permissions {
             do {
                 if try await checkLocalNetworkPermissionWithBonjour() {
                     // Permission is granted, continue to next onboarding step
-                    dependencies[defaults: .appGroup, key: .lastSeenHasLocalNetworkPermission] = true
+                    dependencies[singleton: .storage].writeAsync { db in
+                        db[.lastSeenHasLocalNetworkPermission] = true
+                    }
                 } else {
                     // Permission denied, explain why we need it and show button to open Settings
-                    dependencies[defaults: .appGroup, key: .lastSeenHasLocalNetworkPermission] = false
+                    dependencies[singleton: .storage].writeAsync { db in
+                        db[.lastSeenHasLocalNetworkPermission] = false
+                    }
                 }
             } catch {
                 // Networking failure, handle error
@@ -301,5 +316,30 @@ extension Permissions {
             browser.cancel()
         }
     }
+    
+    public static func requestPermissionsForCalls(
+        presentingViewController: UIViewController? = nil,
+        using dependencies: Dependencies
+    ) {
+        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
+            requestMicrophonePermissionIfNeeded(
+                presentingViewController: presentingViewController,
+                using: dependencies,
+                onAuthorized: { _ in
+                    DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
+                        requestCameraPermissionIfNeeded(
+                            presentingViewController: presentingViewController,
+                            using: dependencies,
+                            onAuthorized: { _ in
+                                DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
+                                    requestLocalNetworkPermissionIfNeeded(using: dependencies)
+                                }
+                            }
+                        )
+                    }
+                }
+            )
+        }
+    }
 }
 
diff --git a/SessionMessagingKit/Utilities/Preferences.swift b/SessionMessagingKit/Utilities/Preferences.swift
index b1779a051..99b55ed52 100644
--- a/SessionMessagingKit/Utilities/Preferences.swift
+++ b/SessionMessagingKit/Utilities/Preferences.swift
@@ -76,6 +76,10 @@ public extension Setting.BoolKey {
     /// Controls whether developer mode is enabled (this displays a section within the Settings screen which allows manual control of feature flags
     /// and system settings for better debugging)
     static let developerModeEnabled: Setting.BoolKey = "developerModeEnabled"
+    
+    /// There is no native api to get local network permission, so we need to modify the state and store in database to update UI accordingly.
+    /// Remove this in the future if Apple provides native api
+    static let lastSeenHasLocalNetworkPermission: Setting.BoolKey = "lastSeenHasLocalNetworkPermission"
 }
 
 // stringlint:ignore_contents
diff --git a/SessionUtilitiesKit/Types/UserDefaultsType.swift b/SessionUtilitiesKit/Types/UserDefaultsType.swift
index fec3de4ba..4918be39c 100644
--- a/SessionUtilitiesKit/Types/UserDefaultsType.swift
+++ b/SessionUtilitiesKit/Types/UserDefaultsType.swift
@@ -110,8 +110,8 @@ public extension UserDefaults.BoolKey {
     /// Indicates whether we had the microphone permission the last time the app when to the background
     static let lastSeenHasMicrophonePermission: UserDefaults.BoolKey = "lastSeenHasMicrophonePermission"
     
-    /// Indicates whether we had the local network permission
-    static let lastSeenHasLocalNetworkPermission: UserDefaults.BoolKey = "lastSeenHasLocalNetworkPermission"
+    /// Indicates whether we had asked for the local network permission
+    static let hasRequestedLocalNetworkPermission: UserDefaults.BoolKey = "hasRequestedLocalNetworkPermission"
 }
 
 public extension UserDefaults.DateKey {
diff --git a/SessionUtilitiesKit/Utilities/Permissions.swift b/SessionUtilitiesKit/Utilities/Permissions.swift
index d8f47f763..87e927c99 100644
--- a/SessionUtilitiesKit/Utilities/Permissions.swift
+++ b/SessionUtilitiesKit/Utilities/Permissions.swift
@@ -50,9 +50,4 @@ public enum Permissions {
                 return .unknown
         }
     }
-    
-    public static func localNetwork(using dependencies: Dependencies) -> Status {
-        let status: Bool = dependencies[defaults:.appGroup, key: .lastSeenHasLocalNetworkPermission]
-        return status ? .granted : .denied
-    }
 }