Skip to content

Instantly share code, notes, and snippets.

@bmc08gt
Last active November 12, 2022 20:00
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save bmc08gt/ff9610a20dcd43c6e6dd5cbd1797e73a to your computer and use it in GitHub Desktop.
Save bmc08gt/ff9610a20dcd43c6e6dd5cbd1797e73a to your computer and use it in GitHub Desktop.
@OptIn(ExperimentalComposeApi::class)
@Composable
fun ActivityResultRegistry.googleSignIn(userPrefs: UserPreferences): GoogleSignInState {
val context = ContextAmbient.current
val options: GoogleSignInOptions = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN).apply {
requestIdToken(context.getString(R.string.google_client_id))
requestEmail()
}.build()
val signInState: MutableState<UserState<*>> = remember {
val currentUser = userPrefs.user.value
mutableStateOf(
if (currentUser != null) {
UserState.LoggedIn(currentUser)
} else {
UserState.LoggedOut
}
)
}
val launcher = activityResultLauncher(SignInActivityContract()) {
GlobalScope.launch {
val result = runCatching { firebaseAuthWithGoogle(it) }
signInState.value = if (result.isSuccess) {
val user = result.getOrNull()
if (user != null) {
UserState.LoggedIn(user)
} else {
UserState.LoginError(Throwable("User is null"))
}
} else {
Log.d("sign-in", "googleSignIn: ${result.exceptionOrNull()}")
UserState.LoginError(result.exceptionOrNull())
}
}
}
return remember(launcher) {
GoogleSignInState(
userPrefs,
GoogleSignIn.getClient(context, options),
signInState,
launcher
)
}
}
private suspend fun firebaseAuthWithGoogle(account: GoogleSignInAccount?) = suspendCoroutine<FirebaseUser> { cont ->
val credential = GoogleAuthProvider.getCredential(account?.idToken, null)
FirebaseAuth.getInstance().signInWithCredential(credential)
.addOnCompleteListener { task ->
if (task.isSuccessful) {
// Sign in success, update UI with the signed-in user's information
val user = FirebaseAuth.getInstance().currentUser
if (user == null) {
cont.resumeWithException(Throwable("User was null"))
return@addOnCompleteListener
}
Log.d("sign-in", "firebaseAuthWithGoogle: ${user.metadata}")
cont.resume(user)
} else {
// If sign in fails, display a message to the user.
Log.e("sign-in", "signInWithCredential:failure", task.exception)
cont.resumeWithException(task.exception ?: Throwable("Failure"))
}
}
}
@Composable
fun AppContent(
appComponent: AppComponent,
authComponent: AuthComponent,
navigationViewModel: NavigationViewModel
) {
val signInState = ContextAmbient.current.findActivity()?.activityResultRegistry?.googleSignIn(authComponent.userPreferences)
Surface(color = MaterialTheme.colors.background) {
signInState?.let { state ->
when {
state.isLoggedIn() -> navigationViewModel.navigateTo(Screen.Home)
else -> navigationViewModel.navigateTo(Screen.Login)
}
}
when (val screen = navigationViewModel.currentScreen) {
Screen.Home -> HomeContent(signInState = signInState)
Screen.Login -> LoginScreen(signInState)
}
}
}
sealed class UserState<T> {
object LoggedOut: UserState<Nothing>()
object LoginRequested: UserState<Nothing>()
object LoggingIn : UserState<Nothing>()
data class LoggedIn(val user: FirebaseUser): UserState<FirebaseUser>()
data class LoginError(val exception: Throwable?): UserState<Throwable?>()
}
class GoogleSignInState(
private val userPreferences: UserPreferences,
private val googleSignInClient: GoogleSignInClient,
private var userState: MutableState<UserState<*>>,
private val launcher: ActivityResultLauncher<GoogleSignInClient>
) {
fun launchSignInRequest() {
userState.value = UserState.LoggingIn
launcher.launch(googleSignInClient)
}
fun login() {
userState.value = UserState.LoginRequested
}
fun logOut() {
userPreferences.logOut()
userState.value = UserState.LoggedOut
}
@Composable
fun isLoggedIn() = userState.value is UserState.LoggedIn
@Composable
fun isLoggingIn() = userState.value is UserState.LoggingIn
@Composable
fun isLoggedOut() = userState.value is UserState.LoggedOut
@Composable
fun wasLoginRequested() = userState.value is UserState.LoginRequested
}
@Composable
fun LoginScreen(signInState: GoogleSignInState?) {
signInState?.let { state ->
if (state.wasLoginRequested()) {
signInState.launchSignInRequest()
}
if (state.isLoggedOut()) {
LoginSplash(onLogin = { state.login() })
} else {
LoginInProgress()
}
}
}
@Composable
fun LoginRootContent(children: @Composable ConstraintLayoutScope.() -> Unit) {
Surface(
color = MaterialTheme.colors.secondary,
modifier = Modifier.fillMaxSize()
) {
ConstraintLayout {
val (logo, title, description) = createRefs()
Image(
modifier = Modifier.constrainAs(logo) {
start.linkTo(parent.start)
end.linkTo(parent.end)
top.linkTo(parent.top)
bottom.linkTo(title.top)
} + Modifier.padding(start = 64.dp, end = 64.dp, top = 64.dp),
contentScale = ContentScale.Inside,
asset = imageResource(R.drawable.waffle)
)
Text(
modifier = Modifier.constrainAs(title) {
start.linkTo(parent.start)
end.linkTo(parent.end)
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
},
text = AnnotatedString("Waffle"),
style = ThemeTypography.h5
)
children(this)
}
}
}
@Composable
fun LoginSplash(onLogin: () -> Unit) {
LoginRootContent {
val button = createRef()
Button(
modifier = Modifier.constrainAs(button) {
start.linkTo(parent.start)
end.linkTo(parent.end)
bottom.linkTo(parent.bottom, 32.dp)
},
backgroundColor = MaterialTheme.colors.surface,
contentColor = MaterialTheme.colors.onSurface,
onClick = { onLogin() }
) {
Image(asset = vectorResource(R.drawable.ic_google))
Text(text = "Sign in with Google")
}
}
}
@Composable
fun LoginInProgress() {
LoginRootContent {
val progress = createRef()
CircularProgressIndicator(modifier = Modifier.constrainAs(progress) {
start.linkTo(parent.start)
end.linkTo(parent.end)
bottom.linkTo(parent.bottom, 32.dp)
})
}
}
class SignInActivityContract : ActivityResultContract<GoogleSignInClient, GoogleSignInAccount?>(),
CoroutineScope by CoroutineScope(Dispatchers.Main) {
val auth = FirebaseAuth.getInstance()
override fun createIntent(context: Context, input: GoogleSignInClient): Intent {
return input.signInIntent
}
override fun parseResult(resultCode: Int, intent: Intent?): GoogleSignInAccount? {
val task = GoogleSignIn.getSignedInAccountFromIntent(intent)
try {
val account = task.getResult(ApiException::class.java) ?: return null
return account
} catch (e: ApiException) {
Log.w("sign-in", "Google sign in failed", e)
return null
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment