Facebook login with Jetpack Compose

Viewed 1863

I started working on a new project that is 100% written with Jetpack compose, meaning we don't have any fragments and we're also following the Single activity pattern.

Now I have to implement the Facebook login but I'm stuck since they're still using the deprecated onActivityResult instead of the new contract api.

Here's the documentation that I'm trying to follow, any help would be greatly appreciated.

Thank you all,

3 Answers

You have to wait this issue to be resolved.

For now you can pass callbackManager down from your activity to the Compose tree using CompositionLocalProvider, like this:

val LocalFacebookCallbackManager =
    staticCompositionLocalOf<CallbackManager> { error("No CallbackManager provided") }

class MainActivity : FragmentActivity() {
    private var callbackManager = CallbackManager.Factory.create();

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Theme {
                CompositionLocalProvider(
                    LocalFacebookCallbackManager provides callbackManager
                ) {
                    LoginScreen()
                }
            }
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        callbackManager.onActivityResult(requestCode, resultCode, data)
        super.onActivityResult(requestCode, resultCode, data)
    }
}

@Composable
fun LoginScreen() {
    val callbackManager = LocalFacebookCallbackManager.current
    DisposableEffect(Unit) {
        LoginManager.getInstance().registerCallback(
            callbackManager,
            object : FacebookCallback<LoginResult> {
                override fun onSuccess(loginResult: LoginResult) {
                    println("onSuccess $loginResult")
                }

                override fun onCancel() {
                    println("onCancel")
                }

                override fun onError(exception: FacebookException) {
                    println("onError $exception")
                }
            }
        )
        onDispose {
            LoginManager.getInstance().unregisterCallback(callbackManager)
        }
    }
    val context = LocalContext.current
    Button(onClick = {
        LoginManager.getInstance()
            .logInWithReadPermissions(context.findActivity(), Arrays.asList("public_profile"));
    }) {
        Text("FB Login")
    }
}

fun Context.findActivity(): Activity? = when (this) {
    is Activity -> this
    is ContextWrapper -> baseContext.findActivity()
    else -> null
}

More general solution is moving facebook logic into a view mode, and passing, then you have to create your own callback manager, something like this:

ActivityResultCallbackManager.kt

val LocalActivityResultCallbackManager =
    staticCompositionLocalOf<ActivityResultCallbackManager> { error("No ActivityResultCallbackManager provided") }

interface ActivityResultCallbackI {
    fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean
}

class ActivityResultCallbackManager {

    private val listeners = mutableListOf<ActivityResultCallbackI>()

    fun addListener(listener : ActivityResultCallbackI) {
        listeners.add(listener)
    }

    fun removeListener(listener : ActivityResultCallbackI) {
        listeners.remove(listener)
    }

    fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) : Boolean =
        listeners.any { it.onActivityResult(requestCode, resultCode, data) }
}

MainActivity.kt

class MainActivity : AppCompatActivity() {
    private var callbackManager = ActivityResultCallbackManager()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        WindowCompat.setDecorFitsSystemWindows(window, false)
        setContent {
            Theme {
                CompositionLocalProvider(
                    LocalActivityResultCallbackManager provides callbackManager
                ) {
                    LoginScreen()
                }
            }
        }
    }

    override fun onActivityResult(
        requestCode: Int,
        resultCode: Int,
        data: Intent?
    ) {
        if (!callbackManager.onActivityResult(requestCode, resultCode, data)) {
            super.onActivityResult(requestCode, resultCode, data)
        }
    }
}

FacebookLoginViewModel.kt

class FacebookLoginViewModel : ViewModel(), ActivityResultCallbackI {
    sealed class LoginState {
        object Initial: LoginState()
        object Processing: LoginState()
        data class Success(val loginResult: LoginResult): LoginState()
        data class Error(val exception: FacebookException): LoginState()
    }

    private var callbackManager = CallbackManager.Factory.create()
    var state by mutableStateOf<LoginState>(LoginState.Initial)
        private set

    init {
        LoginManager.getInstance().registerCallback(
            callbackManager,
            object : FacebookCallback<LoginResult> {
                override fun onSuccess(loginResult: LoginResult) {
                    state = LoginState.Success(loginResult)
                }

                override fun onCancel() {
                    state = LoginState.Initial
                }

                override fun onError(exception: FacebookException) {
                    state = LoginState.Error(exception)
                }
            }
        )
    }

    override fun onCleared() {
        super.onCleared()
        LoginManager.getInstance().unregisterCallback(callbackManager)
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean =
        callbackManager.onActivityResult(requestCode, resultCode, data)

    fun login(context: Context) {
        state = LoginState.Processing
        LoginManager.getInstance()
            .logInWithReadPermissions(context.findActivity(), Arrays.asList("public_profile"));
    }
}

LoginScreen.kt

@Composable
fun LoginScreen() {
    val viewModel: FacebookLoginViewModel = viewModel()
    val callbackManager = LocalActivityResultCallbackManager.current
    DisposableEffect(Unit) {
        callbackManager.addListener(viewModel)
        onDispose {
            callbackManager.removeListener(viewModel)
        }
    }
    val context = LocalContext.current
    Column {
        Text(viewModel.state.toString())
        Button(onClick = {
            viewModel.login(context)
        }) {
            Text("FB Login")
        }
    }
}

Also you can try building this fork, it contains changes from this pull request. It adds support of contract api, and is not yet accepted. Check out changes carefully, it's not official!

Try this its will work:

val callbackManager = CallbackManager.Factory.create()
val loginManager = LoginManager.getInstance()
val context = LocalContext.current

 loginManager.logIn(
     context as ActivityResultRegistryOwner,
     callbackManager,
     listOf("email")
 )

 loginManager.registerCallback(
     callbackManager,
     object : FacebookCallback<LoginResult?> {
         override fun onCancel() {
             TODO("onCancel")
         }

         override fun onError(error: FacebookException) {
             TODO("onError")
         }

         override fun onSuccess(result: LoginResult?) {
             TODO("onSuccess")
         }
   })

Another approach that worked for me is to encapsulate the FB login logic in a blank activity with no UI and start facebook login as soon as it starts. As soon as you get back the results in your activity pass them back to compose function back in step 2.

Step 1: Create FB Login Activity

package com.login.view.login

import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import com.facebook.CallbackManager
import com.facebook.FacebookCallback
import com.facebook.FacebookException
import com.facebook.login.LoginManager
import com.facebook.login.LoginResult

class FBLoginActivity : ComponentActivity() {

    private val callbackManager: CallbackManager by lazy {
        CallbackManager.Factory.create()
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setupLogin()
        doLogin()

    }

    private fun doLogin() {
        LoginManager.getInstance().logInWithReadPermissions(
            this,
            listOf("user_photos", "email", "public_profile", "AccessToken")
        )
    }

    private fun setupLogin() {
        LoginManager.getInstance()
            .registerCallback(callbackManager, callback = object : FacebookCallback<LoginResult> {
                override fun onCancel() {
                    Log.d("TAG++", "facebook login cancelled")
                    setResult(RESULT_CANCELED)
                    finish()
                }

                override fun onError(error: FacebookException) {
                    Log.d("TAG++", "facebook login error")
                    setResult(RESULT_ERROR)
                    finish()
                }

                override fun onSuccess(result: LoginResult) {
                    Log.d("TAG++", "facebook login success")
                    val intent = Intent().apply {
                        putExtra(EXTRA_DATA_FB, result.accessToken)
                    }
                    setResult(RESULT_OK, intent)
                    finish()
                }
            })
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        callbackManager.onActivityResult(requestCode, resultCode, data)
        super.onActivityResult(requestCode, resultCode, data)
    }

    companion object {

        const val RESULT_ERROR = 102
        const val EXTRA_DATA_FB = "extraDataFb"

        fun getInstance(context: Context): Intent {
            return Intent(context, FBLoginActivity::class.java)
        }
    }
}

Step 2: Now just call this with facebookSignRequest.launch(FBLoginActivity.getInstance(context)) from within your compose function like below.

@Composable
fun LoginViewComponent() {
    val context = LocalContext.current

    val facebookSignRequest =
        rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
            if (result.resultCode == Activity.RESULT_OK && result.data != null) {
                val data = result.data?.getStringExtra(FBLoginActivity.EXTRA_DATA_FB)
                //do something with data 
            }
        }

    OutlinedButton(
        onClick = {
            facebookSignRequest.launch(FBLoginActivity.getInstance(context))
        },
        shape = RoundedCornerShape(20),
        border = BorderStroke(1.dp, color = Color.White),
        modifier = Modifier
            .padding(8.dp)
            .height(60.dp)
    ) {
        Image(
            painter = painterResource(id = R.drawable.ic_fb),
            contentDescription = "facebook login",
            colorFilter = ColorFilter.tint(color = textPrimary.copy(alpha = .7f))
        )
    }
}

Step 3: don't forget to add activity to manifest

Related