Last active
October 11, 2023 03:43
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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") | |
} | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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