Skip to content

Instantly share code, notes, and snippets.

@erfansn
Last active October 11, 2023 03:43
Show Gist options
  • Save erfansn/60634517a788ae64eaae5cdba4448950 to your computer and use it in GitHub Desktop.
Save erfansn/60634517a788ae64eaae5cdba4448950 to your computer and use it in GitHub Desktop.
Using the Google Sign-In Api in Compose with the least possible friction
@Composable
fun GoogleAuthScreen() {
val googleAuthState = rememberGoogleAuthState(
clientId = stringResource(R.string.web_client_id),
Scope(Scopes.PROFILE),
Scope(Scopes.EMAIL)
)
DisposableEffect(googleAuthState) {
googleAuthState.onSignInResult = {
when (it) {
is GoogleAccountSignInResult.Error -> {
// TODO: Notify error to user
}
is GoogleAccountSignInResult.Success -> {
it.googleSignInAccount ?: TODO("Notify the user that must allow permissions")
}
}
}
onDispose {
googleAuthState.onSignInResult = null
}
}
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
when (googleAuthState.authStatus) {
AuthenticationStatus.PreSignedIn -> {
Text(text = "You are already signed in")
}
AuthenticationStatus.SignedIn -> {
Button(onClick = { googleAuthState.signOut() }) {
Text(text = "Sign-Out")
}
}
AuthenticationStatus.InProgress -> {
CircularProgressIndicator()
}
AuthenticationStatus.SignedOut -> {
Button(onClick = { googleAuthState.signIn() }) {
Text(text = "Sign-In with Google")
}
}
AuthenticationStatus.PermissionsNotGranted -> {
Text(text = "You need to grant permissions to continue")
Button(onClick = { googleAuthState.requestPermissions() }) {
Text(text = "Request permissions")
}
}
}
}
}
@Stable
interface GoogleAuthState {
val authStatus: AuthenticationStatus
var onSignInResult: ((GoogleAccountSignInResult) -> Unit)?
fun signIn()
fun requestPermissions()
fun signOut()
}
private class MutableGoogleAuthState(
clientId: String,
initStatus: AuthenticationStatus? = null,
private val context: Context,
private val scopes: List<Scope>,
) : GoogleAuthState {
private val googleSignInOptions = GoogleSignInOptions.Builder()
.requestIdToken(clientId)
.apply {
if (scopes.isEmpty()) return@apply
if (scopes.size == 1) {
requestScopes(scopes.single())
} else {
requestScopes(scopes.first(), *scopes.drop(1).toTypedArray())
}
}
.build()
private val googleSignIn = GoogleSignIn.getClient(context, googleSignInOptions)
override var authStatus by mutableStateOf(initStatus ?: AuthenticationStatus.InProgress)
private set
private val currentAccount get() = GoogleSignIn.getLastSignedInAccount(context)
override var onSignInResult: ((GoogleAccountSignInResult) -> Unit)? = null
init {
if (initStatus == null) {
authStatus = when {
currentAccount?.allPermissionsGranted == true -> {
AuthenticationStatus.PreSignedIn
}
currentAccount != null -> {
AuthenticationStatus.PermissionsNotGranted
}
else -> {
AuthenticationStatus.SignedOut
}
}
}
}
fun handleSigningToAccount(data: Intent?) {
try {
val result = GoogleSignIn.getSignedInAccountFromIntent(data).getResult(ApiException::class.java)
val account = if (!result.allPermissionsGranted) {
authStatus = AuthenticationStatus.PermissionsNotGranted
null
} else {
authStatus = AuthenticationStatus.SignedIn
result
}
onSignInResult?.invoke(GoogleAccountSignInResult.Success(account))
} catch (e: ApiException) {
Log.e("GoogleAuthState", e.message, e)
when (e.statusCode) {
GoogleSignInStatusCodes.SIGN_IN_FAILED -> onSignInResult?.invoke(GoogleAccountSignInResult.Error(R.string.sign_in_failed))
CommonStatusCodes.NETWORK_ERROR -> onSignInResult?.invoke(GoogleAccountSignInResult.Error(R.string.network_problem))
}
}
}
private val GoogleSignInAccount.allPermissionsGranted
get() = GoogleSignIn.hasPermissions(this, *scopes.toTypedArray())
var launcher: ManagedActivityResultLauncher<Intent, ActivityResult>? = null
override fun signIn() {
launcher?.launch(googleSignIn.signInIntent)
}
override fun requestPermissions() = signIn()
override fun signOut() {
authStatus = AuthenticationStatus.InProgress
googleSignIn.signOut().addOnCompleteListener {
authStatus = AuthenticationStatus.SignedOut
}
}
companion object {
fun Saver(
clientId: String,
context: Context,
scopes: List<Scope>,
) = Saver<MutableGoogleAuthState, AuthenticationStatus>(
save = { it.authStatus },
restore = { MutableGoogleAuthState(clientId, it, context, scopes) }
)
}
}
@Composable
fun rememberGoogleAuthState(
clientId: String,
vararg scopes: Scope,
): GoogleAuthState {
val context = LocalContext.current
val googleAuthState = rememberSaveable(clientId, saver = MutableGoogleAuthState.Saver(clientId, context, scopes.toList())) {
MutableGoogleAuthState(
context = context,
clientId = clientId,
scopes = scopes.toList(),
)
}
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
googleAuthState.handleSigningToAccount(it.data)
}
DisposableEffect(clientId) {
googleAuthState.launcher = launcher
onDispose {
googleAuthState.launcher = null
}
}
return googleAuthState
}
enum class AuthenticationStatus { PreSignedIn, SignedIn, InProgress, SignedOut, PermissionsNotGranted }
sealed class GoogleAccountSignInResult {
data class Error(@StringRes val messageId: Int) : GoogleAccountSignInResult()
data class Success(val googleSignInAccount: GoogleSignInAccount?) : GoogleAccountSignInResult()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment