Help Users Resolve Permission and Device Setting Errors

Once the user opts into the IQL program, you need to check for errors that might come up while specifying permission and device settings. We have explained how to check for these errors in the various scenarios as described in this section.

This section includes the following topics:

Help Users Resolve Permission and Device Setting Errors

Help Users Resolve Permission and Device Setting Errors

Help Users Resolve Permission and Device Setting Errors

Help Users Resolve Permission and Device Setting Errors

Help Users Resolve Permission and Device Setting Errors

Help Users Resolve Permission and Device Setting Errors

1.0. Listen To Zendrive SDK Callbacks for Permission Or Setting Changes

Use the following sections to listen to SDK callbacks for changes to permissions or settings:

1.1. Create a Permissions Manager

Use the following developer instructions to create a Permissions Manager, which will be responsible for checking device permission changes and settings errors. The Permissions Manager will also notify you whenever a user's permissions are changed, or when any setting errors occur.

enum PermissionErrorType {
    case locationNotAutherizedAlways
    case preciseLocationNotEnabled
    case locationServicesTurnedOff
    case notificationsTurnedOff
}
protocol PermissionManagerDelegate: AnyObject {
    func onLocationPermissionUpdated()
}
final class PermissionsManager {
    let locationPermission = PermissionFactory.locationPermission()
    let notificationPermission = PermissionFactory.notificationPermission()
    var permissionErrors = [PermissionErrorType]()
    var delegate: PermissionManagerDelegate?
    init() {
        NotificationCenter.default.addObserver(self,
                                               selector: #selector(handleAppForeground),
                                               name: UIApplication.didBecomeActiveNotification,
                                               object: nil)
    }
    func checkPermissionErrors() {
        self.permissionErrors.removeAll()
        if let permissionStatus = locationPermission.checkPermission(handler: nil) {
            switch permissionStatus {
            case .restricted(let restrictedStatus):
                switch restrictedStatus {
                case .locationServiceOff:
                    permissionErrors.append(.locationServicesTurnedOff)
                case .whileInUse:
                    permissionErrors.append(.locationNotAutherizedAlways)
                case .precisionOff:
                    permissionErrors.append(.preciseLocationNotEnabled)
                case .denied:
                    permissionErrors.append(.locationNotAutherizedAlways)
                case .unknown:
                    permissionErrors.append(.locationNotAutherizedAlways)
                }
            case .granted:
                break
            case .notDetermined:
                permissionErrors.append(.locationNotAutherizedAlways)
            }
        }
        checkNotificationPermission()
    }
    func checkNotificationPermission() {
        notificationPermission.checkPermission { notificationStatus in
            switch notificationStatus {
            case .notDetermined, .restricted:
                self.permissionErrors.append(.notificationsTurnedOff)
            case .granted:
                break
            }
            self.handlePermissionErrors(errors: self.permissionErrors)
        }
    }
    func getLocationPermissionStatus() -> GenericPermissionStatus<LocationPermissionStatus>? {
        return locationPermission.checkPermission(handler: nil)
    }
    func requestLocationPermission() {
        locationPermission.requestPermission { _ in
            DDLogDebug("requested location permission")
        }
    }
    func onLocationPermissionUpdated() {
        self.delegate?.onLocationPermissionUpdated()
    }
}
private extension PermissionsManager {
    @objc
    func handleAppForeground() {
        if Zendrive.isSDKSetup {
            if IQLRef.sharedInstance.loginRepository.isPermissionFlowCompleted {
                checkPermissionErrors()
            }
        }
    }
    func checkNotificationsPermissionStatus(handler:  @escaping (Bool) -> Void) {
        UNUserNotificationCenter.current().getNotificationSettings { settings in
            if settings.authorizationStatus == .authorized {
                handler(true)
            } else {
                handler(false)
            }
        }
    }
    func handlePermissionErrors(errors: [PermissionErrorType]) {
        var isForeground = true
        DispatchQueue.main.sync {
            isForeground = UIApplication.shared.applicationState == .active
        }
        if isForeground {
            let core = IQLRef.sharedInstance
            core.notifHelper.notifyPermissionErrors(errors) // Notify for in-app notifications
            self.permissionErrors.removeAll()
        } else {
            if errors.isEmpty { // Remove already shown displayed notifications if no errors
                let notificationManager = PermissionErrorUserNotificationManager()
                notificationManager.removeLocationDeniedNotifications()
                return
            }
            checkNotificationsPermissionStatus { isEnabled in
                if isEnabled {
                    DispatchQueue.main.sync {
                        let notificationManager = PermissionErrorUserNotificationManager()
                        notificationManager.showLocationDeniedNotification()
                    }
                }
            }
        }
    }
}

1.1.1.Create a Location Permission Class

Use the following code snippet to create a Location Permission Class to handle location permission errors:

enum LocationPermissionStatus {
    case denied
    case whileInUse
    case precisionOff
    case locationServiceOff
    case unknown
}
@available(iOS 14.0, *)
final class LocationPermissionForiOS14: NSObject, Permission, CLLocationManagerDelegate {
    let locationManager = CLLocationManager()
    var requestPermissionHandler: ((GenericPermissionStatus<LocationPermissionStatus>) -> Void)?
    override init() {
        super.init()
    }
    func checkPermission(handler: ((GenericPermissionStatus<LocationPermissionStatus>) -> Void)?) -> GenericPermissionStatus<LocationPermissionStatus>? {
        var permissionErrors: [PermissionErrorType]
        guard CLLocationManager.authorizationStatus() != .notDetermined else {
            return .notDetermined
        }
        guard CLLocationManager.authorizationStatus() != .denied else {
            return .restricted(.denied)
        }
        if let errors = Zendrive.getSettings()?.errors {
            permissionErrors = errors.compactMap({ error -> PermissionErrorType? in
                switch error.errorType {
                case .locationPermissionNotAuthorized:
                    return .locationNotAutherizedAlways
                case .locationAccuracyAuthorizationReduced:
                    return .preciseLocationNotEnabled
                case .locationServiceOff:
                    return .locationServicesTurnedOff
                case .activityPermissionNotAuthorized:
                    fatalError("Reached invalid case activityPermissionNotAuthorized")
                }
            })
            guard permissionErrors.isEmpty else {
                return getLocationError(error: permissionErrors)
            }
            return .granted
        }
        return nil
    }
    private func getLocationError(error: [PermissionErrorType]) -> (GenericPermissionStatus<LocationPermissionStatus>)? {
        if error.contains(.locationServicesTurnedOff) {
            return .restricted(.locationServiceOff)
        } else if error.contains(.locationNotAutherizedAlways) {
            return .restricted(.whileInUse)
        } else if error.contains(.preciseLocationNotEnabled) {
            return .restricted(.precisionOff)
        } else {
            return nil
        }
    }
    func requestPermission(handler: @escaping (GenericPermissionStatus<LocationPermissionStatus>) -> Void) {
        requestPermissionHandler = handler
        locationManager.requestWhenInUseAuthorization()
    }
}
@available(iOS 13.0, *)
final class LocationPermissionForiOS13: NSObject, Permission, CLLocationManagerDelegate {
    let locationManager = CLLocationManager()
    var requestPermissionHandler: ((GenericPermissionStatus<LocationPermissionStatus>) -> Void)?
    override init() {
        super.init()
        locationManager.delegate = self
    }
    func checkPermission(handler: ((GenericPermissionStatus<LocationPermissionStatus>) -> Void)?) -> GenericPermissionStatus<LocationPermissionStatus>? {
        var permissionErrors: [PermissionErrorType]
        guard CLLocationManager.authorizationStatus() != .notDetermined else {
            return .notDetermined
        }
        guard CLLocationManager.authorizationStatus() != .denied else {
            return .restricted(.denied)
        }
        if let errors = Zendrive.getSettings()?.errors {
            permissionErrors = errors.compactMap({ error -> PermissionErrorType? in
                switch error.errorType {
                case .locationPermissionNotAuthorized:
                    return .locationNotAutherizedAlways
                case .locationAccuracyAuthorizationReduced:
                    return .preciseLocationNotEnabled
                case .locationServiceOff:
                    return .locationServicesTurnedOff
                case .activityPermissionNotAuthorized:
                    fatalError("Reached invalid case activityPermissionNotAuthorized")
                }
            })
            guard permissionErrors.isEmpty else {
                return getLocationError(error: permissionErrors)
            }
            return .granted
        }
        return nil
    }
    private func getLocationError(error: [PermissionErrorType]) -> (GenericPermissionStatus<LocationPermissionStatus>)? {
        if error.contains(.locationServicesTurnedOff) {
            return .restricted(.locationServiceOff)
        } else if error.contains(.locationNotAutherizedAlways) {
            return .restricted(.whileInUse)
        } else if error.contains(.preciseLocationNotEnabled) {
            return .restricted(.precisionOff)
        } else {
            return nil
        }
    }
    func requestPermission(handler: @escaping (GenericPermissionStatus<LocationPermissionStatus>) -> Void) {
        requestPermissionHandler = handler
        locationManager.requestWhenInUseAuthorization()
    }
}j

1.1.2. Create a NotificationPermission Class

Use the following code snippet to create a Notification Permission Class to handle notification permission errors:

enum NotificationPermissionStatus {
    case denied
    case unknown
}
@available(iOS 13.0, *)
final class NotificationPermissionForiOS13: NSObject, Permission {
    typealias T = NotificationPermissionStatus
    let center = UNUserNotificationCenter.current()
    func requestPermission(handler: @escaping (GenericPermissionStatus<NotificationPermissionStatus>) -> Void) {
        let options: UNAuthorizationOptions = [.alert, .sound]
        center.requestAuthorization(options: options) { (granted: Bool, error: Error?) in
            if let err = error {
                DDLogError("Error while requesting notification permission: \(err.localizedDescription)")
                handler(.restricted(.unknown))
                return
            }
            if granted {
                handler(.granted)
            } else {
                handler(.restricted(.denied))
            }
        }
    }
    func checkPermission(handler: ((GenericPermissionStatus<NotificationPermissionStatus>) -> Void)?) -> GenericPermissionStatus<NotificationPermissionStatus>? {
        UNUserNotificationCenter.current().getNotificationSettings { settings in
            switch settings.authorizationStatus {
            case .notDetermined:
                handler?(.notDetermined)
            case .authorized:
                handler?(.granted)
            case .denied:
                handler?(.restricted(.denied))
            default:
                handler?(.restricted(.unknown))
            }
        }
        return nil
    }
}

1.2. Enable the Permissions Manager

Use the following developer instructions to listen to onSettingsChanged Zendrive SDK callback and inform PermissionsManager to pull the latest permissions status.

Pull the exact permission status using Zendrive SDK and OS APIs within Permissions Manager. This will include location permission status via Zendrive SDK and notifications via UNNotificationCenter API. Be sure to include the comment, 'Based on the latest permission status, add or remove notifications to, or from the notification center'.

extension ZendriveManager: ZendriveDelegate {
    ///Implement `settingsChanged` method from ZendriveDelegate to listen to any device or permission error callback from Zendrive SDK.
    func settingsChanged(_ settings: Settings) {
        core.permissionsManager.checkPermissionErrors() // core = IQLRef.sharedInstance
        core.permissionsManager.onLocationPermissionUpdated()
    }
}

2. Refresh Permissions and Device Settings Status

Use the instructions provided below to refresh permissions and device settings status periodically.

2.1. Pull the Latest Permission Status Whenever the Application Is Launched

//Create a singleton instance of "PermissionsManager" which in turn handles the application state changes to check if any permission errors
class IQLRef {
    static let sharedInstance = swIQLRef()
    var permissionsManager: PermissionsManager!
    var backgroundFetchManager: BackgroundFetchManager! // Used in next section
 

    func setup(with config: IQLConfig) {
        ...............
        initializeDependencies()
        ................
    }

    func initializeDependencies() {
           ....................
       
        permissionsManager = PermissionsManager()
        backgroundFetchManager = BackgroundFetchManager()

           ......................
        }
}

2.2. Integrate Background Fetch

Integrate Background Fetch to periodically wake up your application in the background and check for any permission errors that may have occurred.

AppDelegate Code Snippet

class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
                            .
                            .
                            .
                            .
//Invoke below methods of 'BackgroundFetchManager' to register for background app refresh and background processing, 'BackgroundFetchManager' handles checking for any permission errors whenever app wakes up in the background 
        if #available(iOS 13.0, *) {
            core.backgroundFetchManager.registerBackgroundAppRefersh() // core = IQLRef.sharedInstance
            core.backgroundFetchManager.registerBackgroundProcessing()

        } else {
            DDLogInfo("Cannot register BGTasks,OS version is lower than 13.0 ")
        }
                            .
                            .
                            .
                            .
        return true
    }
}

BackgroundFetchManager

Use this code snippet to pull the latest permission status on background fetch.

@available(iOS 13.0, *)
final class BackgroundFetchManager {
    private let bgAppRefreshIdentifier = "\(Bundle.main.bundleIdentifier!).background_app_refresh"
    private let bgProcessIdentifier = "\(Bundle.main.bundleIdentifier!).background_processing"
    
    func registerBackgroundAppRefersh() {
        BGTaskScheduler.shared.register(
            forTaskWithIdentifier: bgAppRefreshIdentifier,
            using: nil
        ) { task in
            DDLogInfo("Triggered BGAppRefreshTask on: \(Date())")
            self.scheduleAppRefresh()
            self.handleAppRefresh(task)
        }
        scheduleAppRefresh()
    }

    func registerBackgroundProcessing() {
        BGTaskScheduler.shared.register(
            forTaskWithIdentifier: bgProcessIdentifier,
            using: nil
        ) { task in
            DDLogInfo("Triggered BGProcessingTask on: \(Date())")
            self.scheduleBackgroundProcessing()
            self.handleAppRefresh(task)
        }
        scheduleBackgroundProcessing()
    }
}

private extension BackgroundFetchManager {
    func scheduleAppRefresh() {
        let request = BGAppRefreshTaskRequest(identifier: bgAppRefreshIdentifier)
        request.earliestBeginDate = Date(timeIntervalSinceNow: 6 * 60 * 60)
        do {
            try BGTaskScheduler.shared.submit(request)
            DDLogInfo("Scheduled a BGAppRefreshTask on: \(Date())")
        } catch {
            DDLogError("Could not schedule app refresh: \(error.localizedDescription)")
        }
    }

    func scheduleBackgroundProcessing() {
        let request = BGProcessingTaskRequest(identifier: bgProcessIdentifier)
        do {
            try BGTaskScheduler.shared.submit(request)
            DDLogInfo("Scheduled a BGProcessingTask on: \(Date())")
        } catch {
            DDLogError("Could not schedule background processing: \(error.localizedDescription)")
        }
    }


    func handleAppRefresh(_ task: BGTask) {
        let queue = OperationQueue()
        queue.maxConcurrentOperationCount = 1
        queue.addOperation {
            if IQLRef.sharedInstance.loginRepository.isLoggedIn {
                let core = IQLRef.sharedInstance
                core.permissionsManager.checkPermissionErrors()
            }
        }

        task.expirationHandler = {
            queue.cancelAllOperations()
            task.setTaskCompleted(success: true)
        }

        let lastOperation = queue.operations.last
        lastOperation?.completionBlock = {
            task.setTaskCompleted(success: true)
        }
    }
}

/* For testing background tasks while in development refer below link
 https://developer.apple.com/documentation/backgroundtasks/starting_and_terminating_tasks_during_development
 Note: Make sure app's buildConfiguration is Release config
 */

2.3. Integrate Silent Push Notification

Integrate silent heartbeat notifications to wake your application up in the background to check for any permission errors that may have occurred. If any errors are found, the user is notified.

class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
                            .
                            .
                            .
                            .
//Register a device to receive remote notifications
        UNUserNotificationCenter.current().delegate = self
        let center = UNUserNotificationCenter.current()
        center.requestAuthorization(options:[.badge, .alert, .sound]) { (granted, error) in
            guard granted else { return }
            DispatchQueue.main.async {
                UIApplication.shared.registerForRemoteNotifications()
            }
        }
                            .
                            .
                            .
                            .
        return true
    }


    //Check for permission errors when silent notification for permission error check is received
    func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
        DDLogInfo("Received remote notification with userInfo: \(userInfo)")
      if let data = userInfo["data"] as? [String: Any],
                    data["type"] as! String == "heartbeat" {
            IQLRef.sharedInstance.permissionsManager.checkPermissionErrors()
        }
    }
}

3.0. Handle Permissions and Setting Issues

Use the following instructions and UX designs to handle permissions and setting issues, for the following scenarios:

  • Location setting is turned Off.

  • Location not set to Always.

  • Location not set to Precise.

  • Present notifications experience to fix the permission issues outside the application.

Location Setting Is Turned Off

Location Not Set to 'Always'

Location Not Set To 'Precise'

4.0. Handle Notification Permission Issues

The following code snippet demonstrates how to check for notification permission errors and display them within the app using in-app notifications:

5.0. IQL Reference Application UX Design

Here is the overall design implementation for the IQL Reference Application:

6.0. Publisher Integration Testing Checklist

Follow the checklist given below to test user's permission and device setting error experience:

  • Permission Errors: Validate that the application displays an error whenever a required permission is missing. Additionally, check that a notification is displayed asking the user to fix the permission issue.