moko-permissions icon indicating copy to clipboard operation
moko-permissions copied to clipboard

Permission Request Not Dispatching `onSuccess()` on first attempt on iOS

Open angelacassanelli opened this issue 1 year ago • 6 comments

Hi,

I'm developing a Kotlin Multiplatform project with native Google Maps integration. The location permission request works perfectly on Android but has issues on iOS.

Devices tested:

  • iPhone 15 Pro (iOS 17)
  • iPhone 12 (iOS 17)

Issue:

When the location permission is requested on iOS in viewDidLoad of MapsViewController, the permission dialog is displayed: even if permission is granted by the user, the onSuccess() event is not dispatched during the first attempt. If the MapsViewController is reopened, permission is already granted, and onSuccess() is correctly triggered.

It seems that permissionsController.providePermission(permission) is being called in the PermissionViewModel without any exceptions, but eventsDispatcher.dispatchEvent { onSuccess() } is not executed.

Expected behavior:

  • When the permission is granted, onSuccess() should be dispatched immediately during the first open of the MapsViewController.

Current behavior:

  • onSuccess() is only dispatched on subsequent opens of the MapsViewController, after permissions have already been granted.

Logs:

  • First open:
D/MapsViewController: viewDidLoad
D/MapsViewModel: requestPermission called
D/MapsViewModel: pre provide NotDetermined
  • Second open:
D/MapsViewController: viewDidLoad
D/MapsViewModel: requestPermission called
D/MapsViewModel: pre provide Granted
D/MapsViewController: Maps permissions granted
D/MapsViewController: setupMapView called
D/MapsViewModel: post provide Granted

Relevant Code:

PermissionViewModel:

class PermissionViewModel(
    override val eventsDispatcher: EventsDispatcher<EventListener>,
    val permissionsController: PermissionsController,
    private val permissionType: Permission
) : ViewModel(), EventsDispatcherOwner<PermissionViewModel.EventListener> {

    val permissionState = MutableStateFlow(PermissionState.NotDetermined)

    init {
        viewModelScope.launch {
            permissionState.update { permissionsController.getPermissionState(permissionType) }
            println(permissionState)
        }
    }

    fun onRequestPermission() {
        requestPermission(permissionType)
    }

    private fun requestPermission(permission: Permission) {
        LogHelper().logDebug("requestPermission called", "PermissionViewModel")
        viewModelScope.launch {
            try {
                permissionsController.getPermissionState(permission)
                    .also {
                        LogHelper().logDebug("pre provide $it", "PermissionViewModel")
                    }
                // Calls suspend function in a coroutine to request some permission.
                permissionsController.providePermission(permission)
                // If there are no exceptions, permission has been granted successfully.
                eventsDispatcher.dispatchEvent { onSuccess() }
            } catch (deniedAlwaysException: DeniedAlwaysException) {
                eventsDispatcher.dispatchEvent { onDeniedAlways(deniedAlwaysException) }
            } catch (deniedException: DeniedException) {
                eventsDispatcher.dispatchEvent { onDenied(deniedException) }
            } finally {
                permissionState.update {
                    permissionsController.getPermissionState(permission)
                        .also {
                            LogHelper().logDebug("post provide $it", "PermissionViewModel")
                        }
                }
            }
        }
    }

    interface EventListener {
        fun onSuccess()
        fun onDenied(exception: DeniedException)
        fun onDeniedAlways(exception: DeniedAlwaysException)
    }
}

MapsViewController:

@OptIn(ExperimentalForeignApi::class)
class MapsViewController : UIViewController(nibName = null, bundle = null), GMSMapViewDelegateProtocol {

    private lateinit var mapView: GMSMapView
    private val defaultLatitude = 41.12
    private val defaultLongitude = 16.87
    private val zoomLevel: Float = 15.0f

    override fun viewDidLoad() {
        super.viewDidLoad()
        LogHelper().logDebug("viewDidLoad", "MapsViewController")

        val permissionHandler = PermissionHandler(
            onPermissionGranted = { onPermissionGranted() },
            onPermissionDenied = { onPermissionDenied() },
            onPermissionDeniedAlways = { onPermissionDeniedAlways() }
        )

        val viewModel = PermissionViewModel(
            eventsDispatcher = EventsDispatcher(listener = permissionHandler),
            permissionsController = PermissionsController(),
            permissionType = Permission.LOCATION
        )

        viewModel.onRequestPermission()
    }

    private fun onPermissionGranted() {
        LogHelper().logDebug("Maps permissions granted", "MapsViewController")
        setupMapView()
    }

    private fun onPermissionDenied() {
        LogHelper().logDebug("Maps permissions denied", "MapsViewController")
        // TODO: handle permission denied
    }

    private fun onPermissionDeniedAlways() {
        LogHelper().logDebug("Maps permissions denied always", "MapsViewController")
        // TODO: handle permission denied always
    }

    private fun setupMapView() {
        LogHelper().logDebug("setupMapView called", "MapsViewController")
        val camera = GMSCameraPosition.cameraWithLatitude(defaultLatitude, defaultLongitude, zoomLevel)
        mapView = GMSMapView.mapWithFrame(view.bounds, camera)
        mapView.delegate = this
        mapView.settings.myLocationButton = true
        mapView.myLocationEnabled = true
        view.addSubview(mapView)
        mapView.autoresizingMask = (UIViewAutoresizingFlexibleWidth or UIViewAutoresizingFlexibleHeight)
    }
}

PermissionHandler:

class PermissionHandler(
    private val onPermissionGranted: () -> Unit,
    private val onPermissionDenied: () -> Unit,
    private val onPermissionDeniedAlways: () -> Unit
) : PermissionViewModel.EventListener {

    override fun onSuccess() {
        onPermissionGranted()
    }

    override fun onDenied(exception: DeniedException) {
        onPermissionDenied()
    }

    override fun onDeniedAlways(exception: DeniedAlwaysException) {
        onPermissionDeniedAlways()
    }
}

Could you please help identify why onSuccess() is not dispatched on the first attempt?

Thank you!

angelacassanelli avatar Sep 20 '24 10:09 angelacassanelli

Same thing here. Seems to be an internal crash or stalling of the library - cause the app is not proceeding after providePermission() call the first time. Checked the coroutine that's launching the flow - it's running normally and is not being killed.

nsmirosh avatar Dec 07 '24 11:12 nsmirosh

@angelacassanelli I don't know if it's still relevant to you - but this is how I overcame the bug:

    private suspend fun requestLocationPermissions(
        onSuccess: suspend () -> Unit = {},
        onDenied: () -> Unit = {}
    ) {
        if (permissionsController.getPermissionState(Permission.COARSE_LOCATION) == PermissionState.Granted) {
            onSuccess()
            return
        }

        //TODO: Hack to overcome the moko libraries' bug for ios first-time permission requst
        screenModelScope.launch(Dispatchers.IO) {
            while (permissionsController.getPermissionState(Permission.COARSE_LOCATION) != PermissionState.Granted) {
                delay(200)
            }
            onSuccess()
        }
        try {
            permissionsController.providePermission(Permission.COARSE_LOCATION)
        } catch (e: Exception) {
            Logger.e("Error requesting location permissions: ${e.message}")
            onDenied()
        }
    }

It seems like something is blocking the thread inside the library - so that's why it's not proceeding further.

nsmirosh avatar Dec 07 '24 11:12 nsmirosh

I'm facing a similar issue: code after controller.providePermission(Permission.GALLERY) is never called, even when user grants access. This only happens on iOS

gregriggins36 avatar Jan 31 '25 02:01 gregriggins36

Same here, 0.19.1 not working for LOCATION for iOS for the first time.

comm1x avatar May 06 '25 18:05 comm1x

Same here, not working for Location in iOS for the 1st time.

alirezaeiii avatar Jun 10 '25 10:06 alirezaeiii

Same here, not working on first attempt

jarg-147 avatar Jul 02 '25 19:07 jarg-147