Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Permission Request Not Dispatching onSuccess() on first attempt on iOS #129

Open
angelacassanelli opened this issue Sep 20, 2024 · 2 comments

Comments

@angelacassanelli
Copy link

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!

@nsmirosh
Copy link

nsmirosh commented Dec 7, 2024

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
Copy link

nsmirosh commented Dec 7, 2024

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants