sgireddy
sgireddy

Reputation: 185

@composable invocations can only happen from the context of a @composable function while work around lack of desktop support for moko:biometry-compose

I am trying to work around the lack of desktop support from dev.icerock.moko:biometry-compose, I am not able to compile desktop version while this library is a part of commonMain. The idea is to push down the library to Android & Native (ios). I tried expect/actual and interface options. But I ran into this in both cases. "@composable invocations can only happen from the context of a @composable function". Any ideas?

interface BiometryAuthenticator {
    suspend fun checkBiometryAuthentication(
        requestTitle: String,
        requestReason: String,
        failureButtonText: String,
        allowDeviceCredentials: Boolean
    ): Boolean
}

expect class BiometryAuthenticatorFactory() {
    @Composable
    fun createBiometryAuthenticator(): BiometryAuthenticator
}

// AndroidBiometryAuthenticatorFactory.kt (androidMain)
package domain

import androidx.compose.runtime.Composable
import dev.icerock.moko.biometry.compose.rememberBiometryAuthenticatorFactory
import dev.icerock.moko.resources.desc.Raw
import dev.icerock.moko.resources.desc.StringDesc
import dev.icerock.moko.biometry.BiometryAuthenticator as MokoBiometryAuthenticator

actual class BiometryAuthenticatorFactory {

    @Composable
    actual fun createBiometryAuthenticator(): BiometryAuthenticator {
        val factory = rememberBiometryAuthenticatorFactory()
        return MokoBiometryAuthenticatorWrapper(factory.createBiometryAuthenticator())
    }

    private class MokoBiometryAuthenticatorWrapper(
        private val authenticator: MokoBiometryAuthenticator
    ) : BiometryAuthenticator {
        override suspend fun checkBiometryAuthentication(
            requestTitle: String,
            requestReason: String,
            failureButtonText: String,
            allowDeviceCredentials: Boolean
        ): Boolean {
            return authenticator.checkBiometryAuthentication(
                requestTitle = StringDesc.Raw(requestTitle),
                requestReason = StringDesc.Raw(requestReason),
                failureButtonText = StringDesc.Raw(failureButtonText),
                allowDeviceCredentials = allowDeviceCredentials
            )
        }
    }
}

  @Composable
  fun LoginScreen() {
    val viewModel = getViewModel(
            key = "biometry-screen",
            factory = viewModelFactory {
                AuthViewModel(                  
                    biometryAuthenticatorFactory.createBiometryAuthenticator() <-- It breaks here 
                )
            }
        ) {
    Column() {
            BindBiometryAuthenticatorEffect(viewModel.biometryAuthenticator) <-- and here....
    }

Upvotes: 2

Views: 91

Answers (2)

sgireddy
sgireddy

Reputation: 185

expect class BiometricAuth() {
    @Composable
    fun authenticate(onSuccess: () -> Unit, onError: (Exception) -> Unit)
}

Android:

actual class BiometricAuth actual constructor() {

    @Composable
    actual fun authenticate(onSuccess: () -> Unit, onError: (Exception) -> Unit) {
        val biometryAuthenticator = BiometryAuthenticator(appContext)

        val viewModel: AndroidAuthViewModel = getViewModel(
            key = "biometry-screen",
            factory = viewModelFactory { AndroidAuthViewModel(biometryAuthenticator)
        })

        Column() {
            BindBiometryAuthenticatorEffect(viewModel.biometryAuthenticator)
            viewModel.tryToAuth(onSuccess, onError)
        }
    }
}

class MainActivity : FragmentActivity() { // ComponentActivity() {
    companion object {
        lateinit var appContext: Context
            private set
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        appContext = applicationContext
        try {
            setContent {
                App()
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }
}

class AndroidAuthViewModel(val biometryAuthenticator: BiometryAuthenticator): ViewModel() {
    fun tryToAuth(onSuccess: () -> Unit, onError: (Exception) -> Unit) = viewModelScope.launch {
        val isSuccess = biometryAuthenticator.checkBiometryAuthentication(
            requestTitle = "Biometry".desc(),
            requestReason = "Invisible App needs your authentication".desc(),
            failureButtonText = "Oops".desc(),
            allowDeviceCredentials = true
        )
        if (isSuccess) {
            onSuccess()
        } else {
            onError(RuntimeException("An error occurred"))
        }
    }
}

iOS

actual class BiometricAuth actual constructor() {
    @Composable
    actual fun authenticate(onSuccess: () -> Unit, onError: (Exception) -> Unit) {
        val biometryAuthenticator = BiometryAuthenticator()
        val viewModel: IOSAuthViewModel = remember { IOSAuthViewModel(biometryAuthenticator) }
        viewModel.tryToAuth(onSuccess, onError)
    }
}
class IOSAuthViewModel(private val biometryAuthenticator: BiometryAuthenticator) {
    private val mainScope = MainScope()
    fun tryToAuth(onSuccess: () -> Unit, onError: (Exception) -> Unit) {
        mainScope.launch {
            try {
                val isSuccess = biometryAuthenticator.checkBiometryAuthentication(
                    requestTitle = "Biometry".desc(),
                    requestReason = "Invisible App needs your authentication".desc(),
                    failureButtonText = "Oops".desc(),
                    allowDeviceCredentials = true
                )
                if (isSuccess) {
                    onSuccess()
                } else {
                    onError(RuntimeException("An error occurred"))
                }
            } catch (e: Exception) {
                onError(e)
            }
        }
    }
}

App

@Composable
@Preview
fun App() {
    MaterialTheme {
        var showContent by remember { mutableStateOf(false) }
        var errorContent by remember { mutableStateOf("") }
        Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
            val auth = BiometricAuth()
            auth.authenticate(
                onSuccess = {
                    showContent = true
                },
                onError = { err ->
                    errorContent = err.message.toString()
                }
            )
            Text("The value of error content is $errorContent")
            AnimatedVisibility(showContent) {
                val greeting = remember { Greeting().greet() }
                Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
                    Image(painterResource(Res.drawable.compose_multiplatform), null)
                    Text("Compose: $greeting")
                }
            }
        }
    }
}

Upvotes: 0

Phil Dukhov
Phil Dukhov

Reputation: 88192

You can move createBiometryAuthenticator outside of viewModelFactory:

val biometryAuthenticator = biometryAuthenticatorFactory.createBiometryAuthenticator()

val viewModel = getViewModel(
    key = "biometry-screen",
    factory = viewModelFactory {
        AuthViewModel(                  
            biometryAuthenticator
        )
    }
) {

To make it more compose-like, I would suggest you rename the method to rememberBiometryAuthenticator, and actually remember the value - otherwise the object will be created on each recomposition, and that is not much expected since the factory will not use the new value.

@Composable
actual fun rememberBiometryAuthenticator(): BiometryAuthenticator {
    val factory = rememberBiometryAuthenticatorFactory()
    return remember { MokoBiometryAuthenticatorWrapper(factory.createBiometryAuthenticator()) }
}

Upvotes: 1

Related Questions